mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-21 17:38:03 +00:00
feat(vscode): 插件工程调试扩展(plg.json 诊断 / SDK 版本 / 构建运行 / 内核日志跟随)
插件的真实形态是「独立子进程 + 内核侧握手」,所以插件问题几乎都在 IDE 之外发生:
编不出来(多半是没声明用哪版 SDK,工具链拿了存储里的 current)、编出来起不来
(产物与内核协议绑定)、起来了行为不对(真因只在内核日志里)。这个扩展把这三件事
拉进 IDE。
- `tools/vscode-hmapdev`:TypeScript 扩展(零运行时依赖,仅 devDeps: typescript + @types/vscode)
- **plg.json 诊断**:必需字段;`sdk` 必须是完整版本号(区间写法 `1.2` 报错并说明
「patch 位恒为 .0」,与工具链 ResolveSDKForProject 同一套规矩);声明的 SDK 未安装时
直接给 `hmapdev sdk install vX.Y.Z`;
- **状态栏**:`插件 · SDK <声明> · hmapdev <版本>`,工具链缺失/工程有错时变色,tooltip 列已装 SDK;
- **命令**:build / build --target all / clean / debug(解释执行)/ 工具链版本 / SDK 列表-安装-切换 /
跟随内核日志 / 停止跟随 / 刷新;
- **任务**:同一批动作注册为 `hmapdev` 任务 + Go 问题匹配器(编译错误进 Problems);
- **内核日志跟随**:读 `<dataDir>/log` 最新 `homed_*.log`,按插件名过滤持续输出;
- schema 校验 + plg.json 骨架片段。
- 诚实边界(写进 README):**不是源码级调试器**——没有断点/单步,没有 DAP 会话;
做的是构建、运行、看内核日志、清单校验。
验证:`tsc` 零错误;`node --test` 13/13(版本规则、诊断分级、SDK 列表解析、状态栏文本、
以及**反向核对抓到的真缺陷**:`hmapdev 未找到` 这类输出曾被解析成版本号 → 已要求版本
token 以数字开头,否则「工具链不在」会被显示成「工具链 <垃圾词>」并让 SDK 诊断失真);
与真实工具链输出的集成核对 PASS(`hmapdev version` / `sdk list` 的真输出解析正确)。
README 同时补两节:plg.json 的 `sdk` 字段语义(含「为什么必须有」与「为什么拒区间写法」)、
本扩展的用法与边界。
This commit is contained in:
3
tools/vscode-hmapdev/.gitignore
vendored
Normal file
3
tools/vscode-hmapdev/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
out/
|
||||
*.vsix
|
||||
5
tools/vscode-hmapdev/.vscodeignore
Normal file
5
tools/vscode-hmapdev/.vscodeignore
Normal file
@ -0,0 +1,5 @@
|
||||
src/
|
||||
tsconfig.json
|
||||
node_modules/
|
||||
out/test/
|
||||
.gitignore
|
||||
69
tools/vscode-hmapdev/README.md
Normal file
69
tools/vscode-hmapdev/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# hmapdev — HomeAgent 插件开发 VSCode 扩展
|
||||
|
||||
调试与构建 HomeAgent 插件工程的 IDE 支持:**plg.json 校验、SDK 版本解析、构建/运行、内核日志跟随**。
|
||||
|
||||
## 为什么需要它
|
||||
|
||||
插件的真实形态是「**独立子进程 + 内核侧握手**」,所以插件的三类问题几乎都在 IDE 之外发生:
|
||||
|
||||
1. **编不出来** —— 最常见的原因不是代码,而是项目没声明要用哪版 SDK,工具链拿了存储里的
|
||||
`current`(可能是陈旧的 `v0.8.0`),于是报一堆看不懂的 `undefined: sdk.XXX`;
|
||||
2. **编出来但起不来** —— 产物与内核**协议绑定**(协议版本 + 共享内存魔数),用错工具链编出来的
|
||||
插件会在握手时被拒;
|
||||
3. **起来了但行为不对** —— 真因往往只在内核日志里(建链失败、崩溃重启、工具报错)。
|
||||
|
||||
本扩展把这三件事拉进 IDE:**先把「用哪版 SDK」摆到明面上**,再让构建/运行/看日志变成一条动作链。
|
||||
|
||||
## 功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|---|---|
|
||||
| **plg.json 诊断** | 必需字段;`sdk` 必须是**完整版本号**(区间写法 `1.2` 会报错并说明「patch 位恒为 .0」);声明的 SDK 若未安装在本地存储,直接给出 `hmapdev sdk install vX.Y.Z` |
|
||||
| **状态栏** | `插件 · SDK <声明> · hmapdev <版本>`;工具链缺失或工程有错时变红/黄,tooltip 列出已装 SDK |
|
||||
| **构建 / 清理 / 运行** | `hmapdev build`、`build --target all`、`clean`、`debug`(解释执行,快速迭代)——在集成终端里跑,可 Ctrl-C |
|
||||
| **任务(Tasks)** | 同一批动作注册为 `hmapdev` 任务,可绑快捷键、串依赖;带 **Go 问题匹配器**,编译错误进 Problems 面板 |
|
||||
| **跟随内核日志** | 读 `<dataDir>/log` 下最新的 `homed_*.log`,按插件名过滤后持续输出(真正的联调回路) |
|
||||
| **SDK 管理** | 查看工具链版本、列出/安装/切换 SDK 版本(走 QuickPick,不用记命令) |
|
||||
| **JSON 支持** | `plg.json` 的 schema 校验 + 骨架片段 |
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
cd tools/vscode-hmapdev
|
||||
npm install
|
||||
npm run compile
|
||||
```
|
||||
|
||||
然后二选一:
|
||||
|
||||
- **开发模式**:在 VSCode 里打开本目录,按 `F5`(Extension Development Host),把插件工程目录作为工作区打开;
|
||||
- **安装到本机**:`npx @vscode/vsce package` 生成 `.vsix`,再 `code --install-extension hmapdev-vscode-0.1.0.vsix`。
|
||||
|
||||
前提:`hmapdev` 在 `PATH` 上(或设置 `hmapdev.path`)。
|
||||
|
||||
## 配置
|
||||
|
||||
| 设置 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `hmapdev.path` | `hmapdev` | 工具链可执行文件路径 |
|
||||
| `hmapdev.kernelDataDir` | 空 | 内核数据目录(`homed -data` 的那个);填了才能跟随内核日志 |
|
||||
| `hmapdev.diagnoseSdk` | `true` | 是否校验声明的 SDK 是否已安装(需要能执行 hmapdev) |
|
||||
|
||||
## 用法(典型开发回路)
|
||||
|
||||
1. 打开插件工程(含 `plg.json`)→ 状态栏出现 `插件 · SDK <版本> · hmapdev <版本>`;
|
||||
2. 若 `sdk` 报错(未声明 / 区间写法 / 未安装)→ 按提示执行 `hmapdev: 安装 SDK 版本…`,再 `hmapdev: 刷新状态`;
|
||||
3. `hmapdev: 构建插件`(或 `构建(全部目标平台)`)→ 编译错误直接进 Problems;
|
||||
4. 快速验证行为:`hmapdev: 运行插件(解释执行)`;
|
||||
5. 与内核联调:设置 `hmapdev.kernelDataDir` → `hmapdev: 跟随内核日志`,只看本插件的行;
|
||||
6. 改代码 → 重复 3/5。装进内核时记得**与内核同批替换**(协议绑定的产物不支持滚动升级)。
|
||||
|
||||
## 诚实的边界
|
||||
|
||||
- **这不是源码级调试器**:没有断点/单步。插件的 Go 代码要么编译成产物在内核里跑、要么用
|
||||
`hmapdev debug`(yaegi 解释执行)跑,两条路都不提供 DAP 调试会话。本扩展做的是
|
||||
「构建 + 运行 + 看内核日志 + 清单校验」,这也是插件问题实际能被定位的方式。
|
||||
- **Windows 目标**:不支持(协议 2 的统一共享内存区未移植到 Windows,内核侧改走 WSL2),
|
||||
扩展只给提示,不假装能构建。
|
||||
- **`sdk` 字段的语义**:它声明的是**本插件针对的 SDK 版本**(= 接口线),不是内核版本。
|
||||
SDK 版本跟随内核中版本、patch 位恒为 `.0`。
|
||||
59
tools/vscode-hmapdev/package-lock.json
generated
Normal file
59
tools/vscode-hmapdev/package-lock.json
generated
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "hmapdev-vscode",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hmapdev-vscode",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.20.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.2.tgz",
|
||||
"integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/vscode": {
|
||||
"version": "1.137.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.137.0.tgz",
|
||||
"integrity": "sha512-0dc/BBWxkyUsJzXIZ7PkKSalThmS4xiBT+8YEDiWdCefRKHGVV5ZNkM5NB5ULYamallYJujIfncNoXWFlyzL8A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
174
tools/vscode-hmapdev/package.json
Normal file
174
tools/vscode-hmapdev/package.json
Normal file
@ -0,0 +1,174 @@
|
||||
{
|
||||
"name": "hmapdev-vscode",
|
||||
"displayName": "HomeAgent Plugin Dev (hmapdev)",
|
||||
"description": "调试与构建 HomeAgent 插件工程:plg.json 校验、SDK 版本解析、hmapdev 构建/运行、内核日志跟随。",
|
||||
"version": "0.1.0",
|
||||
"publisher": "JianFeeeee",
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Debuggers",
|
||||
"Other"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"activationEvents": [
|
||||
"workspaceContains:plg.json",
|
||||
"workspaceContains:**/plg.json"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "hmapdev.build",
|
||||
"title": "hmapdev: 构建插件"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.buildAll",
|
||||
"title": "hmapdev: 构建插件(全部目标平台)"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.clean",
|
||||
"title": "hmapdev: 清理产物"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.run",
|
||||
"title": "hmapdev: 运行插件(解释执行,快速迭代)"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.showVersion",
|
||||
"title": "hmapdev: 查看工具链版本"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.listSdk",
|
||||
"title": "hmapdev: 列出 SDK 版本"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.installSdk",
|
||||
"title": "hmapdev: 安装 SDK 版本…"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.useSdk",
|
||||
"title": "hmapdev: 切换当前 SDK 版本…"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.tailKernelLog",
|
||||
"title": "hmapdev: 跟随内核日志(按插件过滤)"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.stopTailKernelLog",
|
||||
"title": "hmapdev: 停止跟随内核日志"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.openPlgJson",
|
||||
"title": "hmapdev: 打开 plg.json"
|
||||
},
|
||||
{
|
||||
"command": "hmapdev.refresh",
|
||||
"title": "hmapdev: 刷新状态(重新探测工具链与 SDK)"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "HomeAgent Plugin Dev",
|
||||
"properties": {
|
||||
"hmapdev.path": {
|
||||
"type": "string",
|
||||
"default": "hmapdev",
|
||||
"description": "hmapdev 可执行文件路径(默认从 PATH 找)。"
|
||||
},
|
||||
"hmapdev.kernelDataDir": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "内核数据目录(homed -data 的那个目录)。填了才能跟随内核日志调试;留空则「跟随内核日志」会先询问。"
|
||||
},
|
||||
"hmapdev.diagnoseSdk": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "校验 plg.json 里声明的 SDK 版本是否已安装在本地 SDK 存储(需要能执行 hmapdev)。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"taskDefinitions": [
|
||||
{
|
||||
"type": "hmapdev",
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"build",
|
||||
"buildAll",
|
||||
"clean",
|
||||
"run"
|
||||
],
|
||||
"description": "要执行的 hmapdev 动作。"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "插件工程目录(默认取 plg.json 所在目录)。"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"problemMatchers": [
|
||||
{
|
||||
"name": "hmapdev-go",
|
||||
"owner": "go",
|
||||
"source": "hmapdev",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+\\.go):(\\d+):(\\d+):\\s+(.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4
|
||||
},
|
||||
{
|
||||
"regexp": "^(.+\\.go):(\\d+):\\s+(.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"message": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "json",
|
||||
"filenames": [
|
||||
"plg.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"jsonValidation": [
|
||||
{
|
||||
"fileMatch": "plg.json",
|
||||
"url": "./schema/plg.schema.json"
|
||||
}
|
||||
],
|
||||
"snippets": [
|
||||
{
|
||||
"language": "json",
|
||||
"path": "./snippets/plg.json.code-snippets"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"test": "tsc -p ./ && node --test out/test/*.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
41
tools/vscode-hmapdev/schema/plg.schema.json
Normal file
41
tools/vscode-hmapdev/schema/plg.schema.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "HomeAgent 插件清单(plg.json)",
|
||||
"type": "object",
|
||||
"required": ["name", "version", "entry"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "插件名(与目录名一致最省事)"
|
||||
},
|
||||
"name_zh": { "type": "string", "description": "中文显示名" },
|
||||
"name_en": { "type": "string", "description": "英文显示名" },
|
||||
"version": { "type": "string", "description": "插件自身版本号(如 0.1.0),与内核/SDK 版本无关" },
|
||||
"description": { "type": "string" },
|
||||
"author": { "type": "string" },
|
||||
"entry": {
|
||||
"type": "string",
|
||||
"description": "入口产物文件名(子进程模式通常是 plugin.bin;Lua 是 main.lua)"
|
||||
},
|
||||
"sdk": {
|
||||
"type": "string",
|
||||
"pattern": "^v?\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "本插件针对的 SDK 版本,必须是完整版本号(如 1.2.0)。SDK 版本跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本;工具链按此在本地 SDK 存储里选择版本。"
|
||||
},
|
||||
"tags": { "type": "array", "items": { "type": "string" } },
|
||||
"targets": {
|
||||
"type": "string",
|
||||
"description": "目标平台,逗号分隔,如 linux/amd64,darwin/arm64(windows 目标暂不支持插件产物)"
|
||||
},
|
||||
"outdir": { "type": "string", "description": "产物目录(默认 dist)" },
|
||||
"bundle": { "type": "boolean", "description": "是否打包成 .hmap(默认 true)" },
|
||||
"sdk_path": {
|
||||
"type": "string",
|
||||
"description": "直接指定 SDK 源码目录(本机改 SDK 联调时用);设置后优先于 sdk 字段"
|
||||
},
|
||||
"go_version": { "type": "string" },
|
||||
"replaces": { "type": "object", "additionalProperties": { "type": "string" } },
|
||||
"source_dirs": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
26
tools/vscode-hmapdev/snippets/plg.json.code-snippets
Normal file
26
tools/vscode-hmapdev/snippets/plg.json.code-snippets
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"plg.json 骨架": {
|
||||
"prefix": "plg",
|
||||
"body": [
|
||||
"{",
|
||||
" \"name\": \"${1:MyPlugin}\",",
|
||||
" \"name_zh\": \"${2:中文名}\",",
|
||||
" \"name_en\": \"${1:MyPlugin}\",",
|
||||
" \"version\": \"0.1.0\",",
|
||||
" \"description\": \"${3:插件说明}\",",
|
||||
" \"author\": \"${4:HomeAgent}\",",
|
||||
" \"entry\": \"plugin.bin\",",
|
||||
" \"sdk\": \"${5:1.2.0}\",",
|
||||
" \"tags\": [\"${1:MyPlugin}\"],",
|
||||
" \"targets\": \"linux/amd64,darwin/arm64\"",
|
||||
"}",
|
||||
"$0"
|
||||
],
|
||||
"description": "plg.json 必需字段骨架(sdk 必须是完整版本号)"
|
||||
},
|
||||
"sdk 字段": {
|
||||
"prefix": "sdk",
|
||||
"body": ["\"sdk\": \"${1:1.2.0}\","],
|
||||
"description": "声明本插件针对的 SDK 版本(完整版本号;patch 位恒为 .0)"
|
||||
}
|
||||
}
|
||||
174
tools/vscode-hmapdev/src/core.ts
Normal file
174
tools/vscode-hmapdev/src/core.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 纯逻辑层:不 import vscode,便于用 node --test 直接单测。
|
||||
*
|
||||
* 这里的规矩必须与工具链一致(tools/hmapdev/sdk_resolve.go):
|
||||
* - SDK 版本跟随内核中版本、**patch 位恒为 .0** → 一条内核线只有一个 SDK 版本;
|
||||
* - 因此 plg.json 的 `sdk` 必须是**完整版本号**(x.y.z),区间写法("1.2")要报错,
|
||||
* 否则项目会以为「同一条线里还能挑不同 SDK」。
|
||||
*/
|
||||
|
||||
/** plg.json 的字段(未知字段保留,不做拒绝)。 */
|
||||
export interface PlgConfig {
|
||||
name?: string;
|
||||
name_zh?: string;
|
||||
name_en?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
entry?: string;
|
||||
sdk?: string;
|
||||
tags?: string[];
|
||||
targets?: string;
|
||||
sdk_path?: string;
|
||||
outdir?: string;
|
||||
bundle?: boolean;
|
||||
replaces?: Record<string, string>;
|
||||
source_dirs?: string[];
|
||||
}
|
||||
|
||||
/** 诊断级别(与 vscode.DiagnosticSeverity 数值对齐,避免耦合)。 */
|
||||
export enum Severity {
|
||||
Error = 0,
|
||||
Warning = 1,
|
||||
Information = 2,
|
||||
Hint = 3,
|
||||
}
|
||||
|
||||
export interface PlgDiagnostic {
|
||||
severity: Severity;
|
||||
message: string;
|
||||
/** plg.json 里的字段名(用于在 JSON 文档里定位)。 */
|
||||
field?: string;
|
||||
}
|
||||
|
||||
/** 完整版本号(x.y.z,允许 v 前缀)。 */
|
||||
export function isFullVersion(v: string): boolean {
|
||||
return /^v?\d+\.\d+\.\d+$/.test((v ?? "").trim());
|
||||
}
|
||||
|
||||
/** 中版本(x.y)。 */
|
||||
export function isMinorVersion(v: string): boolean {
|
||||
return /^v?\d+\.\d+$/.test((v ?? "").trim());
|
||||
}
|
||||
|
||||
export function normalizeVersion(v: string): string {
|
||||
return (v ?? "").trim().replace(/^v/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 plg.json。
|
||||
*
|
||||
* `installedSdks` 为本地 SDK 存储里已安装的版本(不带 v 前缀);传 undefined 表示
|
||||
* 没探测(例如工具链不可用),此时只校验格式、不报「未安装」。
|
||||
*/
|
||||
export function validatePlg(cfg: PlgConfig, installedSdks?: string[]): PlgDiagnostic[] {
|
||||
const out: PlgDiagnostic[] = [];
|
||||
const req = (field: keyof PlgConfig, hint: string) => {
|
||||
const v = cfg[field];
|
||||
if (v === undefined || v === null || String(v).trim() === "") {
|
||||
out.push({ severity: Severity.Error, message: `${field} 不能为空(${hint})`, field: field as string });
|
||||
}
|
||||
};
|
||||
req("name", "插件名,与目录名一致最省事");
|
||||
req("entry", "入口产物,子进程模式通常是 plugin.bin");
|
||||
req("version", "插件自身版本号,如 0.1.0");
|
||||
|
||||
// SDK 声明:这是「工具链自动选 SDK 版本」的依据,缺了就只能退回 current
|
||||
if (cfg.sdk === undefined || cfg.sdk === null || String(cfg.sdk).trim() === "") {
|
||||
out.push({
|
||||
severity: Severity.Warning,
|
||||
message: "缺少 sdk 字段:工具链无法据此选择 SDK 版本,会退回存储里的 current(换机器/换人后容易编出与预期不符的产物)",
|
||||
field: "sdk",
|
||||
});
|
||||
} else if (isMinorVersion(cfg.sdk)) {
|
||||
out.push({
|
||||
severity: Severity.Error,
|
||||
message:
|
||||
`sdk 必须是完整版本号(如 "1.2.0"):${cfg.sdk} 这种区间写法会让人以为同一条内核线里还能挑不同 SDK。` +
|
||||
`SDK 版本跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本。`,
|
||||
field: "sdk",
|
||||
});
|
||||
} else if (!isFullVersion(cfg.sdk)) {
|
||||
out.push({ severity: Severity.Error, message: `sdk 不是合法版本号(写法:"1.2.0")`, field: "sdk" });
|
||||
} else if (installedSdks && !installedSdks.includes(normalizeVersion(cfg.sdk))) {
|
||||
const have = installedSdks.length ? installedSdks.join(", ") : "(存储里还没有任何 SDK)";
|
||||
out.push({
|
||||
severity: Severity.Error,
|
||||
message: `声明的 SDK ${normalizeVersion(cfg.sdk)} 未安装。已安装:${have}。安装:hmapdev sdk install v${normalizeVersion(cfg.sdk)}`,
|
||||
field: "sdk",
|
||||
});
|
||||
}
|
||||
|
||||
// 目标平台:windows 目前不支持(协议 2 的统一共享内存区未移植)
|
||||
const targets = (cfg.targets ?? "").toLowerCase();
|
||||
if (targets.includes("windows")) {
|
||||
out.push({
|
||||
severity: Severity.Information,
|
||||
message: "windows 目标暂不支持插件产物:协议 2 的统一共享内存区未移植 Windows(内核改走 WSL2)。构建会在该目标上明确报错。",
|
||||
field: "targets",
|
||||
});
|
||||
}
|
||||
if (!cfg.targets) {
|
||||
out.push({ severity: Severity.Information, message: "未声明 targets,构建时按默认目标处理", field: "targets" });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 `hmapdev sdk list` 的输出,返回已安装版本(去 v 前缀、升序)。
|
||||
*
|
||||
* 输出形如:
|
||||
* Installed SDK versions:
|
||||
* * v1.2.0
|
||||
* v0.8.0
|
||||
* 每行可能带 `*` 标记(当前版本)或前导空格。
|
||||
*/
|
||||
export function parseSdkList(text: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const raw of (text ?? "").split(/\r?\n/)) {
|
||||
const line = raw.trim().replace(/^\*\s*/, "").trim();
|
||||
const m = /^v?(\d+\.\d+\.\d+)$/.exec(line);
|
||||
if (m) {
|
||||
out.push(m[1]);
|
||||
}
|
||||
}
|
||||
return out.sort(compareVersions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 `hmapdev version` 的自述(首行形如 `hmapdev 1.2.0`)。
|
||||
*
|
||||
* 只认**以数字开头**的版本 token:否则 `hmapdev 未找到` / `hmapdev error`
|
||||
* 这类输出会被当成版本号,把「工具链不在」误报成「工具链 1.x」
|
||||
* (状态栏与「是否已装 SDK」的判断都基于它,假版本会让诊断全面失真)。
|
||||
*/
|
||||
export function parseToolchainVersion(text: string): string {
|
||||
for (const raw of (text ?? "").split(/\r?\n/)) {
|
||||
const m = /^hmapdev\s+v?(\d+(?:\.\d+)*(?:[-+.][0-9A-Za-z.-]+)?)\s*$/.exec(raw.trim());
|
||||
if (m) {
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 数值比较 x.y.z(字典序会把 1.2.9 排在 1.2.10 之后)。 */
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const pa = normalizeVersion(a).split(".").map((n) => parseInt(n, 10) || 0);
|
||||
const pb = normalizeVersion(b).split(".").map((n) => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
if (d !== 0) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 状态栏文本:插件 + 声明 SDK + 工具链版本(缺项用 "?")。 */
|
||||
export function statusBarText(cfg: PlgConfig | undefined, toolchainVersion: string): string {
|
||||
const name = cfg?.name?.trim() || "(未识别插件)";
|
||||
const sdk = cfg?.sdk ? normalizeVersion(cfg.sdk) : "未声明";
|
||||
const tc = toolchainVersion ? `hmapdev ${toolchainVersion}` : "hmapdev 未找到";
|
||||
return `${name} · SDK ${sdk} · ${tc}`;
|
||||
}
|
||||
272
tools/vscode-hmapdev/src/extension.ts
Normal file
272
tools/vscode-hmapdev/src/extension.ts
Normal file
@ -0,0 +1,272 @@
|
||||
import * as cp from "child_process";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
import { PlgConfig, Severity, statusBarText, validatePlg } from "./core";
|
||||
import { Toolchain } from "./toolchain";
|
||||
|
||||
let out: vscode.OutputChannel;
|
||||
let tc: Toolchain;
|
||||
let status: vscode.StatusBarItem;
|
||||
let diagnostics: vscode.DiagnosticCollection;
|
||||
let tailChild: cp.ChildProcess | undefined;
|
||||
|
||||
/** 找到工作区里的 plg.json(多个时取第一个并提示)。 */
|
||||
async function findPlg(): Promise<vscode.Uri | undefined> {
|
||||
const found = await vscode.workspace.findFiles("**/plg.json", "**/{node_modules,out,dist,build}/**", 5);
|
||||
if (found.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return found[0];
|
||||
}
|
||||
|
||||
async function readPlg(uri: vscode.Uri): Promise<PlgConfig | undefined> {
|
||||
try {
|
||||
const txt = Buffer.from(await vscode.workspace.fs.readFile(uri)).toString("utf8");
|
||||
return JSON.parse(txt) as PlgConfig;
|
||||
} catch (e) {
|
||||
out.appendLine(`error: 解析 ${uri.fsPath} 失败:${e instanceof Error ? e.message : String(e)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function severityToVscode(s: Severity): vscode.DiagnosticSeverity {
|
||||
switch (s) {
|
||||
case Severity.Error:
|
||||
return vscode.DiagnosticSeverity.Error;
|
||||
case Severity.Warning:
|
||||
return vscode.DiagnosticSeverity.Warning;
|
||||
case Severity.Information:
|
||||
return vscode.DiagnosticSeverity.Information;
|
||||
default:
|
||||
return vscode.DiagnosticSeverity.Hint;
|
||||
}
|
||||
}
|
||||
|
||||
/** 在 JSON 文档里定位字段(找不到就标整个文件,至少让人看见)。 */
|
||||
function rangeForField(doc: vscode.TextDocument, field?: string): vscode.Range {
|
||||
if (field) {
|
||||
const idx = doc.getText().indexOf(`"${field}"`);
|
||||
if (idx >= 0) {
|
||||
const start = doc.positionAt(idx);
|
||||
const end = doc.positionAt(idx + field.length + 2);
|
||||
return new vscode.Range(start, end);
|
||||
}
|
||||
}
|
||||
return new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0));
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
const uri = await findPlg();
|
||||
diagnostics.clear();
|
||||
if (!uri) {
|
||||
status.text = statusBarText(undefined, await tc.version());
|
||||
status.tooltip = "工作区里没有找到 plg.json(本扩展只在插件工程里工作)";
|
||||
return;
|
||||
}
|
||||
const cfg = await readPlg(uri);
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tcVersion = await tc.version();
|
||||
const diagSetting = vscode.workspace.getConfiguration("hmapdev").get<boolean>("diagnoseSdk", true);
|
||||
const installed = diagSetting ? await tc.sdkList() : undefined;
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const items = validatePlg(cfg, installed).map((d) => {
|
||||
const vd = new vscode.Diagnostic(rangeForField(doc, d.field), d.message, severityToVscode(d.severity));
|
||||
vd.source = "hmapdev";
|
||||
return vd;
|
||||
});
|
||||
diagnostics.set(uri, items);
|
||||
|
||||
const errors = items.filter((d) => d.severity === vscode.DiagnosticSeverity.Error).length;
|
||||
status.text = `$(tools) ${statusBarText(cfg, tcVersion)}`;
|
||||
status.backgroundColor = tc.isMissing()
|
||||
? new vscode.ThemeColor("statusBarItem.errorBackground")
|
||||
: errors > 0
|
||||
? new vscode.ThemeColor("statusBarItem.warningBackground")
|
||||
: undefined;
|
||||
const installedText = installed ? installed.join(", ") || "(无)" : "(未探测)";
|
||||
status.tooltip = [
|
||||
`插件:${cfg.name ?? "?"}`,
|
||||
`声明 SDK:${cfg.sdk ?? "未声明"}`,
|
||||
`已安装 SDK:${installedText}`,
|
||||
`工具链:${tcVersion ? `hmapdev ${tcVersion}` : "未找到(检查 hmapdev.path / PATH)"}`,
|
||||
`plg.json:${uri.fsPath}`,
|
||||
].join("\n");
|
||||
status.command = "hmapdev.openPlgJson";
|
||||
status.show();
|
||||
}
|
||||
|
||||
async function pluginDir(): Promise<string | undefined> {
|
||||
const uri = await findPlg();
|
||||
return uri ? path.dirname(uri.fsPath) : undefined;
|
||||
}
|
||||
|
||||
async function withDir(fn: (dir: string) => unknown | Promise<unknown>): Promise<void> {
|
||||
const dir = await pluginDir();
|
||||
if (!dir) {
|
||||
void vscode.window.showWarningMessage("当前工作区没有 plg.json,无法定位插件工程。");
|
||||
return;
|
||||
}
|
||||
await fn(dir);
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
out = vscode.window.createOutputChannel("hmapdev");
|
||||
tc = new Toolchain(out);
|
||||
diagnostics = vscode.languages.createDiagnosticCollection("hmapdev");
|
||||
status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
||||
|
||||
context.subscriptions.push(out, diagnostics, status);
|
||||
|
||||
const reg = (id: string, fn: () => unknown) =>
|
||||
context.subscriptions.push(vscode.commands.registerCommand(id, async () => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
out.appendLine(`error: ${msg}`);
|
||||
void vscode.window.showErrorMessage(`hmapdev: ${msg}`);
|
||||
}
|
||||
}));
|
||||
|
||||
reg("hmapdev.build", () => withDir((d) => tc.runInTerminal(["build"], d, "hmapdev build")));
|
||||
reg("hmapdev.buildAll", () => withDir((d) => tc.runInTerminal(["build", "--target", "all"], d, "hmapdev build all")));
|
||||
reg("hmapdev.clean", () => withDir((d) => tc.runInTerminal(["clean"], d, "hmapdev clean")));
|
||||
reg("hmapdev.run", () => withDir((d) => tc.runInTerminal(["debug", d], d, "hmapdev debug")));
|
||||
|
||||
reg("hmapdev.showVersion", async () => {
|
||||
out.show(true);
|
||||
const v = await tc.version(true);
|
||||
if (!v) {
|
||||
void vscode.window.showErrorMessage("找不到 hmapdev:请把它放到 PATH,或设置 hmapdev.path。");
|
||||
return;
|
||||
}
|
||||
await tc.runAndLog(["version"], process.cwd());
|
||||
});
|
||||
|
||||
reg("hmapdev.listSdk", async () => {
|
||||
out.show(true);
|
||||
await tc.runAndLog(["sdk", "list"], process.cwd());
|
||||
await refresh();
|
||||
});
|
||||
|
||||
reg("hmapdev.installSdk", async () => {
|
||||
const v = await vscode.window.showInputBox({
|
||||
title: "安装 SDK 版本",
|
||||
prompt: '输入完整版本号(如 1.2.0)或 latest。注意:SDK 版本跟随内核中版本,patch 位恒为 .0。',
|
||||
placeHolder: "1.2.0",
|
||||
});
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
tc.runInTerminal(["sdk", "install", `v${v.replace(/^v/, "")}`], process.cwd(), "hmapdev sdk install");
|
||||
void vscode.window.showInformationMessage(`安装完成后执行「hmapdev: 刷新状态」以重新校验。`);
|
||||
});
|
||||
|
||||
reg("hmapdev.useSdk", async () => {
|
||||
const list = await tc.sdkList(true);
|
||||
if (!list || list.length === 0) {
|
||||
void vscode.window.showWarningMessage("没有探测到已安装的 SDK 版本(先跑「hmapdev: 列出 SDK 版本」看看)。");
|
||||
return;
|
||||
}
|
||||
const pick = await vscode.window.showQuickPick(list, { title: "切换当前 SDK 版本(存储里的 current)" });
|
||||
if (!pick) {
|
||||
return;
|
||||
}
|
||||
tc.runInTerminal(["sdk", "use", `v${pick}`], process.cwd(), "hmapdev sdk use");
|
||||
setTimeout(() => void refresh(), 1500);
|
||||
});
|
||||
|
||||
reg("hmapdev.tailKernelLog", async () => {
|
||||
const cfgDir = vscode.workspace.getConfiguration("hmapdev");
|
||||
let dataDir = cfgDir.get<string>("kernelDataDir", "");
|
||||
if (!dataDir) {
|
||||
const answer = await vscode.window.showInputBox({
|
||||
title: "内核数据目录",
|
||||
prompt: "homed -data 指向的目录(用于跟随内核日志)。填一次会记住到设置里。",
|
||||
placeHolder: "/home/newqqagent",
|
||||
});
|
||||
if (!answer) {
|
||||
return;
|
||||
}
|
||||
dataDir = answer;
|
||||
await cfgDir.update("kernelDataDir", dataDir, vscode.ConfigurationTarget.Workspace);
|
||||
}
|
||||
const uri = await findPlg();
|
||||
const filter = uri ? (await readPlg(uri))?.name ?? "" : "";
|
||||
tailChild?.kill();
|
||||
tailChild = tc.tailKernelLog(dataDir, filter);
|
||||
out.show(true);
|
||||
});
|
||||
|
||||
reg("hmapdev.stopTailKernelLog", () => {
|
||||
if (tailChild) {
|
||||
tailChild.kill();
|
||||
tailChild = undefined;
|
||||
out.appendLine("已停止跟随内核日志");
|
||||
}
|
||||
});
|
||||
|
||||
reg("hmapdev.openPlgJson", async () => {
|
||||
const uri = await findPlg();
|
||||
if (uri) {
|
||||
await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(uri));
|
||||
} else {
|
||||
void vscode.window.showWarningMessage("工作区里没有 plg.json。");
|
||||
}
|
||||
});
|
||||
|
||||
reg("hmapdev.refresh", async () => {
|
||||
await tc.sdkList(true);
|
||||
await tc.version(true);
|
||||
await refresh();
|
||||
});
|
||||
|
||||
// 任务提供者:把 hmapdev 动作接进 VSCode 的任务体系(可绑定快捷键 / 串联依赖 / 复用问题匹配器)
|
||||
context.subscriptions.push(
|
||||
vscode.tasks.registerTaskProvider("hmapdev", {
|
||||
provideTasks: async () => {
|
||||
const dir = await pluginDir();
|
||||
if (!dir) {
|
||||
return [];
|
||||
}
|
||||
const mk = (action: string, label: string, args: string[]) => {
|
||||
const def: vscode.TaskDefinition = { type: "hmapdev", action };
|
||||
const exec = new vscode.ProcessExecution(
|
||||
vscode.workspace.getConfiguration("hmapdev").get<string>("path", "hmapdev") || "hmapdev",
|
||||
args,
|
||||
{ cwd: dir }
|
||||
);
|
||||
return new vscode.Task(def, vscode.TaskScope.Workspace, label, "hmapdev", exec, ["$hmapdev-go"]);
|
||||
};
|
||||
return [
|
||||
mk("build", "hmapdev: build", ["build"]),
|
||||
mk("buildAll", "hmapdev: build (all targets)", ["build", "--target", "all"]),
|
||||
mk("clean", "hmapdev: clean", ["clean"]),
|
||||
mk("run", "hmapdev: run (interpreted)", ["debug", dir]),
|
||||
];
|
||||
},
|
||||
resolveTask: (task) => task,
|
||||
})
|
||||
);
|
||||
|
||||
// plg.json 变化 → 重算诊断(含保存与外部修改)
|
||||
const watcher = vscode.workspace.createFileSystemWatcher("**/plg.json");
|
||||
context.subscriptions.push(
|
||||
watcher,
|
||||
watcher.onDidChange(() => void refresh()),
|
||||
watcher.onDidCreate(() => void refresh()),
|
||||
watcher.onDidDelete(() => void refresh())
|
||||
);
|
||||
|
||||
void refresh();
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
tailChild?.kill();
|
||||
tailChild = undefined;
|
||||
}
|
||||
106
tools/vscode-hmapdev/src/test/core.test.ts
Normal file
106
tools/vscode-hmapdev/src/test/core.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import * as assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
PlgConfig,
|
||||
Severity,
|
||||
compareVersions,
|
||||
isFullVersion,
|
||||
isMinorVersion,
|
||||
normalizeVersion,
|
||||
parseSdkList,
|
||||
parseToolchainVersion,
|
||||
statusBarText,
|
||||
validatePlg,
|
||||
} from "../core";
|
||||
|
||||
const good: PlgConfig = { name: "memo", version: "0.1.0", entry: "plugin.bin", sdk: "1.2.0" };
|
||||
|
||||
test("isFullVersion / isMinorVersion 区分完整版本与区间写法", () => {
|
||||
assert.equal(isFullVersion("1.2.0"), true);
|
||||
assert.equal(isFullVersion("v1.2.0"), true);
|
||||
assert.equal(isFullVersion("1.2"), false);
|
||||
assert.equal(isFullVersion("1.2.3.4"), false);
|
||||
assert.equal(isMinorVersion("1.2"), true);
|
||||
assert.equal(isMinorVersion("1.2.0"), false);
|
||||
assert.equal(normalizeVersion("v1.2.0"), "1.2.0");
|
||||
});
|
||||
|
||||
test("合法的 plg.json 不产生错误", () => {
|
||||
const d = validatePlg(good, ["1.2.0"]);
|
||||
assert.equal(d.filter((x) => x.severity === Severity.Error).length, 0, JSON.stringify(d));
|
||||
});
|
||||
|
||||
test("缺必需字段要报错并指出字段", () => {
|
||||
const d = validatePlg({ sdk: "1.2.0" }, ["1.2.0"]);
|
||||
const fields = d.filter((x) => x.severity === Severity.Error).map((x) => x.field).sort();
|
||||
assert.deepEqual(fields, ["entry", "name", "version"]);
|
||||
});
|
||||
|
||||
test("区间写法 1.2 必须被拒,并说明 patch 位恒为 .0", () => {
|
||||
const d = validatePlg({ ...good, sdk: "1.2" }, ["1.2.0"]);
|
||||
const err = d.find((x) => x.field === "sdk" && x.severity === Severity.Error);
|
||||
assert.ok(err, "区间写法应报错");
|
||||
assert.match(err!.message, /完整版本号/);
|
||||
assert.match(err!.message, /patch 位恒为 \.0/);
|
||||
});
|
||||
|
||||
test("缺 sdk 字段只警告(向后兼容存量项目)", () => {
|
||||
const d = validatePlg({ name: "memo", version: "0.1.0", entry: "plugin.bin" }, ["1.2.0"]);
|
||||
const sdk = d.find((x) => x.field === "sdk");
|
||||
assert.ok(sdk);
|
||||
assert.equal(sdk!.severity, Severity.Warning);
|
||||
});
|
||||
|
||||
test("声明的 SDK 未安装要报错并给出安装命令", () => {
|
||||
const d = validatePlg(good, ["0.8.0"]);
|
||||
const err = d.find((x) => x.field === "sdk" && x.severity === Severity.Error);
|
||||
assert.ok(err, "未安装应报错");
|
||||
assert.match(err!.message, /hmapdev sdk install v1\.2\.0/);
|
||||
assert.match(err!.message, /0\.8\.0/);
|
||||
});
|
||||
|
||||
test("探测不到已装列表时不误报未安装", () => {
|
||||
const d = validatePlg(good, undefined);
|
||||
assert.equal(d.filter((x) => x.severity === Severity.Error).length, 0, JSON.stringify(d));
|
||||
});
|
||||
|
||||
test("windows 目标给提示(协议 2 未移植)", () => {
|
||||
const d = validatePlg({ ...good, targets: "linux/amd64,windows/amd64" }, ["1.2.0"]);
|
||||
const info = d.find((x) => x.field === "targets");
|
||||
assert.ok(info);
|
||||
assert.match(info!.message, /WSL2/);
|
||||
});
|
||||
|
||||
test("parseSdkList 吃掉 * 标记与空格,并按数值排序", () => {
|
||||
const text = ["Installed SDK versions:", " * v1.2.0", " v0.8.0", " v1.2.10"].join("\n");
|
||||
assert.deepEqual(parseSdkList(text), ["0.8.0", "1.2.0", "1.2.10"]);
|
||||
assert.deepEqual(parseSdkList("No SDK versions installed."), []);
|
||||
});
|
||||
|
||||
test("parseToolchainVersion 从 self-report 里取版本", () => {
|
||||
const text = ["hmapdev 1.2.0", " SDK 模块: gitcode.com/JianFeeeee/homeagent-sdk", " 构建用 Go: go1.25.12"].join("\n");
|
||||
assert.equal(parseToolchainVersion(text), "1.2.0");
|
||||
assert.equal(parseToolchainVersion("Usage:\n hmapdev init <name>"), "");
|
||||
});
|
||||
|
||||
// 反向核对抓到的真缺陷:`hmapdev <非版本>` 形状的输出曾被当成版本号,
|
||||
// 于是「工具链不在」会被显示成「工具链 <垃圾词>」,并让 SDK 诊断跟着失真。
|
||||
test("parseToolchainVersion 不会把非版本 token 当成版本", () => {
|
||||
for (const bad of ["hmapdev 未找到", "hmapdev error", "hmapdev not found", "hmapdev -v", "hmapdev"]) {
|
||||
assert.equal(parseToolchainVersion(bad), "", `不应从 ${JSON.stringify(bad)} 解析出版本`);
|
||||
}
|
||||
assert.equal(parseToolchainVersion("hmapdev 1.3.0-dev"), "1.3.0-dev"); // 开发构建的后缀要带出来
|
||||
assert.equal(parseToolchainVersion("hmapdev v1.2.0"), "1.2.0");
|
||||
});
|
||||
|
||||
test("compareVersions 是数值比较(1.2.10 > 1.2.9)", () => {
|
||||
assert.ok(compareVersions("1.2.10", "1.2.9") > 0);
|
||||
assert.ok(compareVersions("1.2.0", "1.2.0") === 0);
|
||||
assert.ok(compareVersions("0.8.0", "1.2.0") < 0);
|
||||
});
|
||||
|
||||
test("状态栏文本包含插件、声明 SDK 与工具链版本", () => {
|
||||
assert.equal(statusBarText(good, "1.2.0"), "memo · SDK 1.2.0 · hmapdev 1.2.0");
|
||||
assert.equal(statusBarText({ ...good, sdk: undefined }, ""), "memo · SDK 未声明 · hmapdev 未找到");
|
||||
});
|
||||
139
tools/vscode-hmapdev/src/toolchain.ts
Normal file
139
tools/vscode-hmapdev/src/toolchain.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import * as cp from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
import { parseSdkList, parseToolchainVersion } from "./core";
|
||||
|
||||
/** execFile 的 Promise 版(不引第三方依赖)。 */
|
||||
function execFile(
|
||||
file: string,
|
||||
args: string[],
|
||||
cwd: string,
|
||||
timeoutMs = 120_000
|
||||
): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
return new Promise((resolve) => {
|
||||
cp.execFile(file, args, { cwd, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
|
||||
const code = err && typeof (err as { code?: number }).code === "number" ? (err as { code: number }).code : err ? 1 : 0;
|
||||
resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具链封装:定位 hmapdev、执行命令、缓存 version / sdk list。
|
||||
*
|
||||
* 为什么要缓存并显式 refresh:SDK 存储会在外部变化(`hmapdev sdk install` 之后),
|
||||
* 而诊断信息依赖它——不刷新就会一直报「未安装」。
|
||||
*/
|
||||
export class Toolchain {
|
||||
private versionCache?: string;
|
||||
private sdkCache?: string[];
|
||||
private missing = false;
|
||||
|
||||
constructor(private readonly out: vscode.OutputChannel) {}
|
||||
|
||||
private get exe(): string {
|
||||
return vscode.workspace.getConfiguration("hmapdev").get<string>("path", "hmapdev") || "hmapdev";
|
||||
}
|
||||
|
||||
/** 记录一条消息到输出通道(加上工具链前缀,便于与构建输出区分)。 */
|
||||
log(line: string): void {
|
||||
this.out.appendLine(line);
|
||||
}
|
||||
|
||||
async version(refresh = false): Promise<string> {
|
||||
if (this.versionCache !== undefined && !refresh) {
|
||||
return this.versionCache;
|
||||
}
|
||||
const r = await execFile(this.exe, ["version"], process.cwd(), 20_000);
|
||||
if (r.code !== 0 && !r.stdout) {
|
||||
this.missing = true;
|
||||
this.versionCache = "";
|
||||
return "";
|
||||
}
|
||||
this.missing = false;
|
||||
this.versionCache = parseToolchainVersion(r.stdout + r.stderr);
|
||||
return this.versionCache;
|
||||
}
|
||||
|
||||
async sdkList(refresh = false): Promise<string[] | undefined> {
|
||||
if (this.sdkCache !== undefined && !refresh) {
|
||||
return this.sdkCache;
|
||||
}
|
||||
const r = await execFile(this.exe, ["sdk", "list"], process.cwd(), 20_000);
|
||||
if (r.code !== 0 && !r.stdout) {
|
||||
this.sdkCache = undefined; // 探测不到就不做「未安装」判断,避免误报
|
||||
return undefined;
|
||||
}
|
||||
this.sdkCache = parseSdkList(r.stdout + r.stderr);
|
||||
return this.sdkCache;
|
||||
}
|
||||
|
||||
isMissing(): boolean {
|
||||
return this.missing;
|
||||
}
|
||||
|
||||
/** 在集成终端里执行(构建/运行这类长命令:要能看进度、能 Ctrl-C)。 */
|
||||
runInTerminal(args: string[], cwd: string, name: string): vscode.Terminal {
|
||||
const term = vscode.window.createTerminal({ name, cwd });
|
||||
term.show(true);
|
||||
const cmd = [this.exe, ...args].map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ");
|
||||
this.log(`$ ${cmd}`);
|
||||
term.sendText(cmd, true);
|
||||
return term;
|
||||
}
|
||||
|
||||
/** 一次性执行并把输出写进输出通道(查询类命令)。 */
|
||||
async runAndLog(args: string[], cwd: string): Promise<number> {
|
||||
this.log(`$ ${this.exe} ${args.join(" ")}`);
|
||||
const r = await execFile(this.exe, args, cwd, 60_000);
|
||||
if (r.stdout) {
|
||||
this.out.append(r.stdout);
|
||||
}
|
||||
if (r.stderr) {
|
||||
this.out.append(r.stderr);
|
||||
}
|
||||
return r.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟随内核日志:定位 <dataDir>/log 下最新的 homed 日志并按插件名过滤。
|
||||
*
|
||||
* 为什么这是「调试插件」的正路:插件是子进程、跑在内核里,真正的问题几乎都
|
||||
* 表现为内核日志里的几行(握手失败/崩溃重启/工具报错),在 IDE 里跟住它比
|
||||
* 反复手动 tail 高效得多。
|
||||
*/
|
||||
tailKernelLog(dataDir: string, filter: string): cp.ChildProcess | undefined {
|
||||
const logDir = path.join(dataDir, "log");
|
||||
if (!fs.existsSync(logDir)) {
|
||||
this.log(`error: 日志目录不存在:${logDir}(hmapdev.kernelDataDir 是否指对?)`);
|
||||
return undefined;
|
||||
}
|
||||
const newest = fs
|
||||
.readdirSync(logDir)
|
||||
.filter((f) => f.startsWith("homed_") && f.endsWith(".log"))
|
||||
.map((f) => ({ f, m: fs.statSync(path.join(logDir, f)).mtimeMs }))
|
||||
.sort((a, b) => b.m - a.m)[0];
|
||||
if (!newest) {
|
||||
this.log(`error: ${logDir} 下没有 homed_*.log`);
|
||||
return undefined;
|
||||
}
|
||||
const file = path.join(logDir, newest.f);
|
||||
this.log(`跟随 ${file}${filter ? `(过滤 ${filter})` : ""}`);
|
||||
const child = cp.spawn("tail", ["-F", file], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
const emit = (buf: Buffer) => {
|
||||
for (const line of buf.toString("utf8").split(/\r?\n/)) {
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
if (!filter || line.includes(filter)) {
|
||||
this.out.appendLine(line);
|
||||
}
|
||||
}
|
||||
};
|
||||
child.stdout?.on("data", emit);
|
||||
child.stderr?.on("data", emit);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
19
tools/vscode-hmapdev/tsconfig.json
Normal file
19
tools/vscode-hmapdev/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vscode"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "out"]
|
||||
}
|
||||
Reference in New Issue
Block a user