@mearl/setup 3.0.0-beta.1 → 3.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -35
- package/dist/cli.js +10 -10
- package/dist/lifecycle.js +1 -1
- package/dist/plugin.js +32 -8
- package/dist/setupVersion.js +1 -1
- package/dist/skillSourceReceipts.d.ts +25 -0
- package/dist/skillSourceReceipts.js +94 -0
- package/dist/skillSources.d.ts +7 -22
- package/dist/skillSources.js +43 -96
- package/dist/skills.js +4 -2
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全局安装 setup:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @mearl/setup@beta [install] [--local] [--cloud|--qoder-cloud] [--mcp]
|
|
9
|
-
npx @mearl/setup@beta update [--local] [--cloud|--qoder-cloud] [--mcp]
|
|
10
|
-
npx @mearl/setup@beta uninstall [--local] [--cloud|--qoder-cloud] [--mcp] [--full]
|
|
11
|
-
npx @mearl/setup@beta extension [--full] [--force]
|
|
12
|
-
npx @mearl/setup@beta plugin [name] <path>
|
|
13
|
-
npx @mearl/setup@beta plugin install <skills add 参数...>
|
|
14
|
-
npx @mearl/setup@beta plugin uninstall <name>
|
|
15
|
-
npx @mearl/setup@beta provider <install|update|uninstall> <package|id>
|
|
8
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta [install] [--local] [--cloud|--qoder-cloud] [--mcp]
|
|
9
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta update [--local] [--cloud|--qoder-cloud] [--mcp]
|
|
10
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall [--local] [--cloud|--qoder-cloud] [--mcp] [--full]
|
|
11
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension [--full] [--force]
|
|
12
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin [name] <path>
|
|
13
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin install <skills add 参数...>
|
|
14
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin uninstall <name>
|
|
15
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider <install|update|uninstall> <package|id>
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
浏览器 Provider 独立安装和演进。安装后运行 `mearl <providerId>` 查看该版本声明的能力、
|
|
19
19
|
配置和启动参数。
|
|
20
20
|
|
|
21
|
-
在 Qoder Cloud Agent 环境中,使用 `npx @mearl/setup@beta --qoder-cloud --yes` 选择 Qoder
|
|
21
|
+
在 Qoder Cloud Agent 环境中,使用 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --qoder-cloud --yes` 选择 Qoder
|
|
22
22
|
配对模式。它与 `--cloud` 安装同一套 cloud-server、client 和 Skill,只把 cloud-server
|
|
23
23
|
模式记录为 Qoder;`mearl-cloud-server` 的命令交互保持不变,`start` 和 `restart` 输出新的
|
|
24
24
|
配对指令后退出,不监听端口或保留守护进程。配对 URL 不包含 Qoder PAT。
|
|
@@ -28,7 +28,7 @@ npx @mearl/setup@beta provider <install|update|uninstall> <package|id>
|
|
|
28
28
|
一个命令安装 skill 并注册它的 CLI、SDK 和 MCP 能力:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npx @mearl/setup@beta plugin install https://github.com/F-loat/mearl/tree/v3.0.0-beta.1 --skill browser-tab-manager --yes
|
|
31
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin install https://github.com/F-loat/mearl/tree/v3.0.0-beta.1 --skill browser-tab-manager --yes
|
|
32
32
|
mearl tabs summary
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -45,8 +45,8 @@ Native preset 批次新建的插件注册使用 setup 托管归属,后续通
|
|
|
45
45
|
已通过其他渠道分发的 skill 或自定义入口,可以直接传入路径注册:
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
npx @mearl/setup@beta plugin /absolute/path/to/browser-tab-manager
|
|
49
|
-
npx @mearl/setup@beta plugin custom ./skills/custom/entry.mjs
|
|
48
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin /absolute/path/to/browser-tab-manager
|
|
49
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin custom ./skills/custom/entry.mjs
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
path 接受目录或具体文件;目录默认读取 `scripts/main.mjs` 或 `scripts/main.py`,相对路径基于调用工作目录解析。插件名优先使用显式 name,其次读取入口的 `name` metadata,最后回退到 Skill 目录名;常规注册只传 path,显式 name 仅用于覆盖或兼容旧入口。Python 插件要求 `python3 >= 3.9`,可通过 `MEARL_PYTHON` 指定解释器路径;setup 只验证入口可加载,不安装 Python,也不执行 `pip install`。
|
|
@@ -59,7 +59,7 @@ native-host 会幂等初始化 Skills 模式。基础环境补齐或初始化失
|
|
|
59
59
|
不再需要插件时执行:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
npx @mearl/setup@beta plugin uninstall tabs
|
|
62
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin uninstall tabs
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
若入口位于 `~/.agents/skills/<skill>/`,setup 会先执行
|
|
@@ -75,9 +75,9 @@ Provider 由 npm 包分发,但包本身无需暴露 CLI 或安装脚本。setu
|
|
|
75
75
|
可创建的浏览器类型名称及其表单参数:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
npx @mearl/setup@beta provider install @example/mearl-provider-device
|
|
79
|
-
npx @mearl/setup@beta provider update example-device
|
|
80
|
-
npx @mearl/setup@beta provider uninstall example-device
|
|
78
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider install @example/mearl-provider-device
|
|
79
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider update example-device
|
|
80
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider uninstall example-device
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
安装参数接受包名、版本或 dist-tag;更新和卸载可使用 providerId 或完整包名。核心 @mearl 包固定使用公共 npm registry;其余 scope 沿用调用方的包管理器配置,组织 registry 由能力安装引导提供。安装沿用启动 setup 的包管理器,
|
|
@@ -88,8 +88,8 @@ npx @mearl/setup@beta provider uninstall example-device
|
|
|
88
88
|
## 行为
|
|
89
89
|
|
|
90
90
|
- Mearl 自有 npm 包的安装和更新固定使用 `https://registry.npmjs.org/`,与最新版本查询使用同一来源;第三方包沿用当前包管理器的源配置。
|
|
91
|
-
- `install` 可以省略;直接执行 `npx @mearl/setup@beta` 等同于 `npx @mearl/setup@beta install`。
|
|
92
|
-
以选项开头时也默认安装,例如 `npx @mearl/setup@beta --cloud --yes`。
|
|
91
|
+
- `install` 可以省略;直接执行 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta` 等同于 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta install`。
|
|
92
|
+
以选项开头时也默认安装,例如 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --cloud --yes`。
|
|
93
93
|
- `install` 未指定环境时默认 `--local`。
|
|
94
94
|
- `-h` / `--help` 显示帮助,`-v` / `--version` 显示 setup 版本。
|
|
95
95
|
- `update` 和 `uninstall` 未指定环境时,从当前 `PATH` 中的
|
|
@@ -137,19 +137,19 @@ npx @mearl/setup@beta provider uninstall example-device
|
|
|
137
137
|
## 安装或更新浏览器扩展
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
npx @mearl/setup@beta extension
|
|
140
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
首次使用时可一次安装完整的本地浏览器环境:
|
|
144
144
|
|
|
145
145
|
```bash
|
|
146
|
-
npx @mearl/setup@beta extension --full
|
|
146
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension --full
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
首次安装、旧版 setup 扩展首次迁移,或 Native Host reload 不可用时才需要 CDP。如果
|
|
150
150
|
Chrome 尚未开放 CDP,交互命令会提示打开
|
|
151
151
|
`chrome://inspect/#remote-debugging`、启用远程调试并允许连接,然后等待安装完成。面向人的
|
|
152
|
-
安装指引不要添加 `--yes`。无 TTY 环境使用 `npx @mearl/setup@beta extension --yes`;它只尝试
|
|
152
|
+
安装指引不要添加 `--yes`。无 TTY 环境使用 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension --yes`;它只尝试
|
|
153
153
|
一次,CDP 不可用时立即中断。无法确认 setup 管理来源且 CDP 不可用时不会下载或修改插件
|
|
154
154
|
目录,打开 CDP 后重新执行同一命令即可。Chrome 版本过低、缺少扩展安装所需的 CDP 方法时,
|
|
155
155
|
命令会中断并提示升级 Chrome。
|
|
@@ -179,7 +179,7 @@ DevTools 面板的设置按钮会显示红点,快捷设置中提供“立即
|
|
|
179
179
|
|
|
180
180
|
自动更新不会降级较新的 Host,也不会改动 cloud、MCP 或独立安装的 Mearl Skill。
|
|
181
181
|
目标 npm 版本尚未发布、网络不可用或更新失败时,旧 Host 会继续运行;下一次扩展后台
|
|
182
|
-
生命周期可重新尝试,也可手动执行 `npx @mearl/setup@beta update --local`。
|
|
182
|
+
生命周期可重新尝试,也可手动执行 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta update --local`。
|
|
183
183
|
|
|
184
184
|
## 人工与 Agent 使用
|
|
185
185
|
|
|
@@ -194,41 +194,41 @@ DevTools 面板的设置按钮会显示红点,快捷设置中提供“立即
|
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
196
|
# 默认安装本地浏览器环境
|
|
197
|
-
npx @mearl/setup
|
|
197
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup
|
|
198
198
|
|
|
199
199
|
# 与上一条等价
|
|
200
|
-
npx @mearl/setup@beta install
|
|
200
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta install
|
|
201
201
|
|
|
202
202
|
# Agent / CI 无交互安装
|
|
203
|
-
npx @mearl/setup@beta --yes
|
|
203
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --yes
|
|
204
204
|
|
|
205
205
|
# 安装云端 Agent 环境
|
|
206
|
-
npx @mearl/setup@beta --cloud
|
|
206
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --cloud
|
|
207
207
|
|
|
208
208
|
# 同一台机器同时作为本地浏览器宿主和云端 Agent
|
|
209
|
-
npx @mearl/setup@beta --local --cloud
|
|
209
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --local --cloud
|
|
210
210
|
|
|
211
211
|
# 自动更新当前 PATH 中检测到的全部 Mearl 环境
|
|
212
|
-
npx @mearl/setup@beta update
|
|
212
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta update
|
|
213
213
|
|
|
214
214
|
# 独立安装或更新到最新浏览器扩展
|
|
215
|
-
npx @mearl/setup@beta extension
|
|
215
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension
|
|
216
216
|
|
|
217
217
|
# 安装完整本地环境(client、Native Host、Skill 和扩展)
|
|
218
|
-
npx @mearl/setup@beta extension --full
|
|
218
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension --full
|
|
219
219
|
|
|
220
220
|
# 只卸载本地环境
|
|
221
|
-
npx @mearl/setup@beta uninstall --local
|
|
221
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall --local
|
|
222
222
|
|
|
223
223
|
# 自动检测并无交互卸载
|
|
224
|
-
npx @mearl/setup@beta uninstall --yes
|
|
224
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall --yes
|
|
225
225
|
|
|
226
226
|
# 卸载环境,并清理全部 CLI 插件与 Provider
|
|
227
|
-
npx @mearl/setup@beta uninstall --full
|
|
227
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall --full
|
|
228
228
|
|
|
229
229
|
# 安装可选浏览器 Provider
|
|
230
|
-
npx @mearl/setup@beta provider install @example/mearl-provider-device
|
|
231
|
-
npx @mearl/setup@beta provider install @example/mearl-provider-cloud
|
|
230
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider install @example/mearl-provider-device
|
|
231
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider install @example/mearl-provider-cloud
|
|
232
232
|
```
|
|
233
233
|
|
|
234
234
|
原有 Mearl 环境包的 npm/npx 安装方式和 CLI 保持可用;setup 是统一的便利入口。
|
package/dist/cli.js
CHANGED
|
@@ -177,7 +177,7 @@ function printHelp() {
|
|
|
177
177
|
const output = `Mearl Setup v${SETUP_VERSION}
|
|
178
178
|
|
|
179
179
|
Usage:
|
|
180
|
-
npx @mearl/setup [command] [options]
|
|
180
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup [command] [options]
|
|
181
181
|
|
|
182
182
|
Environment commands:
|
|
183
183
|
install [options] Install an environment (default command; target: local)
|
|
@@ -221,15 +221,15 @@ General options:
|
|
|
221
221
|
--help, -h Show this help
|
|
222
222
|
|
|
223
223
|
Examples:
|
|
224
|
-
npx @mearl/setup
|
|
225
|
-
npx @mearl/setup --cloud
|
|
226
|
-
npx @mearl/setup --local --cloud
|
|
227
|
-
npx @mearl/setup update
|
|
228
|
-
npx @mearl/setup uninstall --full
|
|
229
|
-
npx @mearl/setup plugin uninstall yuque
|
|
230
|
-
npx @mearl/setup provider install @example/mearl-provider-brow
|
|
231
|
-
npx @mearl/setup extension --full`;
|
|
232
|
-
console.log(output.replaceAll('npx @mearl/setup', SETUP_COMMAND));
|
|
224
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup
|
|
225
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup --cloud
|
|
226
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup --local --cloud
|
|
227
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup update
|
|
228
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup uninstall --full
|
|
229
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup plugin uninstall yuque
|
|
230
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup provider install @example/mearl-provider-brow
|
|
231
|
+
npx --@mearl:registry=https://registry.npmjs.org @mearl/setup extension --full`;
|
|
232
|
+
console.log(output.replaceAll('npx --@mearl:registry=https://registry.npmjs.org @mearl/setup', SETUP_COMMAND));
|
|
233
233
|
}
|
|
234
234
|
async function runSetupLifecycle(options, dependencies) {
|
|
235
235
|
let preset;
|
package/dist/lifecycle.js
CHANGED
|
@@ -236,7 +236,7 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
236
236
|
writeLine('No installed Mearl environment was detected on the current PATH.');
|
|
237
237
|
if (options.operation === 'uninstall')
|
|
238
238
|
return true;
|
|
239
|
-
writeLine('Run: npx @mearl/setup');
|
|
239
|
+
writeLine('Run: npx --@mearl:registry=https://registry.npmjs.org @mearl/setup');
|
|
240
240
|
return false;
|
|
241
241
|
}
|
|
242
242
|
if (options.operation === 'update') {
|
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readlinkSync, realpathSync, rmdirSync, statSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import { assertPluginName, findPluginDirectoryEntry, readOfficialPluginCatalog, findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, OFFICIAL_PLUGIN_MANAGER, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
4
|
+
import { assertPluginName, findPluginDirectoryEntry, skillManifestPath, readOfficialPluginCatalog, findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, OFFICIAL_PLUGIN_MANAGER, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
5
|
+
import { preserveSkillDisabled } from './skillSources.js';
|
|
5
6
|
import { claimPresetInstallation } from './presetInstallationLease.js';
|
|
6
7
|
import { ensurePluginRuntime } from './pluginRuntime.js';
|
|
7
8
|
import { captureSkillsCli, parseInstalledSkills, stripAnsi } from './skills.js';
|
|
@@ -52,8 +53,22 @@ export async function ensureOfficialPlugin(name, runner = runCommand) {
|
|
|
52
53
|
const definition = accepted?.plugins[name];
|
|
53
54
|
if (!definition)
|
|
54
55
|
throw new Error(`未知官方插件: ${name}`);
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
const existing = findPluginRegistration(name);
|
|
57
|
+
if (existing) {
|
|
58
|
+
if (!accepted.selection || existing.managedBy !== OFFICIAL_PLUGIN_MANAGER)
|
|
59
|
+
return 'existing';
|
|
60
|
+
const cli = await captureSkillsCli({ runCommand: runner });
|
|
61
|
+
if (!cli || cli.selection?.path !== accepted.selection.path || cli.selection.sha256 !== accepted.selection.sha256)
|
|
62
|
+
throw new Error('PLUGIN_CATALOG_SELECTION_CHANGED');
|
|
63
|
+
const declaration = cli.managedSkills[definition.skill];
|
|
64
|
+
if (!declaration?.source || declaration.source !== definition.installArgs[0])
|
|
65
|
+
throw new Error('PLUGIN_CATALOG_SKILL_UNDECLARED');
|
|
66
|
+
const status = cli.inspectGlobal(declaration.source, definition.skill, declaration.version, { migrateFrom: declaration.migrateFrom });
|
|
67
|
+
if (status.state === 'ready')
|
|
68
|
+
return 'existing';
|
|
69
|
+
if (status.state === 'conflict')
|
|
70
|
+
throw new Error(`${status.reason}: ${definition.skill}`);
|
|
71
|
+
}
|
|
57
72
|
const lease = claimPresetInstallation();
|
|
58
73
|
try {
|
|
59
74
|
return await withOwnedCommands(lease.lifecycle, async () => {
|
|
@@ -276,13 +291,22 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
276
291
|
if (status.state !== 'ready' && (registered || status.state !== 'registration'))
|
|
277
292
|
throw new Error(`${status.reason}: ${options.expectedSkill}`);
|
|
278
293
|
};
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
const assertSource = () => {
|
|
295
|
+
if (!options.expectedSkill || !declaration)
|
|
296
|
+
return;
|
|
297
|
+
const status = cli.inspectGlobal(declaration.source, options.expectedSkill, declaration.version, {
|
|
298
|
+
migrateFrom: options.registrationManagedBy === OFFICIAL_PLUGIN_MANAGER ? declaration.migrateFrom : undefined,
|
|
299
|
+
});
|
|
281
300
|
if (status.state === 'conflict')
|
|
282
301
|
throw new Error(`${status.reason}: ${options.expectedSkill}`);
|
|
283
|
-
}
|
|
302
|
+
};
|
|
303
|
+
assertSource();
|
|
284
304
|
await options.confirm?.();
|
|
285
|
-
|
|
305
|
+
assertSource();
|
|
306
|
+
const runInstall = () => cli.run(['add', ...args, '--global'], { tee: true });
|
|
307
|
+
const installed = await (options.expectedSkill
|
|
308
|
+
? preserveSkillDisabled(join(homedir(), '.agents', 'skills', options.expectedSkill), runInstall)
|
|
309
|
+
: runInstall());
|
|
286
310
|
if (installed.code !== 0)
|
|
287
311
|
return installed.code;
|
|
288
312
|
await options.confirm?.();
|
|
@@ -307,7 +331,7 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
307
331
|
}
|
|
308
332
|
const plugins = new Map();
|
|
309
333
|
for (const skillPath of paths) {
|
|
310
|
-
if (!statSync(join(skillPath, 'SKILL.md')).isFile())
|
|
334
|
+
if (!statSync(skillManifestPath(skillPath) ?? join(skillPath, 'SKILL.md')).isFile())
|
|
311
335
|
throw new Error(`安装结果缺少 SKILL.md: ${skillPath}`);
|
|
312
336
|
const entry = findPluginDirectoryEntry(skillPath);
|
|
313
337
|
if (!entry)
|
package/dist/setupVersion.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
3
|
export const SETUP_VERSION = require('../package.json').version;
|
|
4
|
-
export const SETUP_COMMAND = `npx @mearl/setup${SETUP_VERSION.includes('-beta.') ? '@' + SETUP_VERSION : ''}`;
|
|
4
|
+
export const SETUP_COMMAND = `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup${SETUP_VERSION.includes('-beta.') ? '@' + SETUP_VERSION : ''}`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type SkillSourceScope = 'global' | 'project';
|
|
2
|
+
export interface TrackedSkillSource {
|
|
3
|
+
name: string;
|
|
4
|
+
source: string;
|
|
5
|
+
scope: SkillSourceScope;
|
|
6
|
+
ref?: string;
|
|
7
|
+
skillPath?: string;
|
|
8
|
+
sourceType?: string;
|
|
9
|
+
expectedHash?: string;
|
|
10
|
+
hashKind: 'git-tree' | 'folder';
|
|
11
|
+
}
|
|
12
|
+
export interface SourceOptions {
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
homeDirectory?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function readTrackedSkillSources(scope: SkillSourceScope, cwd?: string, options?: SourceOptions): TrackedSkillSource[];
|
|
17
|
+
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
18
|
+
export declare function isPublicSkillSource(source: TrackedSkillSource): boolean;
|
|
19
|
+
export declare function gitSource(source: string, ref?: string): {
|
|
20
|
+
repository: string;
|
|
21
|
+
ref?: string;
|
|
22
|
+
subdirectory?: string;
|
|
23
|
+
};
|
|
24
|
+
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
25
|
+
export declare function matchesTrackedSkillSource(requested: string, tracked: TrackedSkillSource, normalize?: (source: string, sourceType?: string) => string): boolean;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
function lockFile(scope, cwd, options) {
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
return scope === 'project' ? join(cwd, 'skills-lock.json') : env.XDG_STATE_HOME ? join(env.XDG_STATE_HOME, 'skills', '.skill-lock.json') : join(options.homeDirectory ?? homedir(), '.agents', '.skill-lock.json');
|
|
8
|
+
}
|
|
9
|
+
export function readTrackedSkillSources(scope, cwd, options = {}) {
|
|
10
|
+
const file = lockFile(scope, cwd, options);
|
|
11
|
+
let text;
|
|
12
|
+
try {
|
|
13
|
+
if (statSync(file).size > 4 * 1024 * 1024)
|
|
14
|
+
throw new Error('Skill source lock exceeds 4 MiB');
|
|
15
|
+
text = readFileSync(file, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT')
|
|
19
|
+
return [];
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const lock = JSON.parse(text);
|
|
23
|
+
if (!lock || !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills))
|
|
24
|
+
throw new Error('Invalid Skills source lock');
|
|
25
|
+
return Object.entries(lock.skills).flatMap(([name, entry]) => {
|
|
26
|
+
if (!entry || typeof entry !== 'object' || entry.sourceType === 'node_modules')
|
|
27
|
+
return [];
|
|
28
|
+
if (!name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
29
|
+
throw new Error('Invalid tracked Skill name');
|
|
30
|
+
const source = typeof entry.sourceUrl === 'string' ? entry.sourceUrl : entry.source;
|
|
31
|
+
if (typeof source !== 'string' || !source.trim())
|
|
32
|
+
return [];
|
|
33
|
+
if (source.length > 4096 || /[\0\r\n]/.test(source))
|
|
34
|
+
throw new Error('Invalid tracked Skill source');
|
|
35
|
+
const expectedHash = scope === 'global' ? entry.skillFolderHash : entry.computedHash;
|
|
36
|
+
return [{ name, source, scope, ...(typeof entry.sourceType === 'string' ? { sourceType: entry.sourceType } : {}), ...(typeof entry.ref === 'string' ? { ref: entry.ref } : {}), ...(typeof entry.skillPath === 'string' ? { skillPath: entry.skillPath } : {}), ...(typeof expectedHash === 'string' && expectedHash ? { expectedHash } : {}), hashKind: scope === 'global' && typeof expectedHash === 'string' && /^[a-f0-9]{40}$/.test(expectedHash) ? 'git-tree' : 'folder' }];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
40
|
+
export function isPublicSkillSource(source) {
|
|
41
|
+
return source.sourceType === undefined || ['github', 'gitlab', 'git', 'local', 'well-known'].includes(source.sourceType);
|
|
42
|
+
}
|
|
43
|
+
export function gitSource(source, ref) {
|
|
44
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(source))
|
|
45
|
+
return { repository: `https://github.com/${source}.git`, ...(ref ? { ref } : {}) };
|
|
46
|
+
if (/^[\w.-]+@[\w.-]+:[^\s]+$/.test(source))
|
|
47
|
+
return { repository: source, ...(ref ? { ref } : {}) };
|
|
48
|
+
const url = new URL(source);
|
|
49
|
+
if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) || url.password || (url.username && url.protocol !== 'ssh:') || url.search || url.hash)
|
|
50
|
+
throw new Error('Skill source is not a supported Git repository');
|
|
51
|
+
if (url.hostname === 'github.com' && !url.port) {
|
|
52
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
53
|
+
if (parts.length >= 2)
|
|
54
|
+
return { repository: `https://github.com/${parts[0]}/${parts[1].replace(/\.git$/, '')}.git`, ...(ref || (['tree', 'blob'].includes(parts[2]) && parts[3]) ? { ref: ref || decodeURIComponent(parts[3]) } : {}), ...(['tree', 'blob'].includes(parts[2]) && parts.length > 4 ? { subdirectory: decodeURIComponent(parts.slice(4).join('/')).replace(/\/SKILL\.md$/, '') } : {}) };
|
|
55
|
+
}
|
|
56
|
+
return { repository: url.href, ...(ref ? { ref } : {}) };
|
|
57
|
+
}
|
|
58
|
+
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
59
|
+
export function matchesTrackedSkillSource(requested, tracked, normalize = source => source) {
|
|
60
|
+
const location = (source, ref, sourceType) => {
|
|
61
|
+
const value = normalize(source, sourceType);
|
|
62
|
+
if (typeof value !== 'string' || !value || value.length > 8192 || /[\0\r\n]/.test(value))
|
|
63
|
+
throw new Error('Invalid normalized Skill source');
|
|
64
|
+
const hash = value.indexOf('#');
|
|
65
|
+
const fragment = hash < 0 ? undefined : value.slice(hash + 1);
|
|
66
|
+
const plain = hash < 0 ? value : value.slice(0, hash);
|
|
67
|
+
const parsed = isAbsolute(plain) ? { repository: plain, ref: ref ?? fragment } : gitSource(plain, ref ?? fragment);
|
|
68
|
+
let repository = parsed.repository;
|
|
69
|
+
if (repository.startsWith('file:'))
|
|
70
|
+
repository = fileURLToPath(repository);
|
|
71
|
+
if (isAbsolute(repository))
|
|
72
|
+
repository = resolve(repository);
|
|
73
|
+
else {
|
|
74
|
+
const ssh = repository.match(/^[\w.-]+@([\w.-]+):(.+)$/);
|
|
75
|
+
const url = new URL(ssh ? `ssh://${ssh[1]}/${ssh[2]}` : repository);
|
|
76
|
+
// Authentication transport is independent of the repository's identity.
|
|
77
|
+
repository = `${url.hostname.toLowerCase()}${url.port ? ':' + url.port : ''}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`;
|
|
78
|
+
}
|
|
79
|
+
return { ...parsed, repository, ref: parsed.ref || undefined };
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const expected = location(requested);
|
|
83
|
+
const actual = location(tracked.source, tracked.ref, tracked.sourceType);
|
|
84
|
+
if (expected.repository !== actual.repository || expected.ref !== actual.ref)
|
|
85
|
+
return false;
|
|
86
|
+
if (!expected.subdirectory)
|
|
87
|
+
return true;
|
|
88
|
+
const path = (tracked.skillPath ?? actual.subdirectory ?? '').replace(/(?:^|\/)SKILL\.md$/, '').replace(/\/+$/, '');
|
|
89
|
+
return path === expected.subdirectory || path.startsWith(expected.subdirectory + '/');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return normalize(requested) === normalize(tracked.source, tracked.sourceType) && !tracked.ref;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/skillSources.d.ts
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import { type CommandRunner } from '@mearl/daemon-core';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
source: string;
|
|
6
|
-
scope: SkillSourceScope;
|
|
7
|
-
ref?: string;
|
|
8
|
-
skillPath?: string;
|
|
9
|
-
sourceType?: string;
|
|
10
|
-
expectedHash?: string;
|
|
11
|
-
hashKind: 'git-tree' | 'folder';
|
|
12
|
-
}
|
|
13
|
-
interface SourceOptions {
|
|
14
|
-
env?: NodeJS.ProcessEnv;
|
|
15
|
-
homeDirectory?: string;
|
|
16
|
-
}
|
|
17
|
-
export declare function readTrackedSkillSources(scope: SkillSourceScope, cwd?: string, options?: SourceOptions): TrackedSkillSource[];
|
|
18
|
-
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
19
|
-
export declare function isPublicSkillSource(source: TrackedSkillSource): boolean;
|
|
20
|
-
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
21
|
-
export declare function matchesTrackedSkillSource(requested: string, tracked: TrackedSkillSource, normalize?: (source: string, sourceType?: string) => string): boolean;
|
|
2
|
+
import { readTrackedSkillSources, type SourceOptions, type SkillSourceScope, type TrackedSkillSource } from './skillSourceReceipts.js';
|
|
3
|
+
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
4
|
+
export type { SkillSourceScope, TrackedSkillSource } from './skillSourceReceipts.js';
|
|
22
5
|
/** Matches the public Skills folder hash without writing to the installed Skill. */
|
|
23
6
|
export declare function hashSkillDirectory(directory: string, ignoreRuntimeFiles?: boolean): Promise<string>;
|
|
24
7
|
/** Checks source content in a temporary checkout; never invokes the mutating Skills `check` alias. */
|
|
@@ -31,16 +14,18 @@ export declare function checkTrackedSkillUpdates(options?: SourceOptions & {
|
|
|
31
14
|
scope: SkillSourceScope;
|
|
32
15
|
}>>;
|
|
33
16
|
export interface SkillInstallationStatus {
|
|
34
|
-
state: 'ready' | 'missing' | 'version' | 'registration' | 'conflict';
|
|
17
|
+
state: 'ready' | 'missing' | 'version' | 'registration' | 'migration' | 'conflict';
|
|
35
18
|
reason?: string;
|
|
36
19
|
}
|
|
37
20
|
/** Developer links keep their Git checkout as the source receipt without changing a manager's lock. */
|
|
38
21
|
export declare function readLinkedSkillSource(directory: string, name: string): TrackedSkillSource | undefined;
|
|
22
|
+
/** Keep the user's disabled state when an installer replaces the Skill directory. */
|
|
23
|
+
export declare function preserveSkillDisabled<T>(directory: string, operation: () => Promise<T>): Promise<T>;
|
|
39
24
|
/** CLI and Agent readiness use the same source, manifest and registration evidence. */
|
|
40
25
|
export declare function inspectGlobalSkill(source: string, name: string, version?: string, options?: {
|
|
41
26
|
homeDirectory?: string;
|
|
42
27
|
readSources?: typeof readTrackedSkillSources;
|
|
43
28
|
externalManager?: boolean;
|
|
29
|
+
migrateFrom?: readonly string[];
|
|
44
30
|
normalizeSource?: (source: string, sourceType?: string) => string;
|
|
45
31
|
}): SkillInstallationStatus;
|
|
46
|
-
export {};
|
package/dist/skillSources.js
CHANGED
|
@@ -1,104 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdtemp, readFile, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
3
|
+
import { lstatSync, realpathSync, renameSync } from 'node:fs';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { homedir, tmpdir } from 'node:os';
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
7
|
import { runCommand } from '@mearl/daemon-core';
|
|
9
8
|
import { satisfies, valid } from 'semver';
|
|
10
|
-
import { DISABLED_SKILL_MANIFEST, findPluginDirectoryEntry, findPluginRegistration, listPluginNames, OFFICIAL_PLUGIN_MANAGER, pluginRegistrationNeedsRefresh, readInstalledSkillMetadata } from '@mearl/client/plugin';
|
|
9
|
+
import { DISABLED_SKILL_MANIFEST, getSkillEnabled, findPluginDirectoryEntry, findPluginRegistration, listPluginNames, OFFICIAL_PLUGIN_MANAGER, pluginRegistrationNeedsRefresh, readInstalledSkillMetadata } from '@mearl/client/plugin';
|
|
11
10
|
import { existsSync } from 'node:fs';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return scope === 'project' ? join(cwd, 'skills-lock.json') : env.XDG_STATE_HOME ? join(env.XDG_STATE_HOME, 'skills', '.skill-lock.json') : join(options.homeDirectory ?? homedir(), '.agents', '.skill-lock.json');
|
|
15
|
-
}
|
|
16
|
-
export function readTrackedSkillSources(scope, cwd, options = {}) {
|
|
17
|
-
const file = lockFile(scope, cwd, options);
|
|
18
|
-
let text;
|
|
19
|
-
try {
|
|
20
|
-
if (statSync(file).size > 4 * 1024 * 1024)
|
|
21
|
-
throw new Error('Skill source lock exceeds 4 MiB');
|
|
22
|
-
text = readFileSync(file, 'utf8');
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
if (error.code === 'ENOENT')
|
|
26
|
-
return [];
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
const lock = JSON.parse(text);
|
|
30
|
-
if (!lock || !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills))
|
|
31
|
-
throw new Error('Invalid Skills source lock');
|
|
32
|
-
return Object.entries(lock.skills).flatMap(([name, entry]) => {
|
|
33
|
-
if (!entry || typeof entry !== 'object' || entry.sourceType === 'node_modules')
|
|
34
|
-
return [];
|
|
35
|
-
if (!name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
36
|
-
throw new Error('Invalid tracked Skill name');
|
|
37
|
-
const source = typeof entry.sourceUrl === 'string' ? entry.sourceUrl : entry.source;
|
|
38
|
-
if (typeof source !== 'string' || !source.trim())
|
|
39
|
-
return [];
|
|
40
|
-
if (source.length > 4096 || /[\0\r\n]/.test(source))
|
|
41
|
-
throw new Error('Invalid tracked Skill source');
|
|
42
|
-
const expectedHash = scope === 'global' ? entry.skillFolderHash : entry.computedHash;
|
|
43
|
-
return [{ name, source, scope, ...(typeof entry.sourceType === 'string' ? { sourceType: entry.sourceType } : {}), ...(typeof entry.ref === 'string' ? { ref: entry.ref } : {}), ...(typeof entry.skillPath === 'string' ? { skillPath: entry.skillPath } : {}), ...(typeof expectedHash === 'string' && expectedHash ? { expectedHash } : {}), hashKind: scope === 'global' && typeof expectedHash === 'string' && /^[a-f0-9]{40}$/.test(expectedHash) ? 'git-tree' : 'folder' }];
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
47
|
-
export function isPublicSkillSource(source) {
|
|
48
|
-
return source.sourceType === undefined || ['github', 'gitlab', 'git', 'local', 'well-known'].includes(source.sourceType);
|
|
49
|
-
}
|
|
50
|
-
function gitSource(source, ref) {
|
|
51
|
-
if (/^[\w.-]+\/[\w.-]+$/.test(source))
|
|
52
|
-
return { repository: `https://github.com/${source}.git`, ...(ref ? { ref } : {}) };
|
|
53
|
-
if (/^[\w.-]+@[\w.-]+:[^\s]+$/.test(source))
|
|
54
|
-
return { repository: source, ...(ref ? { ref } : {}) };
|
|
55
|
-
const url = new URL(source);
|
|
56
|
-
if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) || url.password || (url.username && url.protocol !== 'ssh:') || url.search || url.hash)
|
|
57
|
-
throw new Error('Skill source is not a supported Git repository');
|
|
58
|
-
if (url.hostname === 'github.com' && !url.port) {
|
|
59
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
60
|
-
if (parts.length >= 2)
|
|
61
|
-
return { repository: `https://github.com/${parts[0]}/${parts[1].replace(/\.git$/, '')}.git`, ...(ref || (['tree', 'blob'].includes(parts[2]) && parts[3]) ? { ref: ref || decodeURIComponent(parts[3]) } : {}), ...(['tree', 'blob'].includes(parts[2]) && parts.length > 4 ? { subdirectory: decodeURIComponent(parts.slice(4).join('/')).replace(/\/SKILL\.md$/, '') } : {}) };
|
|
62
|
-
}
|
|
63
|
-
return { repository: url.href, ...(ref ? { ref } : {}) };
|
|
64
|
-
}
|
|
65
|
-
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
66
|
-
export function matchesTrackedSkillSource(requested, tracked, normalize = source => source) {
|
|
67
|
-
const location = (source, ref, sourceType) => {
|
|
68
|
-
const value = normalize(source, sourceType);
|
|
69
|
-
if (typeof value !== 'string' || !value || value.length > 8192 || /[\0\r\n]/.test(value))
|
|
70
|
-
throw new Error('Invalid normalized Skill source');
|
|
71
|
-
const hash = value.indexOf('#');
|
|
72
|
-
const fragment = hash < 0 ? undefined : value.slice(hash + 1);
|
|
73
|
-
const plain = hash < 0 ? value : value.slice(0, hash);
|
|
74
|
-
const parsed = isAbsolute(plain) ? { repository: plain, ref: ref ?? fragment } : gitSource(plain, ref ?? fragment);
|
|
75
|
-
let repository = parsed.repository;
|
|
76
|
-
if (repository.startsWith('file:'))
|
|
77
|
-
repository = fileURLToPath(repository);
|
|
78
|
-
if (isAbsolute(repository))
|
|
79
|
-
repository = resolve(repository);
|
|
80
|
-
else {
|
|
81
|
-
const ssh = repository.match(/^[\w.-]+@([\w.-]+):(.+)$/);
|
|
82
|
-
const url = new URL(ssh ? `ssh://${ssh[1]}/${ssh[2]}` : repository);
|
|
83
|
-
// Authentication transport is independent of the repository's identity.
|
|
84
|
-
repository = `${url.hostname.toLowerCase()}${url.port ? ':' + url.port : ''}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`;
|
|
85
|
-
}
|
|
86
|
-
return { ...parsed, repository, ref: parsed.ref || undefined };
|
|
87
|
-
};
|
|
88
|
-
try {
|
|
89
|
-
const expected = location(requested);
|
|
90
|
-
const actual = location(tracked.source, tracked.ref, tracked.sourceType);
|
|
91
|
-
if (expected.repository !== actual.repository || expected.ref !== actual.ref)
|
|
92
|
-
return false;
|
|
93
|
-
if (!expected.subdirectory)
|
|
94
|
-
return true;
|
|
95
|
-
const path = (tracked.skillPath ?? actual.subdirectory ?? '').replace(/(?:^|\/)SKILL\.md$/, '').replace(/\/+$/, '');
|
|
96
|
-
return path === expected.subdirectory || path.startsWith(expected.subdirectory + '/');
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return normalize(requested) === normalize(tracked.source, tracked.sourceType) && !tracked.ref;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
11
|
+
import { gitSource, isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
12
|
+
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
102
13
|
function skillName(text) {
|
|
103
14
|
return text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1].match(/^name:\s*["']?([^"'\r\n]+?)["']?\s*$/m)?.[1]?.trim();
|
|
104
15
|
}
|
|
@@ -237,6 +148,35 @@ export function readLinkedSkillSource(directory, name) {
|
|
|
237
148
|
return undefined;
|
|
238
149
|
}
|
|
239
150
|
}
|
|
151
|
+
/** Keep the user's disabled state when an installer replaces the Skill directory. */
|
|
152
|
+
export async function preserveSkillDisabled(directory, operation) {
|
|
153
|
+
const disabled = getSkillEnabled(directory) === false;
|
|
154
|
+
try {
|
|
155
|
+
return await operation();
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if (disabled && existsSync(join(directory, 'SKILL.md')))
|
|
159
|
+
renameSync(join(directory, 'SKILL.md'), join(directory, DISABLED_SKILL_MANIFEST));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function canonicalPath(path) {
|
|
163
|
+
const absolute = resolve(path);
|
|
164
|
+
try {
|
|
165
|
+
return realpathSync(absolute);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
const parent = dirname(absolute);
|
|
169
|
+
return parent === absolute ? absolute : join(canonicalPath(parent), basename(absolute));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function hasOnlySetupRegistrations(directory) {
|
|
173
|
+
const contains = (root, entry) => {
|
|
174
|
+
const path = relative(root, entry);
|
|
175
|
+
return path !== '' && path !== '..' && !path.startsWith('..' + sep) && !isAbsolute(path);
|
|
176
|
+
};
|
|
177
|
+
const registrations = listPluginNames().flatMap(name => findPluginRegistration(name) ?? []).filter(registration => contains(resolve(directory), resolve(registration.entry)) || contains(canonicalPath(directory), canonicalPath(registration.entry)));
|
|
178
|
+
return registrations.length > 0 && registrations.every(registration => registration.managedBy === OFFICIAL_PLUGIN_MANAGER);
|
|
179
|
+
}
|
|
240
180
|
/** CLI and Agent readiness use the same source, manifest and registration evidence. */
|
|
241
181
|
export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
242
182
|
if (!source.trim() || !name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
@@ -244,10 +184,17 @@ export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
|
244
184
|
const directory = join(options.homeDirectory ?? homedir(), '.agents', 'skills', name);
|
|
245
185
|
if (existsSync(join(directory, 'SKILL.md')) && existsSync(join(directory, DISABLED_SKILL_MANIFEST)))
|
|
246
186
|
return { state: 'conflict', reason: 'SKILL_MANIFEST_CONFLICT' };
|
|
247
|
-
const
|
|
187
|
+
const receipt = (options.readSources ?? readTrackedSkillSources)('global', undefined, { homeDirectory: options.homeDirectory }).find(entry => entry.name === name);
|
|
188
|
+
const tracked = receipt
|
|
248
189
|
?? (!/(?:#|\/(?:tree|blob)\/)/.test(source) ? readLinkedSkillSource(directory, name) : undefined);
|
|
249
|
-
if (tracked && ((!options.externalManager && !isPublicSkillSource(tracked)) || !matchesTrackedSkillSource(source, tracked, options.normalizeSource)))
|
|
190
|
+
if (tracked && ((!options.externalManager && !isPublicSkillSource(tracked)) || !matchesTrackedSkillSource(source, tracked, options.normalizeSource))) {
|
|
191
|
+
const metadata = readInstalledSkillMetadata(directory);
|
|
192
|
+
if (receipt && (options.externalManager || isPublicSkillSource(receipt)) && metadata?.name === name &&
|
|
193
|
+
options.migrateFrom?.some(previous => matchesTrackedSkillSource(previous, receipt, options.normalizeSource)) && hasOnlySetupRegistrations(directory)) {
|
|
194
|
+
return { state: 'migration', reason: 'SKILL_SOURCE_MIGRATION_REQUIRED' };
|
|
195
|
+
}
|
|
250
196
|
return { state: 'conflict', reason: 'SKILL_SOURCE_CONFLICT' };
|
|
197
|
+
}
|
|
251
198
|
const metadata = readInstalledSkillMetadata(directory);
|
|
252
199
|
if (!metadata)
|
|
253
200
|
return { state: 'missing', reason: 'SKILL_NOT_INSTALLED' };
|
package/dist/skills.js
CHANGED
|
@@ -59,9 +59,11 @@ export async function captureSkillsCli(options = {}) {
|
|
|
59
59
|
const managedSkills = Object.fromEntries(Object.entries(current.preset?.skills ?? {}).filter(([, entry]) => entry.enabled !== false));
|
|
60
60
|
const backend = registry.get('skills.manager')?.();
|
|
61
61
|
if (backend !== undefined) {
|
|
62
|
+
if (backend?.readSources !== undefined && typeof backend.readSources !== 'function')
|
|
63
|
+
throw new Error('Invalid Skill source reader');
|
|
62
64
|
if (!backend || typeof backend.id !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(backend.id) || typeof backend.cli !== 'function')
|
|
63
65
|
throw new Error('The selected Skill manager requires a preset update for CLI operations');
|
|
64
|
-
return { id: backend.id, managedSkills, selection, inspectGlobal: (source, name, version) => inspectGlobalSkill(source, name, version, { externalManager: true, normalizeSource: backend.normalizeSource, readSources }), run: async (args, settings) => {
|
|
66
|
+
return { id: backend.id, managedSkills, selection, inspectGlobal: (source, name, version, inspection) => inspectGlobalSkill(source, name, version, { migrateFrom: inspection?.migrateFrom, externalManager: true, normalizeSource: backend.normalizeSource, readSources: backend.readSources ?? readSources }), run: async (args, settings) => {
|
|
65
67
|
const result = await backend.cli([...args], settings);
|
|
66
68
|
if (!result || !Number.isInteger(result.code) || typeof result.stdout !== 'string' || typeof result.stderr !== 'string')
|
|
67
69
|
throw new Error('Invalid Skill manager CLI result');
|
|
@@ -71,7 +73,7 @@ export async function captureSkillsCli(options = {}) {
|
|
|
71
73
|
const invocation = resolveSkillsInvocation(options);
|
|
72
74
|
if (!invocation)
|
|
73
75
|
return undefined;
|
|
74
|
-
return { id: 'skills', managedSkills, selection, inspectGlobal: (source, name, version) => inspectGlobalSkill(source, name, version, { readSources }), run: (args, settings) => runner(invocation.command, [...invocation.argsPrefix, ...args], { ...settings, env: invocation.env }) };
|
|
76
|
+
return { id: 'skills', managedSkills, selection, inspectGlobal: (source, name, version, inspection) => inspectGlobalSkill(source, name, version, { migrateFrom: inspection?.migrateFrom, readSources }), run: (args, settings) => runner(invocation.command, [...invocation.argsPrefix, ...args], { ...settings, env: invocation.env }) };
|
|
75
77
|
}
|
|
76
78
|
export function parseInstalledSkills(output) {
|
|
77
79
|
const value = JSON.parse(stripAnsi(output).trim());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/setup",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.3",
|
|
4
4
|
"description": "One-shot environment setup and configuration for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"./preset": {
|
|
46
46
|
"types": "./dist/preset.d.ts",
|
|
47
47
|
"import": "./dist/preset.js"
|
|
48
|
+
},
|
|
49
|
+
"./skill-source-receipts": {
|
|
50
|
+
"types": "./dist/skillSourceReceipts.d.ts",
|
|
51
|
+
"import": "./dist/skillSourceReceipts.js"
|
|
48
52
|
}
|
|
49
53
|
},
|
|
50
54
|
"bin": {
|
|
@@ -74,11 +78,11 @@
|
|
|
74
78
|
"extract-zip": "^2.0.1",
|
|
75
79
|
"ws": "^8.18.3",
|
|
76
80
|
"semver": "^7.8.5",
|
|
77
|
-
"@mearl/
|
|
78
|
-
"@mearl/
|
|
79
|
-
"@mearl/
|
|
80
|
-
"@mearl/
|
|
81
|
-
"@mearl/
|
|
81
|
+
"@mearl/daemon-core": "3.0.0-beta.3",
|
|
82
|
+
"@mearl/provider": "3.0.0-beta.3",
|
|
83
|
+
"@mearl/browser-core": "3.0.0-beta.3",
|
|
84
|
+
"@mearl/client": "3.0.0-beta.3",
|
|
85
|
+
"@mearl/preset": "3.0.0-beta.3"
|
|
82
86
|
},
|
|
83
87
|
"devDependencies": {
|
|
84
88
|
"@types/node": "^24.9.1",
|