@harmonyos-arkts/d2h 0.1.27 → 0.1.29
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/AGENTS.md +2 -0
- package/README.md +43 -3
- package/agents/d2h.md +16 -14
- package/dist/bin/install-opencode.js +28 -23
- package/dist/src/install-codex.js +5 -126
- package/dist/src/install-skills.js +147 -0
- package/dist/src/install-vscode.js +2 -2
- package/dist/src/select-agent.js +207 -0
- package/docs/environment-check.md +3 -3
- package/package.json +6 -6
- package/skills/arkts-mvvm-architecture/SKILL.md +2 -0
- package/skills/d2h-architecture-plan/SKILL.md +19 -26
- package/skills/d2h-architecture-plan/references/architecture-plan.example.json +80 -315
- package/skills/d2h-architecture-plan/references/directory-creation.md +27 -34
- package/skills/d2h-architecture-plan/references/hybrid-design.md +13 -15
- package/skills/d2h-architecture-plan/references/json-contract.md +29 -91
- package/skills/d2h-architecture-plan/references/layout-reference.md +30 -27
- package/skills/d2h-architecture-plan/references/plan-contract.md +27 -34
- package/skills/d2h-architecture-plan/references/source-review.md +1 -1
- package/skills/d2h-architecture-plan/scripts/architecture_plan.py +29 -163
- package/skills/d2h-architecture-plan/scripts/test_architecture_plan.py +205 -342
- package/skills/d2h-spec-develop/SKILL.md +5 -4
- package/{install-opencode.sh → tooling/install-opencode.sh} +3 -3
- package/{opencode → tooling/opencode}/interactive-tools/advance-progress-demo.mjs +1 -1
- package/{opencode → tooling/opencode}/interactive-tools/demo.mjs +3 -3
- package/{opencode → tooling/opencode}/interactive-tools/prepare-demo.mjs +1 -1
- package/{scripts → tooling/scripts}/check-environment.mjs +3 -3
- package/skills/d2h-architecture-plan/references/selection-flow.md +0 -71
- /package/{opencode → tooling/opencode}/agents.json +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/browser-host.ts +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/core.ts +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/demo-workflow.ts +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/package-lock.json +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/package.json +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/panel.ts +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/prepare-progress-demo.mjs +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/progress.ts +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/server-legacy.mjs +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/server.mjs +0 -0
- /package/{opencode → tooling/opencode}/interactive-tools/tui.tsx +0 -0
- /package/{opencode → tooling/opencode}/vscode-d2h-package.json +0 -0
- /package/{opencode → tooling/opencode}/vscode-d2h-plugin.mjs +0 -0
package/AGENTS.md
CHANGED
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
处理迁移请求时,由当前正式主 Agent `agents/d2h.md` 统一编排专项 Skill。`agents/android-to-hmos-00-orchestrator.md`仅保留旧名称兼容,不是规则真源。
|
|
4
4
|
|
|
5
5
|
以真实 HarmonyOS 应用质量为目标,以 AppGraph facts 和迁移报告降低遗漏风险。不要恢复已废弃的旧 SPEC/PLAN/TASK 多 Agent 文档流程或静态转译器;当前使用的 Kernel Task Plan(`droid2hmos task plan/validate/next/show` 派生计划)属于 Migration Kernel 协议,不在此列。
|
|
6
|
+
|
|
7
|
+
`agents/`、`skills/` 用于迁移流程开发;安装器、宿主集成、检查脚本和对应测试集中在 `tooling/`。流程契约与运行支持仍在 `contracts/`、`schemas/`、`workflow/`。构建、检查和测试命令在仓库根目录运行。
|
package/README.md
CHANGED
|
@@ -20,7 +20,15 @@
|
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
不传 `--agent` 时,在终端用上下键选择安装目标,回车确认,也可输入序号或名称;前两项固定为 OpenCode、VS Code,不显示命令名或路径。检测到工具命令或配置目录时显示“可配置”,否则显示“未检测到”;VS Code 以发现鸿蒙插件为准。检测结果不代替安装校验。未检测到的工具仍可手动选择。
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx --yes @harmonyos-arkts/d2h@latest install --force
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
CI、管道等非交互环境必须显式传入 `--agent`。`upgrade` 同样支持交互选择,`doctor` 保留默认检查 OpenCode 的行为。
|
|
30
|
+
|
|
31
|
+
直接安装到 OpenCode:
|
|
24
32
|
|
|
25
33
|
```bash
|
|
26
34
|
npx --yes @harmonyos-arkts/d2h@latest install --agent opencode --force
|
|
@@ -104,10 +112,42 @@ npx --yes @harmonyos-arkts/d2h@latest upgrade --check --agent codex
|
|
|
104
112
|
npx --yes @harmonyos-arkts/d2h@latest doctor --agent codex
|
|
105
113
|
```
|
|
106
114
|
|
|
107
|
-
默认安装到 `~/.
|
|
115
|
+
默认安装到 `~/.codex/skills/`;设置 `CODEX_HOME` 时安装到其 `skills/` 子目录,可用 `--config-dir <path>` 指定安装根目录(Skills 位于其 `skills/` 子目录)。新建 Codex 对话后调用 `$d2h` 开始或恢复迁移。迁移主入口由 `agents/d2h.md` 生成同名 Skill,专项 Skills 保留完整资源;`--link` 仅链接专项 Skills,主入口始终生成副本。安装器不改写 Codex 的 `config.toml` 或 `AGENTS.md`;模型、审批和沙箱权限由当前 Codex 会话配置继承。OpenCode 指标插件的 `--remote` 不适用于 Codex。
|
|
116
|
+
|
|
117
|
+
## Qoder、TRAE、Claude、DSH、ZCode、CodeBuddy、WorkBuddy
|
|
118
|
+
|
|
119
|
+
使用 `--agent` 选择目标,无需指定 `--config-dir`:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx --yes @harmonyos-arkts/d2h@latest install --agent qoder --force
|
|
123
|
+
npx --yes @harmonyos-arkts/d2h@latest upgrade --check --agent qoder
|
|
124
|
+
npx --yes @harmonyos-arkts/d2h@latest doctor --agent qoder
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
将 `qoder` 替换为下表对应参数即可。各目标仅写入自己的技能目录及安装记录,主入口由 `agents/d2h.md` 生成 `skills/d2h/SKILL.md`,不改写宿主设置或安装 OpenCode 插件。
|
|
128
|
+
|
|
129
|
+
| 工具 | `--agent` | 默认技能目录 | 调用入口 |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| [Qoder](https://docs.qoder.com/cli/Skills) | `qoder` | `~/.qoder/skills/` | `/d2h` |
|
|
132
|
+
| [TRAE](https://docs.trae.ai/ide/skills) | `trae` | `~/.trae/skills/` | 在技能列表选择 d2h,或要求使用 d2h 技能 |
|
|
133
|
+
| [TRAE 国内版](https://docs.trae.cn/ide_skills?lang=zh) | `trae-cn` | `~/.trae-cn/skills/` | 同上 |
|
|
134
|
+
| [Claude Code](https://code.claude.com/docs/en/skills) | `claude` | `~/.claude/skills/` | `/d2h` |
|
|
135
|
+
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/skills.md) | `dsh` | `~/.dsh/skills/` | `/d2h` |
|
|
136
|
+
| [ZCode](https://zcode.z.ai/en/docs/skill) | `zcode` | `~/.zcode/skills/` | `$d2h` |
|
|
137
|
+
| [CodeBuddy](https://www.codebuddy.ai/docs/cli/skills) | `codebuddy` | `~/.codebuddy/skills/` | `/d2h` |
|
|
138
|
+
| [WorkBuddy](https://www.workbuddy.cn/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/Skills-Market) | `workbuddy` | `~/.workbuddy/skills/` | 在技能列表选择 d2h,或要求使用 d2h 技能 |
|
|
139
|
+
|
|
140
|
+
Claude Code、DSH 分别遵循 `CLAUDE_CONFIG_DIR`、`DSH_HOME`;`--config-dir` 优先于环境变量和默认目录。不同客户端版本的本地目录可能不同,可通过 `--config-dir` 指向实际技能目录的父目录。`--link`、`--dry-run`、`--force` 同 Codex;`--remote` 仅用于 OpenCode 系宿主,在这些工具上会提前拒绝。安装后新建会话查看技能列表;文件安装及校验通过不代表已完成各宿主内的迁移流程验收。
|
|
108
141
|
|
|
109
142
|
## 公共产物与执行工具
|
|
110
143
|
|
|
111
144
|
`d2h workflow --help` 查看能力,`d2h workflow contract artifacts` 查看产物所有权。运行时与契约随 D2H 包分发,不再位于规划 Skill 内;只复制或删除某个 Skill 不影响公共工具。Python 解释器可用 `D2H_PYTHON` 显式指定。详见 [工作流解耦说明](docs/workflow-decoupling.md)。
|
|
112
145
|
|
|
113
|
-
增量流程可先用 `d2h-spec-plan` 输出可评审波次计划,再由原 `d2h-spec-generate` 的 `from-plan` 模式展开所选成员,旧独立生成模式保留。新计划以 `spec_generation_policy=required-v1` 对未生成或失效正文阻止派发;评审用 `d2h workflow validate --planning-only`,生成交接见 `d2h workflow contract generation`。主入口 `d2h.md` 由人工适配。
|
|
146
|
+
增量流程可先用 `d2h-spec-plan` 输出可评审波次计划,再由原 `d2h-spec-generate` 的 `from-plan` 模式展开所选成员,旧独立生成模式保留。新计划以 `spec_generation_policy=required-v1` 对未生成或失效正文阻止派发;评审用 `d2h workflow validate --planning-only`,生成交接见 `d2h workflow contract generation`。主入口 `d2h.md` 由人工适配。
|
|
147
|
+
## 开发目录
|
|
148
|
+
|
|
149
|
+
- `agents/`、`skills/`:迁移主流程与专项技能。
|
|
150
|
+
- `tooling/`:安装器(`bin/`、`src/`)、宿主集成(`opencode/`)、检查与发布脚本(`scripts/`)及测试(`test/`)。
|
|
151
|
+
- `contracts/`、`schemas/`、`workflow/`:流程契约、数据格式与运行支持。
|
|
152
|
+
|
|
153
|
+
在仓库根目录运行 `npm run check` 和 `npm test`;编译产物仍输出到 `dist/`,CLI 入口保持不变。
|
package/agents/d2h.md
CHANGED
|
@@ -40,7 +40,7 @@ description: Android 应用到 HarmonyOS 的 AI 驱动迁移主 Agent;按迁
|
|
|
40
40
|
| 根据已确认功能范围与已有页面任务规划里程碑和波次 | `d2h-spec-plan` |
|
|
41
41
|
| 按固定 Spec 范围串行实施、构建、检查并返回提交与证据 | `d2h-spec-develop` |
|
|
42
42
|
| 根据 Android 工程结构与必要源码做目录级架构规划,详列推荐方案并对照另两套;确认并要求创建后落地目录壳子 | `d2h-architecture-plan` |
|
|
43
|
-
|
|
|
43
|
+
| 落实模块内 MVVM 分层、状态管理及 ArkTS 编码规范 | `arkts-mvvm-architecture` |
|
|
44
44
|
| 生成或执行 Python Hypium 端到端功能测试 | `d2h-hypium-forge` |
|
|
45
45
|
| 迁移应用名称、图标、版本号、包名等身份信息 | `arkts-app-identity` |
|
|
46
46
|
| 迁移用户可见文案与多语言资源,或清理硬编码文案 | `arkts-i18n` |
|
|
@@ -63,13 +63,20 @@ description: Android 应用到 HarmonyOS 的 AI 驱动迁移主 Agent;按迁
|
|
|
63
63
|
### 二、功能清单生成
|
|
64
64
|
|
|
65
65
|
1. 加载 `d2h-app-solution`,直接读取 Android 源码完成功能清单、版本阶段及逐功能技术栈、三方 SDK/SO 和权限分析,产物写入 `.migration/feature-analysis/`,生成 `inventory.json` 功能清单。
|
|
66
|
+
2. 功能范围确认后,评估已选功能是否实际依赖测试账号、环境地址、白名单、权限及第三方配置,仅将必需项作为前置条件;可选项允许留空,不阻塞流程,无需外部条件时不创建模板或要求填写。按需在迁移工作区的 `.migration/private/test-credentials.json`(账号、密码、API Key 等凭据)和 `test-environment.json`(环境、账号标识、准备事项)创建占位模板;先确认该私有目录被所在 Git 仓库忽略,不覆盖已有文件。告知开发者绝对路径、需填字段和受影响功能,请其本地填写后回复“已填写”;已有有效配置直接复用,不等待无关条件阻塞后续准备。
|
|
67
|
+
3. 凭据仅供实际登录或测试步骤通过本地文件读取,不进入提示词、日志、Spec、账本、看板或测试代码。账本及看板只记录文件位置、账号标识、准备状态和影响;验证码、扫码等临时操作在实际登录时请求配合,不长期保存。
|
|
66
68
|
|
|
67
69
|
### 三、HarmonyOS 工程架构设计
|
|
68
70
|
|
|
69
71
|
1. 加载 `d2h-architecture-plan`,输入 Android 工程目录、已确认功能范围和第二阶段已有功能分析作为定位线索。遵循该 Skill 的默认 Android 阅读范围、鸿蒙工程结合选择及目录级分析深度,不另做全量扫描或完整业务链审计。
|
|
70
|
-
2.
|
|
71
|
-
3.
|
|
72
|
+
2. 按该 Skill 在对话中比较三套目录结构、职责与取舍,说明推荐理由并取得用户实际选择,处理修订意见;已有当前有效选择时直接沿用。用户回答完成后,写入 `.migration/architecture-plan/architecture-plan.md`(详列三套目录并标明最终方案)和 `architecture-plan.json`(项目名称、最终嵌套目录、主要决定与依赖说明),供只读看板展示。沿用该 Skill 的基础依赖/业务 feature 分层和共享所有权规则;候选页面族不是最终页面/Spec 清单。
|
|
73
|
+
3. 确认当前方案且已要求创建后,按该 Skill 的目录落地流程重新读取最终 JSON 与 Markdown 的实际确认,创建或复用基础工程、补齐关键目录并保存 `shell-receipt.json`。完整迁移已有创建要求,或已有“采用推荐并创建”授权时,不追加确认;预检已有壳则复用,不把目录壳子当成注册完成的模块或业务实现。
|
|
72
74
|
4. 创建/复用后核对 `.migration/config.json` 的 `harmonyProject` 与实际目标根一致;路径变化时通过现有 CLI 配置入口同步。向后续阶段交接当前规划路径、选定方案、目标根、模块/目录归属、共享所有者与必要前置,实际落地时再附 receipt 及未完成项。完整迁移的架构阶段须完成选型,以及本次要求的创建/复用后才能标记完成;仅请求规划则按 Skill 的规划交付条件结束。
|
|
75
|
+
5. 目标工程和目录落地后,按场景完成资源迁移,再生成 Spec;仅请求架构规划时不执行资源迁移。
|
|
76
|
+
|
|
77
|
+
- 迁移应用身份时使用 `arkts-app-identity`。
|
|
78
|
+
- 迁移字符串和多语言时使用 `arkts-i18n`,禁止用 `.ets` 硬编码代替字符串资源迁移。
|
|
79
|
+
- 转换图片和 drawable 时使用 `android2hmos_resources_convert`;覆盖源码实际使用的资源目录,发现尺寸或比例问题时使用 `arkts-icon-sizing`。
|
|
73
80
|
|
|
74
81
|
### 四、Android 工程分析
|
|
75
82
|
|
|
@@ -79,25 +86,20 @@ description: Android 应用到 HarmonyOS 的 AI 驱动迁移主 Agent;按迁
|
|
|
79
86
|
### 五、spec 设计与生成
|
|
80
87
|
|
|
81
88
|
1. 加载 `d2h-spec-generate`,根据 Facts、`.migration/utg/page.json`、源码和目标工程现状确定页面边界、依赖与顺序,生成 `pages.json`、页面 `spec.md` 和初始 `batch_report.json`。已有产物按该技能规则复用,不能覆盖执行历史。
|
|
82
|
-
2.
|
|
89
|
+
2. 将已确认功能范围、选定架构、已有页面产物及未满足的外部条件交给 `d2h-spec-plan`,规划范围映射、任务排序、里程碑和波次,将受影响任务及条件记入波次前置与风险;主 Agent 按技能要求核对规划结果。
|
|
83
90
|
3. 执行顺序以 spec-plan 的里程碑、波次和任务排序为准;页面归属、生成时的顺序和依赖作为规划依据。排序不得违反实际前置,不拆合 Spec、不改变正文布局或账本任务身份。
|
|
84
91
|
|
|
85
92
|
### 六、代码转换
|
|
86
93
|
|
|
87
|
-
1.
|
|
88
|
-
|
|
89
|
-
- 规划工程分层、状态管理、导航和共享能力时使用 `arkts-mvvm-architecture`。
|
|
90
|
-
- 迁移应用身份时使用 `arkts-app-identity`。
|
|
91
|
-
- 迁移字符串和多语言时使用 `arkts-i18n`,禁止用 `.ets` 硬编码代替字符串资源迁移。
|
|
92
|
-
- 转换图片和 drawable 时使用 `android2hmos_resources_convert`;发现尺寸或比例问题时使用 `arkts-icon-sizing`。
|
|
94
|
+
1. 编写 ArkTS 前加载 `arkts-mvvm-architecture`,落实模块内分层与编码规范;目录和模块归属沿用已确认架构方案,不重新规划。沿用已迁移的资源,缺失时按对应专项 Skill 补齐;不得用 emoji 或字符替代已有图标资源。
|
|
93
95
|
|
|
94
|
-
2. 加载 `d2h-spec-develop`,主 Agent 按有效计划初始化或恢复 `batch_report.json
|
|
96
|
+
2. 加载 `d2h-spec-develop`,主 Agent 按有效计划初始化或恢复 `batch_report.json`,按里程碑、波次和任务排序严格串行推进。任意时刻只运行一个独立 page-worker;可将同一波次内排序相邻、共享页面或模块上下文的 2~3 个 Spec 交给同一 worker,具体分组和逐项交接按该技能执行:
|
|
95
97
|
|
|
96
|
-
a. 主 Agent 按账本中的有效规划,只从纳入的任务中,根据实际前置证据选择当前波次内按任务排序最早未完成且前置满足的任务,固定本次任务 ID
|
|
97
|
-
b. 向 page-worker
|
|
98
|
+
a. 主 Agent 按账本中的有效规划,只从纳入的任务中,根据实际前置证据选择当前波次内按任务排序最早未完成且前置满足的任务,固定本次任务 ID、版本和允许路径;未纳入任务不得因规格已生成而派发。进入相关波次前核对外部条件,区分实现与验收所需条件;仅阻塞实际依赖项,主 Agent 按依赖调整后继续其他可执行任务,不能用固定登录成功或伪造结果绕过验收。
|
|
99
|
+
b. 向 page-worker 交接固定的有序任务 ID、当前 Spec、源码与目标工程、Git 基线、真实共享接口和检查要求。worker 加载 `d2h-spec-develop`,只实施当前获授权的 Spec;主 Agent 组织测试与验收,各 Spec 独立记录结果。
|
|
98
100
|
c. 主 Agent 按页面流程组织检查;需要生成或更新 Python Hypium 测试资产时加载 `d2h-hypium-forge`。核对实际断言、执行结果和证据,未执行、缺失或过期的结果不能标记通过。
|
|
99
101
|
d. 主 Agent 复核作用域提交和工作区,核对 `batch_report.json` 中记录的真实实现文件、来源、本次构建与测试证据、差异及阻塞,原位更新并保留历史。
|
|
100
|
-
e.
|
|
102
|
+
e. 当前任务闭环后,重新核对前置并评估上下文与宿主续会话能力,适合时复用同一 worker 继续组内下一项,否则明确调整分组并重新派发剩余任务;本组完成后再按计划派发下一组。受阻时继续修复当前任务或记录外部阻塞,不自动跳过或扩组。里程碑与波次组织交付顺序,不拆分页面闭环或改变验收要求。
|
|
101
103
|
|
|
102
104
|
若当前环境不支持独立 page-worker,必须明确报告该开发模式无法执行,不能静默改成主 Agent 批量开发。
|
|
103
105
|
|
|
@@ -15,10 +15,11 @@ import { findMetricsPluginCacheDirs, metricsPluginCacheRoots } from '../src/metr
|
|
|
15
15
|
import { inspectToolchain, installToolchain, loadToolchain } from '../src/toolchain.js';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import { inspectVSCodeInstallation, installVSCode } from '../src/install-vscode.js';
|
|
18
|
-
import {
|
|
18
|
+
import { isSkillHost, skillHosts, skillHostConfigRoot, inspectSkillHostInstallation, installSkillHost } from '../src/install-skills.js';
|
|
19
19
|
import { installTuiExtension, inspectTuiExtension } from '../src/install-tui-extension.js';
|
|
20
20
|
import { runWorkflow } from '../src/workflow.js';
|
|
21
|
-
|
|
21
|
+
import { selectAgent } from '../src/select-agent.js';
|
|
22
|
+
const help = `Install this package's skills and agents into supported AI coding tools.
|
|
22
23
|
|
|
23
24
|
Usage: d2h [options]
|
|
24
25
|
|
|
@@ -26,7 +27,7 @@ Usage: d2h [options]
|
|
|
26
27
|
d2h upgrade Reapply this package's tested toolchain and definitions
|
|
27
28
|
d2h upgrade --check Preview version and installation status only
|
|
28
29
|
d2h workflow Validate artifacts, record execution and generate reports
|
|
29
|
-
d2h doctor [--agent
|
|
30
|
+
d2h doctor [--agent <name>] [--config-dir <path>]
|
|
30
31
|
Check definitions, host environment and CLI capabilities
|
|
31
32
|
|
|
32
33
|
--check-env Check host tools and write reports without installing skills
|
|
@@ -34,12 +35,16 @@ Usage: d2h [options]
|
|
|
34
35
|
Use d2h --check-env --help for check options
|
|
35
36
|
|
|
36
37
|
--source-dir <path> Source root (defaults to this package)
|
|
37
|
-
--agent <name> opencode
|
|
38
|
+
--agent <name> opencode, vscode, deveco, codex, qoder, trae,
|
|
39
|
+
trae-cn, claude, dsh, zcode, codebuddy or workbuddy
|
|
40
|
+
Omit for interactive selection with local detection hints
|
|
38
41
|
--extension-dir <path> VS Code HarmonyOS Dev Assistant extension root
|
|
39
42
|
--config-dir <path> Override the selected agent's config root
|
|
40
43
|
OPENCODE_CONFIG_DIR / DEVECO_CONFIG_DIR, or
|
|
41
44
|
$XDG_CONFIG_HOME/<agent>, or ~/.config/<agent>
|
|
42
|
-
Codex: ~/.
|
|
45
|
+
Codex: $CODEX_HOME or ~/.codex; other skill hosts: ~/.<agent>
|
|
46
|
+
Skills under skills/; trae-cn uses ~/.trae-cn
|
|
47
|
+
Claude/DSH also honor CLAUDE_CONFIG_DIR/DSH_HOME
|
|
43
48
|
--link Create absolute symbolic links (development only)
|
|
44
49
|
--force Replace conflicting definitions and managed CLI npm links
|
|
45
50
|
--remote Use opencode-metrics-plugin instead of the default lite plugin
|
|
@@ -77,10 +82,10 @@ try {
|
|
|
77
82
|
else
|
|
78
83
|
throw new Error(`unknown doctor option: ${arg}`);
|
|
79
84
|
}
|
|
80
|
-
if (!['opencode', 'deveco', 'vscode'
|
|
85
|
+
if (!['opencode', 'deveco', 'vscode'].includes(agent) && !isSkillHost(agent))
|
|
81
86
|
throw new Error(`unsupported agent: ${agent}`);
|
|
82
|
-
configDir ??= agent
|
|
83
|
-
const installation = agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : agent
|
|
87
|
+
configDir ??= isSkillHost(agent) ? skillHostConfigRoot(agent) : process.env[agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), agent);
|
|
88
|
+
const installation = agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : isSkillHost(agent) ? inspectSkillHostInstallation(agent, configDir) : inspectInstallation(configDir);
|
|
84
89
|
const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
|
|
85
90
|
const definitionsMatch = installation.status === 'passed' && installation.packageVersion === packageVersion &&
|
|
86
91
|
(agent === 'vscode' || ('agent' in installation && installation.agent === agent));
|
|
@@ -88,13 +93,13 @@ try {
|
|
|
88
93
|
const results = inspectToolchain(packageRoot);
|
|
89
94
|
for (const item of results)
|
|
90
95
|
console.log(`${item.status}: ${item.package} expected ${item.expectedVersion}, installed ${item.installedVersion ?? 'missing'}; capabilities ${JSON.stringify(item.capabilities)}`);
|
|
91
|
-
const pluginCache = agent
|
|
96
|
+
const pluginCache = isSkillHost(agent) ? [] : findMetricsPluginCacheDirs(metricsPluginCacheRoots(agent));
|
|
92
97
|
console.log(`plugin cache: ${pluginCache.length ? `${pluginCache.join(', ')}; cleared by d2h install/upgrade` : 'none'}`);
|
|
93
98
|
if (agent === 'opencode' || agent === 'vscode') {
|
|
94
99
|
const tui = inspectTuiExtension({ agent: agent === 'vscode' ? 'vscode' : 'opencode', extensionDir });
|
|
95
100
|
console.log(`${tui.status}: opencode-tui-extension ${tui.summary}`);
|
|
96
101
|
}
|
|
97
|
-
const checkerUrl = new URL('../../scripts/check-environment.mjs', import.meta.url);
|
|
102
|
+
const checkerUrl = new URL('../../tooling/scripts/check-environment.mjs', import.meta.url);
|
|
98
103
|
const { main } = await import(__rewriteRelativeImportExtension(checkerUrl.href));
|
|
99
104
|
const environmentExit = await main([]);
|
|
100
105
|
if (environmentExit !== 0 || !definitionsMatch || results.some(item => item.status !== 'passed'))
|
|
@@ -103,7 +108,7 @@ try {
|
|
|
103
108
|
else if (args.includes('--check-env')) {
|
|
104
109
|
if (args.filter(arg => arg === '--check-env').length !== 1)
|
|
105
110
|
throw new Error('--check-env may only be specified once');
|
|
106
|
-
const checkerUrl = new URL('../../scripts/check-environment.mjs', import.meta.url);
|
|
111
|
+
const checkerUrl = new URL('../../tooling/scripts/check-environment.mjs', import.meta.url);
|
|
107
112
|
const { main } = await import(__rewriteRelativeImportExtension(checkerUrl.href));
|
|
108
113
|
process.exitCode = await main(args.filter(arg => arg !== '--check-env'));
|
|
109
114
|
}
|
|
@@ -117,7 +122,6 @@ try {
|
|
|
117
122
|
let remote = false;
|
|
118
123
|
const options = {
|
|
119
124
|
sourceDir: packageRoot,
|
|
120
|
-
agent: 'opencode',
|
|
121
125
|
};
|
|
122
126
|
for (let index = 0; index < installArgs.length; index++) {
|
|
123
127
|
const arg = installArgs[index];
|
|
@@ -150,22 +154,23 @@ try {
|
|
|
150
154
|
throw new Error(`unknown option: ${arg}`);
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
options.agent ??= await selectAgent(extensionDir);
|
|
158
|
+
if (!options.agent || (!['opencode', 'deveco', 'vscode'].includes(options.agent) && !isSkillHost(options.agent))) {
|
|
159
|
+
throw new Error(`unsupported agent: ${options.agent}; choose opencode, deveco, vscode or ${Object.keys(skillHosts).join(', ')}`);
|
|
155
160
|
}
|
|
156
|
-
if (options.agent
|
|
157
|
-
throw new Error(
|
|
161
|
+
if (isSkillHost(options.agent) && remote)
|
|
162
|
+
throw new Error(`--remote is not supported by ${skillHosts[options.agent].label}`);
|
|
158
163
|
if (options.agent !== 'vscode' && extensionDir)
|
|
159
164
|
throw new Error('--extension-dir requires --agent vscode');
|
|
160
165
|
if (options.agent === 'vscode' && options.configDir)
|
|
161
166
|
throw new Error('--config-dir is not used for VS Code; use --extension-dir');
|
|
162
|
-
options.configDir ??= options.agent
|
|
167
|
+
options.configDir ??= isSkillHost(options.agent) ? skillHostConfigRoot(options.agent) : process.env[options.agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), options.agent);
|
|
163
168
|
if (checkOnly) {
|
|
164
|
-
const installation = options.agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : options.agent
|
|
169
|
+
const installation = options.agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : isSkillHost(options.agent) ? inspectSkillHostInstallation(options.agent, options.configDir) : inspectInstallation(options.configDir);
|
|
165
170
|
console.log(`definitions ${installation.status}: ${installation.summary}`);
|
|
166
171
|
for (const item of inspectToolchain(packageRoot))
|
|
167
172
|
console.log(`${item.status}: ${item.package} expected ${item.expectedVersion}, installed ${item.installedVersion ?? 'missing'}`);
|
|
168
|
-
const pluginCache = options.agent
|
|
173
|
+
const pluginCache = isSkillHost(options.agent) ? [] : findMetricsPluginCacheDirs(metricsPluginCacheRoots(options.agent));
|
|
169
174
|
console.log(`plugin cache: ${pluginCache.length ? `${pluginCache.join(', ')}; cleared by d2h install/upgrade` : 'none'}`);
|
|
170
175
|
}
|
|
171
176
|
else if (!skipTools)
|
|
@@ -177,22 +182,22 @@ try {
|
|
|
177
182
|
if (options.agent === 'vscode') {
|
|
178
183
|
vscodeExtensionRoot = installVSCode({ sourceDir: options.sourceDir, extensionDir, remote, dryRun: options.dryRun }).extensionRoot;
|
|
179
184
|
}
|
|
180
|
-
else if (options.agent
|
|
181
|
-
|
|
185
|
+
else if (isSkillHost(options.agent))
|
|
186
|
+
installSkillHost(options.agent, { ...options, remote });
|
|
182
187
|
else
|
|
183
188
|
installOpenCode({ ...options, remote });
|
|
184
189
|
if ((options.agent === 'opencode' || options.agent === 'vscode') && !skipTools)
|
|
185
190
|
installTuiExtension({ dryRun: options.dryRun, force: options.force, extensionRoot: vscodeExtensionRoot });
|
|
186
191
|
if (!options.dryRun) {
|
|
187
192
|
const target = options.agent === 'vscode' ? 'VS Code HarmonyOS Dev Assistant' :
|
|
188
|
-
options.agent === 'deveco' ? 'DevEco Code' : options.agent
|
|
193
|
+
options.agent === 'deveco' ? 'DevEco Code' : isSkillHost(options.agent) ? skillHosts[options.agent].label : 'OpenCode';
|
|
189
194
|
const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
|
|
190
195
|
console.log('\n============================================================');
|
|
191
196
|
console.log(`[D2H] Installation successful / 安装成功`);
|
|
192
197
|
console.log(`Target / 目标: ${target}`);
|
|
193
198
|
console.log(`Version / 版本: ${packageVersion}`);
|
|
194
199
|
console.log('Agent: d2h');
|
|
195
|
-
console.log(options.agent
|
|
200
|
+
console.log(isSkillHost(options.agent) ? `Next / 下一步: start a new ${target} conversation and invoke ${skillHosts[options.agent].invocation}.` : `Next / 下一步: restart ${target}, then select the d2h Agent.`);
|
|
196
201
|
console.log('============================================================');
|
|
197
202
|
}
|
|
198
203
|
}
|
|
@@ -1,131 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import { stat, digest, sameTree, contains, resolvedLocation } from './install-opencode.js';
|
|
1
|
+
import { installSkillHost, inspectSkillHostInstallation, skillHostConfigRoot } from './install-skills.js';
|
|
5
2
|
export function codexConfigRoot() {
|
|
6
|
-
return
|
|
7
|
-
}
|
|
8
|
-
// Keep the orchestration prompt as an explicitly invokable skill: custom agents
|
|
9
|
-
// are subagents in Codex, whereas D2H needs to drive the main conversation.
|
|
10
|
-
function definitions(sourceRoot) {
|
|
11
|
-
const skills = path.join(sourceRoot, 'skills');
|
|
12
|
-
const entries = fs.readdirSync(skills).sort()
|
|
13
|
-
.filter(name => fs.existsSync(path.join(skills, name, 'SKILL.md')))
|
|
14
|
-
.map(name => ({ name, source: path.join(skills, name) }));
|
|
15
|
-
if (!entries.length)
|
|
16
|
-
throw new Error(`no skills found under ${skills}`);
|
|
17
|
-
if (entries.some(entry => entry.name === 'd2h'))
|
|
18
|
-
throw new Error('reserved Codex orchestration skill: d2h');
|
|
19
|
-
const prompt = fs.readFileSync(path.join(sourceRoot, 'agents', 'd2h.md'), 'utf8');
|
|
20
|
-
if (!/^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(prompt))
|
|
21
|
-
throw new Error('d2h agent front matter missing');
|
|
22
|
-
return [...entries, { name: 'd2h', content: prompt }];
|
|
3
|
+
return skillHostConfigRoot('codex');
|
|
23
4
|
}
|
|
24
5
|
export function inspectCodexInstallation(configDir = codexConfigRoot()) {
|
|
25
|
-
|
|
26
|
-
const receipt = path.join(root, '.d2h-codex-installation.json');
|
|
27
|
-
if (!fs.existsSync(receipt))
|
|
28
|
-
return { status: 'missing', summary: `installation record missing: ${receipt}` };
|
|
29
|
-
const record = JSON.parse(fs.readFileSync(receipt, 'utf8'));
|
|
30
|
-
if (record.schemaVersion !== 1 || record.agent !== 'codex' || !Array.isArray(record.entries) || !record.entries.length) {
|
|
31
|
-
throw new Error(`invalid installation record: ${receipt}`);
|
|
32
|
-
}
|
|
33
|
-
const changed = record.entries.filter(entry => {
|
|
34
|
-
if (!/^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*$/.test(entry.name))
|
|
35
|
-
throw new Error(`invalid installation entry: ${entry.name}`);
|
|
36
|
-
const target = path.join(root, 'skills', entry.name);
|
|
37
|
-
return !stat(target) || digest(target) !== entry.digest;
|
|
38
|
-
});
|
|
39
|
-
return { status: changed.length ? 'failed' : 'passed', agent: 'codex', packageVersion: record.packageVersion,
|
|
40
|
-
summary: changed.length ? `changed or missing: ${changed.map(entry => entry.name).join(', ')}` : `${record.entries.length} installed skills verified` };
|
|
6
|
+
return inspectSkillHostInstallation('codex', configDir);
|
|
41
7
|
}
|
|
42
|
-
export function installCodex(
|
|
43
|
-
|
|
44
|
-
throw new Error('--remote metrics plugins are not supported by Codex');
|
|
45
|
-
if (!configDir || ['.', '..'].includes(configDir))
|
|
46
|
-
throw new Error(`unsafe Codex config directory: ${configDir}`);
|
|
47
|
-
const sourceRoot = fs.realpathSync(path.resolve(sourceDir));
|
|
48
|
-
const root = resolvedLocation(path.resolve(configDir));
|
|
49
|
-
if (root === path.parse(root).root || root === fs.realpathSync(os.homedir()) ||
|
|
50
|
-
contains(sourceRoot, root) || contains(root, sourceRoot))
|
|
51
|
-
throw new Error(`unsafe Codex config directory: ${configDir}`);
|
|
52
|
-
const skillsRoot = path.join(root, 'skills');
|
|
53
|
-
if (stat(skillsRoot)?.isSymbolicLink())
|
|
54
|
-
throw new Error(`refusing linked installation directory: ${skillsRoot}`);
|
|
55
|
-
const receipt = path.join(root, '.d2h-codex-installation.json');
|
|
56
|
-
if (stat(receipt)?.isSymbolicLink())
|
|
57
|
-
throw new Error(`refusing linked installation record: ${receipt}`);
|
|
58
|
-
const entries = definitions(sourceRoot).map(entry => {
|
|
59
|
-
const target = path.join(skillsRoot, entry.name);
|
|
60
|
-
const existing = stat(target);
|
|
61
|
-
const unchanged = entry.source ? (link && existing?.isSymbolicLink()
|
|
62
|
-
? path.resolve(path.dirname(target), fs.readlinkSync(target)) === entry.source
|
|
63
|
-
: !link && sameTree(entry.source, target))
|
|
64
|
-
: existing?.isDirectory() && !existing.isSymbolicLink() &&
|
|
65
|
-
fs.readdirSync(target).length === 1 && fs.existsSync(path.join(target, 'SKILL.md')) &&
|
|
66
|
-
fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8') === entry.content;
|
|
67
|
-
if (!unchanged && existing && !force)
|
|
68
|
-
throw new Error(`skill '${entry.name}' already exists at ${target}; rerun with --force`);
|
|
69
|
-
return { ...entry, target, unchanged };
|
|
70
|
-
});
|
|
71
|
-
if (dryRun) {
|
|
72
|
-
for (const entry of entries)
|
|
73
|
-
log(`would install Codex skill: ${entry.name} -> ${entry.target}`);
|
|
74
|
-
return { skills: entries.length, configRoot: root };
|
|
75
|
-
}
|
|
76
|
-
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
77
|
-
if (!contains(root, fs.realpathSync(skillsRoot)))
|
|
78
|
-
throw new Error(`installation escaped config directory: ${skillsRoot}`);
|
|
79
|
-
for (const entry of entries) {
|
|
80
|
-
if (entry.unchanged) {
|
|
81
|
-
log(`unchanged Codex skill: ${entry.name}`);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const staging = fs.mkdtempSync(path.join(skillsRoot, '.d2h-install-'));
|
|
85
|
-
const staged = path.join(staging, 'new');
|
|
86
|
-
const backup = path.join(staging, 'backup');
|
|
87
|
-
try {
|
|
88
|
-
if (entry.source) {
|
|
89
|
-
if (link)
|
|
90
|
-
fs.symlinkSync(entry.source, staged, process.platform === 'win32' ? 'junction' : 'dir');
|
|
91
|
-
else
|
|
92
|
-
fs.cpSync(entry.source, staged, { recursive: true });
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
fs.mkdirSync(staged);
|
|
96
|
-
fs.writeFileSync(path.join(staged, 'SKILL.md'), entry.content);
|
|
97
|
-
}
|
|
98
|
-
if (stat(entry.target))
|
|
99
|
-
fs.renameSync(entry.target, backup);
|
|
100
|
-
try {
|
|
101
|
-
fs.renameSync(staged, entry.target);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
if (stat(backup))
|
|
105
|
-
fs.renameSync(backup, entry.target);
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
if (stat(backup) && !stat(entry.target))
|
|
111
|
-
log(`installation backup retained: ${backup}`);
|
|
112
|
-
else
|
|
113
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
114
|
-
}
|
|
115
|
-
log(`installed Codex skill: ${entry.target}`);
|
|
116
|
-
}
|
|
117
|
-
const packageFile = path.join(sourceRoot, 'package.json');
|
|
118
|
-
const packageVersion = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')).version : 'source';
|
|
119
|
-
const temporary = `${receipt}.${process.pid}.tmp`;
|
|
120
|
-
try {
|
|
121
|
-
fs.writeFileSync(temporary, JSON.stringify({ schemaVersion: 1, agent: 'codex', packageVersion,
|
|
122
|
-
entries: entries.map(entry => ({ name: entry.name, digest: digest(entry.target) })) }, null, 2) + '\n');
|
|
123
|
-
fs.renameSync(temporary, receipt);
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
if (fs.existsSync(temporary))
|
|
127
|
-
fs.rmSync(temporary);
|
|
128
|
-
}
|
|
129
|
-
log(`Codex installation complete: ${entries.length} skill(s). Start a new conversation and invoke $d2h.`);
|
|
130
|
-
return { skills: entries.length, configRoot: root };
|
|
8
|
+
export function installCodex(options) {
|
|
9
|
+
return installSkillHost('codex', options);
|
|
131
10
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { stat, digest, sameTree, contains, resolvedLocation } from './install-opencode.js';
|
|
5
|
+
export const skillHosts = {
|
|
6
|
+
codex: { label: 'Codex', directory: '.codex', environment: 'CODEX_HOME', invocation: '$d2h' },
|
|
7
|
+
qoder: { label: 'Qoder', directory: '.qoder', invocation: '/d2h' },
|
|
8
|
+
trae: { label: 'TRAE', directory: '.trae', invocation: 'd2h skill' },
|
|
9
|
+
'trae-cn': { label: 'TRAE CN', directory: '.trae-cn', invocation: 'd2h skill' },
|
|
10
|
+
claude: { label: 'Claude Code', directory: '.claude', environment: 'CLAUDE_CONFIG_DIR', invocation: '/d2h' },
|
|
11
|
+
dsh: { label: 'DeepSeek Harness', directory: '.dsh', environment: 'DSH_HOME', invocation: '/d2h' },
|
|
12
|
+
zcode: { label: 'ZCode', directory: '.zcode', invocation: '$d2h' },
|
|
13
|
+
codebuddy: { label: 'CodeBuddy', directory: '.codebuddy', invocation: '/d2h' },
|
|
14
|
+
workbuddy: { label: 'WorkBuddy', directory: '.workbuddy', invocation: 'd2h skill' }
|
|
15
|
+
};
|
|
16
|
+
export function isSkillHost(agent) {
|
|
17
|
+
return Object.hasOwn(skillHosts, agent);
|
|
18
|
+
}
|
|
19
|
+
export function skillHostConfigRoot(agent) {
|
|
20
|
+
const host = skillHosts[agent];
|
|
21
|
+
return ('environment' in host && process.env[host.environment]) || path.join(os.homedir(), host.directory);
|
|
22
|
+
}
|
|
23
|
+
// Keep the orchestration prompt as an explicitly invokable skill so it drives
|
|
24
|
+
// the main conversation without requiring host-specific custom agent formats.
|
|
25
|
+
function definitions(sourceRoot) {
|
|
26
|
+
const skills = path.join(sourceRoot, 'skills');
|
|
27
|
+
const entries = fs.readdirSync(skills).sort()
|
|
28
|
+
.filter(name => fs.existsSync(path.join(skills, name, 'SKILL.md')))
|
|
29
|
+
.map(name => ({ name, source: path.join(skills, name) }));
|
|
30
|
+
if (!entries.length)
|
|
31
|
+
throw new Error(`no skills found under ${skills}`);
|
|
32
|
+
if (entries.some(entry => entry.name === 'd2h'))
|
|
33
|
+
throw new Error('reserved orchestration skill: d2h');
|
|
34
|
+
const prompt = fs.readFileSync(path.join(sourceRoot, 'agents', 'd2h.md'), 'utf8');
|
|
35
|
+
if (!/^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(prompt))
|
|
36
|
+
throw new Error('d2h agent front matter missing');
|
|
37
|
+
return [...entries, { name: 'd2h', content: prompt }];
|
|
38
|
+
}
|
|
39
|
+
export function inspectSkillHostInstallation(agent, configDir = skillHostConfigRoot(agent)) {
|
|
40
|
+
const root = path.resolve(configDir);
|
|
41
|
+
const receipt = path.join(root, `.d2h-${agent}-installation.json`);
|
|
42
|
+
if (!fs.existsSync(receipt))
|
|
43
|
+
return { status: 'missing', summary: `installation record missing: ${receipt}` };
|
|
44
|
+
const record = JSON.parse(fs.readFileSync(receipt, 'utf8'));
|
|
45
|
+
if (record.schemaVersion !== 1 || record.agent !== agent || !Array.isArray(record.entries) || !record.entries.length) {
|
|
46
|
+
throw new Error(`invalid installation record: ${receipt}`);
|
|
47
|
+
}
|
|
48
|
+
const changed = record.entries.filter(entry => {
|
|
49
|
+
if (!/^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*$/.test(entry.name))
|
|
50
|
+
throw new Error(`invalid installation entry: ${entry.name}`);
|
|
51
|
+
const target = path.join(root, 'skills', entry.name);
|
|
52
|
+
return !stat(target) || digest(target) !== entry.digest;
|
|
53
|
+
});
|
|
54
|
+
return { status: changed.length ? 'failed' : 'passed', agent, packageVersion: record.packageVersion,
|
|
55
|
+
summary: changed.length ? `changed or missing: ${changed.map(entry => entry.name).join(', ')}` : `${record.entries.length} installed skills verified` };
|
|
56
|
+
}
|
|
57
|
+
export function installSkillHost(agent, { sourceDir, configDir = skillHostConfigRoot(agent), link = false, force = false, dryRun = false, remote = false, log = console.log }) {
|
|
58
|
+
const host = skillHosts[agent];
|
|
59
|
+
if (remote)
|
|
60
|
+
throw new Error(`--remote metrics plugins are not supported by ${host.label}`);
|
|
61
|
+
if (!configDir || ['.', '..'].includes(configDir))
|
|
62
|
+
throw new Error(`unsafe skill config directory: ${configDir}`);
|
|
63
|
+
const sourceRoot = fs.realpathSync(path.resolve(sourceDir));
|
|
64
|
+
const root = resolvedLocation(path.resolve(configDir));
|
|
65
|
+
if (root === path.parse(root).root || root === fs.realpathSync(os.homedir()) ||
|
|
66
|
+
contains(sourceRoot, root) || contains(root, sourceRoot))
|
|
67
|
+
throw new Error(`unsafe skill config directory: ${configDir}`);
|
|
68
|
+
const skillsRoot = path.join(root, 'skills');
|
|
69
|
+
if (stat(skillsRoot)?.isSymbolicLink())
|
|
70
|
+
throw new Error(`refusing linked installation directory: ${skillsRoot}`);
|
|
71
|
+
const receipt = path.join(root, `.d2h-${agent}-installation.json`);
|
|
72
|
+
if (stat(receipt)?.isSymbolicLink())
|
|
73
|
+
throw new Error(`refusing linked installation record: ${receipt}`);
|
|
74
|
+
const entries = definitions(sourceRoot).map(entry => {
|
|
75
|
+
const target = path.join(skillsRoot, entry.name);
|
|
76
|
+
const existing = stat(target);
|
|
77
|
+
const unchanged = entry.source ? (link && existing?.isSymbolicLink()
|
|
78
|
+
? path.resolve(path.dirname(target), fs.readlinkSync(target)) === entry.source
|
|
79
|
+
: !link && sameTree(entry.source, target))
|
|
80
|
+
: existing?.isDirectory() && !existing.isSymbolicLink() &&
|
|
81
|
+
fs.readdirSync(target).length === 1 && fs.existsSync(path.join(target, 'SKILL.md')) &&
|
|
82
|
+
fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8') === entry.content;
|
|
83
|
+
if (!unchanged && existing && !force)
|
|
84
|
+
throw new Error(`skill '${entry.name}' already exists at ${target}; rerun with --force`);
|
|
85
|
+
return { ...entry, target, unchanged };
|
|
86
|
+
});
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
for (const entry of entries)
|
|
89
|
+
log(`would install ${host.label} skill: ${entry.name} -> ${entry.target}`);
|
|
90
|
+
return { skills: entries.length, configRoot: root };
|
|
91
|
+
}
|
|
92
|
+
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
93
|
+
if (!contains(root, fs.realpathSync(skillsRoot)))
|
|
94
|
+
throw new Error(`installation escaped config directory: ${skillsRoot}`);
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (entry.unchanged) {
|
|
97
|
+
log(`unchanged ${host.label} skill: ${entry.name}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const staging = fs.mkdtempSync(path.join(skillsRoot, '.d2h-install-'));
|
|
101
|
+
const staged = path.join(staging, 'new');
|
|
102
|
+
const backup = path.join(staging, 'backup');
|
|
103
|
+
try {
|
|
104
|
+
if (entry.source) {
|
|
105
|
+
if (link)
|
|
106
|
+
fs.symlinkSync(entry.source, staged, process.platform === 'win32' ? 'junction' : 'dir');
|
|
107
|
+
else
|
|
108
|
+
fs.cpSync(entry.source, staged, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
fs.mkdirSync(staged);
|
|
112
|
+
fs.writeFileSync(path.join(staged, 'SKILL.md'), entry.content);
|
|
113
|
+
}
|
|
114
|
+
if (stat(entry.target))
|
|
115
|
+
fs.renameSync(entry.target, backup);
|
|
116
|
+
try {
|
|
117
|
+
fs.renameSync(staged, entry.target);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (stat(backup))
|
|
121
|
+
fs.renameSync(backup, entry.target);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (stat(backup) && !stat(entry.target))
|
|
127
|
+
log(`installation backup retained: ${backup}`);
|
|
128
|
+
else
|
|
129
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
log(`installed ${host.label} skill: ${entry.target}`);
|
|
132
|
+
}
|
|
133
|
+
const packageFile = path.join(sourceRoot, 'package.json');
|
|
134
|
+
const packageVersion = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')).version : 'source';
|
|
135
|
+
const temporary = `${receipt}.${process.pid}.tmp`;
|
|
136
|
+
try {
|
|
137
|
+
fs.writeFileSync(temporary, JSON.stringify({ schemaVersion: 1, agent, packageVersion,
|
|
138
|
+
entries: entries.map(entry => ({ name: entry.name, digest: digest(entry.target) })) }, null, 2) + '\n');
|
|
139
|
+
fs.renameSync(temporary, receipt);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (fs.existsSync(temporary))
|
|
143
|
+
fs.rmSync(temporary);
|
|
144
|
+
}
|
|
145
|
+
log(`${host.label} installation complete: ${entries.length} skill(s). Start a new conversation and invoke ${host.invocation}.`);
|
|
146
|
+
return { skills: entries.length, configRoot: root };
|
|
147
|
+
}
|
|
@@ -133,8 +133,8 @@ export function installVSCode({ sourceDir, extensionDir, bundleCacheRoot, plugin
|
|
|
133
133
|
{ source: path.join(sourceRoot, 'skills'), destination: path.join(pluginRoot, 'skills') },
|
|
134
134
|
{ source: path.join(sourceRoot, 'agents'), destination: path.join(pluginRoot, 'agents') },
|
|
135
135
|
{ source: path.join(sourceRoot, 'AGENTS.md'), destination: path.join(pluginRoot, 'AGENTS.md') },
|
|
136
|
-
{ source: path.join(sourceRoot, 'opencode', 'vscode-d2h-plugin.mjs'), destination: path.join(pluginRoot, 'src', 'index.js') },
|
|
137
|
-
{ source: path.join(sourceRoot, 'opencode', 'vscode-d2h-package.json'), destination: path.join(pluginRoot, 'package.json') },
|
|
136
|
+
{ source: path.join(sourceRoot, 'tooling', 'opencode', 'vscode-d2h-plugin.mjs'), destination: path.join(pluginRoot, 'src', 'index.js') },
|
|
137
|
+
{ source: path.join(sourceRoot, 'tooling', 'opencode', 'vscode-d2h-package.json'), destination: path.join(pluginRoot, 'package.json') },
|
|
138
138
|
];
|
|
139
139
|
for (const entry of copies)
|
|
140
140
|
if (!fs.existsSync(entry.source))
|