@mearl/setup 3.0.0-beta.4 → 3.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,31 +4,32 @@
4
4
 
5
5
  Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全局安装 setup:
6
6
 
7
+ 要求 Node.js >= 20.18.0。安装前检查 `node --version`;3.0 beta 继续显式使用 `@beta`,
8
+ 避免 npm 在未指定版本时按旧 Node 的兼容性选到历史安装器。
9
+
7
10
  ```bash
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>
11
+ npx @mearl/setup@beta [install] [--local] [--cloud] [--cloud-mode <id>] [--mcp]
12
+ npx @mearl/setup@beta update [--local] [--cloud] [--cloud-mode <id>] [--mcp]
13
+ npx @mearl/setup@beta uninstall [--local] [--cloud] [--cloud-mode <id>] [--mcp] [--full]
14
+ npx @mearl/setup@beta extension [--full] [--force]
15
+ npx @mearl/setup@beta plugin [name] <path>
16
+ npx @mearl/setup@beta plugin install <skills add 参数...>
17
+ npx @mearl/setup@beta plugin uninstall <name>
18
+ npx @mearl/setup@beta provider <install|update|uninstall> <package|id>
19
+ npx @mearl/setup@beta preset repair [--replace-skill <name> ...]
16
20
  ```
17
21
 
18
22
  浏览器 Provider 独立安装和演进。安装后运行 `mearl <providerId>` 查看该版本声明的能力、
19
23
  配置和启动参数。
20
24
 
21
- 在 Qoder Cloud Agent 环境中,使用 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --qoder-cloud --yes` 选择 Qoder
22
- 配对模式。它与 `--cloud` 安装同一套 cloud-server、client 和 Skill,只把 cloud-server
23
- 模式记录为 Qoder;`mearl-cloud-server` 的命令交互保持不变,`start` 和 `restart` 输出新的
24
- 配对指令后退出,不监听端口或保留守护进程。配对 URL 不包含 Qoder PAT。
25
+ 可选云协议先通过 preset 安装对应能力,再用 `--cloud-mode <id>` 选择。Qoder 配对见 [`@mearl/plugin-qoder`](../plugin-qoder/README.md);setup 不默认包含或安装它。
25
26
 
26
27
  ## 安装与注册 Skill 插件
27
28
 
28
29
  一个命令安装 skill 并注册它的 CLI、SDK 和 MCP 能力:
29
30
 
30
31
  ```bash
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
+ npx @mearl/setup@beta plugin install https://github.com/mearlhq/mearl/tree/codex/open-source-ready --skill browser-tab-manager --yes
32
33
  mearl tabs summary
33
34
  ```
34
35
 
@@ -38,6 +39,14 @@ mearl tabs summary
38
39
 
39
40
  Native preset 批次新建的插件注册使用 setup 托管归属,后续通过 Agent 更新仍保留该归属;同目录已有的用户注册保持原归属,自定义用户别名也会保留。托管卸载遇到用户别名共用同一 Skill 时,只清除托管注册并保留 Skill 文件。
40
41
 
42
+ GUI 和 CLI 使用同一套 preset 安装器及覆盖规则。已保存的安装尚未完成时,运行 `preset repair` 从接受的本地资源重试,不重新下载 preset,也不隐式覆盖冲突来源。显式重复指定 `--replace-skill` 即授权本次替换这些 Skill 的来源或同名旧插件注册:
43
+
44
+ ```bash
45
+ npx @mearl/setup@beta preset repair --replace-skill example-skill --replace-skill another-skill
46
+ ```
47
+
48
+ 已安装的 Skill 仅修复注册;来源冲突项重新安装。未列出的冲突仍会阻止安装,声明外的工具名与变更后的安装身份也会被拒绝。覆盖授权不会保留到后续普通修复或自动更新。缺少浏览器权限时仍需在扩展中授权。输出沿用 preset JSON,已就绪退出 `0`、仍有待完成项退出 `3`、安装失败退出 `1`,无效参数退出 `2`。
49
+
41
50
  自动注册默认使用安装目录名;入口可声明 `name` 指定简短命令名。
42
51
  重装会刷新同名绑定。JavaScript 入口必须默认导出 `register(app)`,Python 入口必须导出 `register(app)` 函数;完整协议见
43
52
  [插件运行时合同](../../docs/plugin-runtime.md)。
@@ -45,8 +54,8 @@ Native preset 批次新建的插件注册使用 setup 托管归属,后续通
45
54
  已通过其他渠道分发的 skill 或自定义入口,可以直接传入路径注册:
46
55
 
47
56
  ```bash
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
57
+ npx @mearl/setup@beta plugin /absolute/path/to/browser-tab-manager
58
+ npx @mearl/setup@beta plugin custom ./skills/custom/entry.mjs
50
59
  ```
51
60
 
52
61
  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 +68,7 @@ native-host 会幂等初始化 Skills 模式。基础环境补齐或初始化失
59
68
  不再需要插件时执行:
60
69
 
61
70
  ```bash
62
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta plugin uninstall tabs
71
+ npx @mearl/setup@beta plugin uninstall tabs
63
72
  ```
64
73
 
65
74
  若入口位于 `~/.agents/skills/<skill>/`,setup 会先执行
@@ -75,9 +84,9 @@ Provider 由 npm 包分发,但包本身无需暴露 CLI 或安装脚本。setu
75
84
  可创建的浏览器类型名称及其表单参数:
76
85
 
77
86
  ```bash
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
87
+ npx @mearl/setup@beta provider install @example/mearl-provider-device
88
+ npx @mearl/setup@beta provider update example-device
89
+ npx @mearl/setup@beta provider uninstall example-device
81
90
  ```
82
91
 
83
92
  安装参数接受包名、版本或 dist-tag;更新和卸载可使用 providerId 或完整包名。核心 @mearl 包固定使用公共 npm registry;其余 scope 沿用调用方的包管理器配置,组织 registry 由能力安装引导提供。安装沿用启动 setup 的包管理器,
@@ -88,21 +97,20 @@ npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider unin
88
97
  ## 行为
89
98
 
90
99
  - Mearl 自有 npm 包的安装和更新固定使用 `https://registry.npmjs.org/`,与最新版本查询使用同一来源;第三方包沿用当前包管理器的源配置。
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`。
100
+ - `install` 可以省略;直接执行 `npx @mearl/setup@beta` 等同于 `npx @mearl/setup@beta install`。
101
+ 以选项开头时也默认安装,例如 `npx @mearl/setup@beta --cloud --yes`。
93
102
  - `install` 未指定环境时默认 `--local`。
94
103
  - `-h` / `--help` 显示帮助,`-v` / `--version` 显示 setup 版本。
95
104
  - `update` 和 `uninstall` 未指定环境时,从当前 `PATH` 中的
96
105
  `mearl-native-host`、`mearl-cloud-server`、`mearl-mcp-server` 自动检测,可同时处理
97
106
  多个已安装环境。
98
107
  - `--local`、`--cloud` 与 `--mcp` 可以组合,显式传入时只处理选中的环境;
99
- `--qoder-cloud` 选择相同的 cloud 环境并记录 Qoder 配对模式,不能与 `--cloud` 同时使用。
108
+ `--cloud-mode <id>` 同时选择 cloud 环境,模式实现必须来自已安装能力。
100
109
  - 已有环境沿用其原包管理器和全局安装前缀;首次安装沿用启动 setup 的包管理器。
101
110
  - `uninstall` 也识别 `npm link` 或 `dev:link` 注册到 npm 全局目录的开发软链,
102
111
  通过原 npm 前缀移除包与命令链接,保留仓库源码。安装和更新的自动检测仅处理常规全局安装。
103
112
  - cloud 环境统一管理 `@mearl/cloud-server` 与 `@mearl/client`:WebSocket 模式首次安装后
104
- 启动 Server,更新后仅在 Server 原本运行时重启;Qoder 模式由相同的 `start` 命令生成
105
- 新配对但不起服务;卸载前均调用 `stop`。
113
+ 启动 Server,更新后仅在 Server 原本运行时重启;其他 mode 由安装的 `cloud.server.<id>` 处理;卸载前均调用 `stop`。
106
114
  - local/cloud 共用一份 `@mearl/client`;组合安装只处理一次,卸载单个环境时若另一个环境仍在使用则保留。
107
115
  - 从 2.5.x 更新 cloud 环境时会先卸载旧 `@mearl/cloud-client`,避免它遗留的 `mearl` bin 与统一 client 冲突。
108
116
  - local/cloud 共用的全局 mearl skill 通过 `skills` 的 JSON 清单定位,只处理一次。
@@ -121,7 +129,7 @@ npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider unin
121
129
  CDP 标签 ID 计数也保留,防止旧请求命中新标签。
122
130
  - `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
123
131
  - 默认的 `install` / `update` 不处理浏览器扩展。`extension` 是独立命令:从
124
- `https://github.com/F-loat/mearl/releases/download/v3.0.0-beta.1/` 读取同版本 beta 扩展发行元数据(正式 setup 使用 `releases/latest/download/`)并下载对应的 `mearl-chrome-v*.zip`,原子解压到
132
+ `https://github.com/mearlhq/mearl/releases/download/v<version>/` 读取同版本 beta 扩展发行元数据(正式 setup 使用 `releases/latest/download/`)并下载对应的 `mearl-chrome-v*.zip`,原子解压到
125
133
  `~/.mearl/chrome-extension`。首次安装通过浏览器级 CDP 的 `Extensions.loadUnpacked`
126
134
  加载;后续更新由 `mearl check --local --json` 确认当前扩展仍来自 setup 管理目录后,直接替换
127
135
  文件并通过 Native Host 触发扩展 reload。固定目录和 manifest 中的 `key` 保证扩展 ID
@@ -137,19 +145,19 @@ npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider unin
137
145
  ## 安装或更新浏览器扩展
138
146
 
139
147
  ```bash
140
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension
148
+ npx @mearl/setup@beta extension
141
149
  ```
142
150
 
143
151
  首次使用时可一次安装完整的本地浏览器环境:
144
152
 
145
153
  ```bash
146
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension --full
154
+ npx @mearl/setup@beta extension --full
147
155
  ```
148
156
 
149
157
  首次安装、旧版 setup 扩展首次迁移,或 Native Host reload 不可用时才需要 CDP。如果
150
158
  Chrome 尚未开放 CDP,交互命令会提示打开
151
159
  `chrome://inspect/#remote-debugging`、启用远程调试并允许连接,然后等待安装完成。面向人的
152
- 安装指引不要添加 `--yes`。无 TTY 环境使用 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension --yes`;它只尝试
160
+ 安装指引不要添加 `--yes`。无 TTY 环境使用 `npx @mearl/setup@beta extension --yes`;它只尝试
153
161
  一次,CDP 不可用时立即中断。无法确认 setup 管理来源且 CDP 不可用时不会下载或修改插件
154
162
  目录,打开 CDP 后重新执行同一命令即可。Chrome 版本过低、缺少扩展安装所需的 CDP 方法时,
155
163
  命令会中断并提示升级 Chrome。
@@ -179,7 +187,7 @@ DevTools 面板的设置按钮会显示红点,快捷设置中提供“立即
179
187
 
180
188
  自动更新不会降级较新的 Host,也不会改动 cloud、MCP 或独立安装的 Mearl Skill。
181
189
  目标 npm 版本尚未发布、网络不可用或更新失败时,旧 Host 会继续运行;下一次扩展后台
182
- 生命周期可重新尝试,也可手动执行 `npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta update --local`。
190
+ 生命周期可重新尝试,也可手动执行 `npx @mearl/setup@beta update --local`。
183
191
 
184
192
  ## 人工与 Agent 使用
185
193
 
@@ -194,41 +202,41 @@ DevTools 面板的设置按钮会显示红点,快捷设置中提供“立即
194
202
 
195
203
  ```bash
196
204
  # 默认安装本地浏览器环境
197
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup
205
+ npx @mearl/setup
198
206
 
199
207
  # 与上一条等价
200
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta install
208
+ npx @mearl/setup@beta install
201
209
 
202
210
  # Agent / CI 无交互安装
203
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --yes
211
+ npx @mearl/setup@beta --yes
204
212
 
205
213
  # 安装云端 Agent 环境
206
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --cloud
214
+ npx @mearl/setup@beta --cloud
207
215
 
208
216
  # 同一台机器同时作为本地浏览器宿主和云端 Agent
209
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta --local --cloud
217
+ npx @mearl/setup@beta --local --cloud
210
218
 
211
219
  # 自动更新当前 PATH 中检测到的全部 Mearl 环境
212
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta update
220
+ npx @mearl/setup@beta update
213
221
 
214
222
  # 独立安装或更新到最新浏览器扩展
215
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension
223
+ npx @mearl/setup@beta extension
216
224
 
217
225
  # 安装完整本地环境(client、Native Host、Skill 和扩展)
218
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta extension --full
226
+ npx @mearl/setup@beta extension --full
219
227
 
220
228
  # 只卸载本地环境
221
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall --local
229
+ npx @mearl/setup@beta uninstall --local
222
230
 
223
231
  # 自动检测并无交互卸载
224
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall --yes
232
+ npx @mearl/setup@beta uninstall --yes
225
233
 
226
234
  # 卸载环境,并清理全部 CLI 插件与 Provider
227
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta uninstall --full
235
+ npx @mearl/setup@beta uninstall --full
228
236
 
229
237
  # 安装可选浏览器 Provider
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
238
+ npx @mearl/setup@beta provider install @example/mearl-provider-device
239
+ npx @mearl/setup@beta provider install @example/mearl-provider-cloud
232
240
  ```
233
241
 
234
242
  原有 Mearl 环境包的 npm/npx 安装方式和 CLI 保持可用;setup 是统一的便利入口。
@@ -237,6 +245,12 @@ npx --@mearl:registry=https://registry.npmjs.org @mearl/setup@beta provider inst
237
245
 
238
246
  `mearl-setup preset apply <source-url-or-path>` 准备确切资源、激活本机 Node 选择,并向兼容扩展同步同一批浏览器资源;`preset update` 保留用户覆盖,`preset status` 只读检查实际就绪状态。退出码 3 表示存在待同步/安装组件。完整扩展、默认设置、Skills/Provider 和网页一键安装使用同一实际状态核验,见 [本机安装合同](../../docs/preset-installation.md)。
239
247
 
240
- 普通 install/update 在替换包前保存选中 preset 的确切资源和期望选择,运行时初始化后再同步宿主和安装 Skills;`extension --full` 也在替换扩展前完成准备。已有本机 preset 优先保留,显式 `--preset` 可更新或修复资源;检测到已安装包为 3.0 以前版本且尚无选择时,读取公共部署目录的 `legacyPreset`。配置或资源不可用会在包变更前停止,不回退到无内部能力的安装。部分升级后的重试使用已保存安装记录,即使包版本已变为 3.x 也不重新下载。维护旧版本线和卸载不自动迁移。
248
+ 普通 install/update 在替换包前保存选中 preset 的确切资源和期望选择,运行时初始化后再同步宿主和安装 Skills;`extension --full` 也在替换扩展前完成准备。已有本机 preset 优先保留,显式 `--preset` 可更新或修复资源。配置或资源不可用会在包变更前停止,不回退到无内部能力的安装。部分升级后的重试使用已保存安装记录,即使包版本已变为 3.x 也不重新下载。卸载不准备或安装 preset。
241
249
 
242
250
  默认目录为公共站点的 `installation.json`,可通过 `MEARL_INSTALLATION_CONFIG_URL` 指定其他 HTTPS 目录(本地验证允许回环 HTTP)。核心不包含组织 ID 或私有来源;目录沿用网页安装的校验合同。本机与浏览器未全部确认时仍报告 pending,准备成功不表示已发行或已完成真实企业授权验收。
251
+
252
+ ## 浏览器 preset 与发行入口
253
+
254
+ 主动执行 setup 时,先沿用本机已选 preset;本机未选择时,在运行时初始化后、安装 Skill 前采用已连接浏览器的受信 preset。显式 `--preset <source>` 优先。多个浏览器的选择不同、来源不受信或状态读取失败时要求显式选择。首次连接最多等待约一分钟;浏览器关闭时只安装基础环境并说明重试方式。普通连接和后台核心更新不会自动安装首次本机 preset。
255
+
256
+ 发行包可设置本机进程环境变量 `MEARL_SETUP_DEFAULT_PRESET` 提供默认清单来源;默认值仅在两端均无选择时生效,插件、Provider、卸载等子命令保持原行为。安装与状态核验仍由公共安装器负责。
@@ -6,15 +6,15 @@ import path from 'node:path';
6
6
  import { createHash, randomUUID } from 'node:crypto';
7
7
  import { runUnifiedCheck } from '@mearl/client/check';
8
8
  import { CLIENT_VERSION, invoke as invokeLocalMearl } from '@mearl/client/local';
9
- import extractZip from 'extract-zip';
9
+ import AdmZip from 'adm-zip';
10
10
  import WebSocket from 'ws';
11
11
  import { compareMearlReleaseVersions, isMearlReleaseVersion } from './version.js';
12
12
  export const MEARL_EXTENSION_DOWNLOAD_PAGE = SETUP_VERSION.includes('-beta.')
13
- ? `https://github.com/F-loat/mearl/releases/tag/v${SETUP_VERSION}`
14
- : 'https://github.com/F-loat/mearl/releases/latest';
13
+ ? `https://github.com/mearlhq/mearl/releases/tag/v${SETUP_VERSION}`
14
+ : 'https://github.com/mearlhq/mearl/releases/latest';
15
15
  export const MEARL_EXTENSION_REMOTE_CONFIG_URL = SETUP_VERSION.includes('-beta.')
16
- ? `https://github.com/F-loat/mearl/releases/download/v${SETUP_VERSION}/config.json`
17
- : 'https://github.com/F-loat/mearl/releases/latest/download/config.json';
16
+ ? `https://github.com/mearlhq/mearl/releases/download/v${SETUP_VERSION}/config.json`
17
+ : 'https://github.com/mearlhq/mearl/releases/latest/download/config.json';
18
18
  export const MEARL_EXTENSION_DIRECTORY_NAME = 'chrome-extension';
19
19
  export const MEARL_EXTENSION_ID = 'aoehhjnofngknnjefamjbplchbolghkm';
20
20
  export const MEARL_EXTENSION_SETUP_MARKER = '.mearl-setup.json';
@@ -52,7 +52,7 @@ export function extensionArchiveUrl(version) {
52
52
  if (!isMearlReleaseVersion(version)) {
53
53
  throw new Error(`Invalid Mearl browser extension version: ${version}`);
54
54
  }
55
- return `https://github.com/F-loat/mearl/releases/download/v${version}/mearl-chrome-v${version}.zip`;
55
+ return `https://github.com/mearlhq/mearl/releases/download/v${version}/mearl-chrome-v${version}.zip`;
56
56
  }
57
57
  function validateExtensionRelease(release) {
58
58
  if (!isMearlReleaseVersion(release.version)) {
@@ -414,6 +414,22 @@ function readExtensionManifest(directory) {
414
414
  }
415
415
  return parsed;
416
416
  }
417
+ async function extractExtensionArchive(archivePath, destination) {
418
+ const archive = new AdmZip(archivePath);
419
+ // Extension releases contain ordinary files and directories, never symlinks.
420
+ // Validate every entry before writing anything into the fresh staging directory.
421
+ for (const entry of archive.getEntries()) {
422
+ const name = entry.entryName;
423
+ const type = (entry.header.attr >>> 16) & 0o170000;
424
+ if (name.includes('\\') || name.includes('\0') ||
425
+ path.posix.isAbsolute(name) || path.win32.isAbsolute(name) ||
426
+ name.split('/').some(part => part === '..' || part === '.') ||
427
+ (type !== 0 && type !== 0o100000 && type !== 0o040000)) {
428
+ throw new Error('Extension archive contains an unsafe entry');
429
+ }
430
+ }
431
+ await archive.extractAllToAsync(destination, false);
432
+ }
417
433
  async function downloadAndReplaceExtension(releaseInput, targetDirectory, fetchImpl, extractArchive) {
418
434
  const release = validateExtensionRelease(releaseInput);
419
435
  const parentDirectory = path.dirname(targetDirectory);
@@ -462,7 +478,7 @@ export async function updateSetupManagedBrowserExtensionFiles(options, dependenc
462
478
  }
463
479
  const fetchImpl = dependencies.fetch ?? fetch;
464
480
  const extractArchive = dependencies.extractArchive ??
465
- ((archivePath, destination) => extractZip(archivePath, { dir: destination }));
481
+ extractExtensionArchive;
466
482
  const release = await downloadAndReplaceExtension(options.release, targetDirectory, fetchImpl, extractArchive);
467
483
  return { ...release, path: targetDirectory };
468
484
  }
@@ -470,7 +486,7 @@ export async function installMearlBrowserExtension(options = {}, dependencies =
470
486
  const connectCdp = dependencies.connectCdp ?? connectBrowserExtensionViaCdp;
471
487
  const fetchImpl = dependencies.fetch ?? fetch;
472
488
  const extractArchive = dependencies.extractArchive ??
473
- ((archivePath, destination) => extractZip(archivePath, { dir: destination }));
489
+ extractExtensionArchive;
474
490
  const loadUnpacked = dependencies.loadUnpacked ?? loadUnpackedViaCdp;
475
491
  const inspectInstallation = dependencies.inspectInstallation ?? inspectMearlExtensionViaCdp;
476
492
  const inspectInstalled = dependencies.inspectInstalled ?? inspectSetupManagedExtension;
@@ -0,0 +1,20 @@
1
+ import { invokeLocalHost, listLiveBrowsers } from '@mearl/client/local';
2
+ import { loadInstallationConfig } from '@mearl/preset/installation';
3
+ import { createNodePresetLoader } from './preset.js';
4
+ import type { StagedPresetInstallation } from './preset.js';
5
+ interface Options {
6
+ env?: NodeJS.ProcessEnv;
7
+ waitMs?: number;
8
+ writeLine?: (line: string) => void;
9
+ }
10
+ interface Dependencies {
11
+ browsers?: typeof listLiveBrowsers;
12
+ invoke?: typeof invokeLocalHost;
13
+ configuration?: () => ReturnType<typeof loadInstallationConfig>;
14
+ load?: ReturnType<typeof createNodePresetLoader>;
15
+ now?: () => number;
16
+ sleep?: (ms: number) => Promise<void>;
17
+ }
18
+ /** Explicit setup may complete a trusted browser selection before installing any Skills. */
19
+ export declare function prepareBrowserSetupPreset(options?: Options, dependencies?: Dependencies): Promise<StagedPresetInstallation | undefined>;
20
+ export {};
@@ -0,0 +1,82 @@
1
+ import { invokeLocalHost, listLiveBrowsers } from '@mearl/client/local';
2
+ import { installationOriginCovered, loadInstallationConfig, matchesInstallationOrigin, trustedInstallationPreset } from '@mearl/preset/installation';
3
+ import { createNodePresetLoader, PresetInstaller, resolveInstallationConfigUrl } from './preset.js';
4
+ /** Explicit setup may complete a trusted browser selection before installing any Skills. */
5
+ export async function prepareBrowserSetupPreset(options = {}, dependencies = {}) {
6
+ const env = options.env ?? process.env;
7
+ const directory = env.MEARL_CAPABILITY_DIR;
8
+ const current = await new PresetInstaller({ directory }).store.current();
9
+ if (current.activation || current.receipt)
10
+ return;
11
+ const now = dependencies.now ?? Date.now;
12
+ const deadline = now() + (options.waitMs ?? 0);
13
+ const browsers = dependencies.browsers ?? listLiveBrowsers;
14
+ const invoke = dependencies.invoke ?? invokeLocalHost;
15
+ const sleep = dependencies.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
16
+ const configuration = dependencies.configuration ?? (() => {
17
+ const url = resolveInstallationConfigUrl(env.MEARL_INSTALLATION_CONFIG_URL);
18
+ return loadInstallationConfig(url, url.origin, AbortSignal.timeout(5_000));
19
+ });
20
+ const read = async (timeoutSec = 5) => {
21
+ const targets = browsers().filter(browser => !browser.provider && browser.transport === 'extension' && (browser.status === undefined || browser.status === 'connected') && (!env.MEARL_SOCKET_PATH || browser.socketPath === env.MEARL_SOCKET_PATH));
22
+ if (!targets.length)
23
+ return;
24
+ return (await Promise.all(targets.map(async (browser) => {
25
+ let value;
26
+ try {
27
+ value = await invoke('preset_browser_status', {}, { socketPath: browser.socketPath, timeoutSec });
28
+ }
29
+ catch {
30
+ throw new Error(`Cannot read preset from ${browser.browserId}. Reconnect the extension and retry setup, or use --preset <source>.`);
31
+ }
32
+ if (value?.error || typeof value?.browserReady !== 'boolean' || !Object.hasOwn(value, 'browserRevision') || (value.presetId !== undefined && (typeof value.presetId !== 'string' || !value.presetId || typeof value.presetSource !== 'string' || typeof value.browserRevision !== 'string' || value.browserReady !== true)))
33
+ throw new Error(`Browser preset is unavailable in ${browser.browserId}. Reconnect the extension and retry setup, or use --preset <source>.`);
34
+ if (!value.presetId)
35
+ return [];
36
+ return [{ browserId: browser.browserId, presetId: value.presetId, presetSource: value.presetSource, browserRevision: value.browserRevision }];
37
+ }))).flat();
38
+ };
39
+ let selected = await read();
40
+ if (!selected && now() < deadline)
41
+ options.writeLine?.('Waiting for the Mearl extension to connect and checking its installed preset…');
42
+ while (!selected && now() < deadline) {
43
+ await sleep(Math.min(1000, deadline - now()));
44
+ if (now() < deadline)
45
+ selected = await read(Math.max(0.1, Math.min(5, (deadline - now()) / 1000)));
46
+ }
47
+ if (!selected) {
48
+ options.writeLine?.('No connected Mearl extension was found. Continuing without a browser preset; connect and rerun setup to adopt it, or use --preset <source> to select a preset.');
49
+ return;
50
+ }
51
+ if (!selected.length)
52
+ return;
53
+ const accepted = selected;
54
+ const first = accepted[0];
55
+ if (accepted.some(value => value.presetId !== first.presetId || value.presetSource !== first.presetSource))
56
+ throw new Error('Connected browsers use different presets. Select one explicitly with --preset <source>.');
57
+ const trusted = trustedInstallationPreset(await configuration(), first.presetId, first.presetSource);
58
+ if (!trusted)
59
+ throw new Error('The browser preset is not in the trusted installation catalog. Install it explicitly with --preset <source>.');
60
+ const load = dependencies.load ?? createNodePresetLoader();
61
+ const installer = new PresetInstaller({ directory, load: (source, resourceOptions) => {
62
+ const url = new URL(source);
63
+ if (url.protocol !== 'https:' || url.username || url.password || !trusted.origins.some(origin => matchesInstallationOrigin(origin, url)))
64
+ throw new Error('Browser preset resource is outside the trusted installation catalog');
65
+ return load(source, resourceOptions);
66
+ } });
67
+ options.writeLine?.(`Using the browser's trusted preset: ${first.presetId}`);
68
+ return installer.stage(first.presetSource, {
69
+ expectedId: first.presetId, expectedRevision: null, signal: AbortSignal.timeout(120_000),
70
+ validatePrepared: async (prepared) => {
71
+ if (prepared.preset.permissions.origins.some(origin => !installationOriginCovered(origin, trusted.origins)))
72
+ throw new Error('Browser preset permissions exceed the trusted installation catalog');
73
+ },
74
+ confirm: async () => {
75
+ if (JSON.stringify(trustedInstallationPreset(await configuration(), first.presetId, first.presetSource)) !== JSON.stringify(trusted))
76
+ throw new Error('Browser preset trust changed during setup');
77
+ const latest = await read();
78
+ if (!latest || latest.length !== accepted.length || accepted.some(value => !latest.some(other => value.browserId === other.browserId && value.presetId === other.presetId && value.presetSource === other.presetSource && value.browserRevision === other.browserRevision)))
79
+ throw new Error('The browser preset changed during setup. Retry or use --preset <source>.');
80
+ },
81
+ });
82
+ }
package/dist/cli.d.ts CHANGED
@@ -5,16 +5,17 @@ import type { MearlProfile } from './profiles.js';
5
5
  import { ensureOfficialPlugin, installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
6
6
  import { installProviderPackage, uninstallProviderPackage, updateProviderPackage } from './provider.js';
7
7
  import { runPresetCommand } from './preset.js';
8
- import { prepareSetupPreset } from './presetUpgrade.js';
8
+ import { prepareSetupPreset } from './setupPreset.js';
9
9
  export { SETUP_VERSION } from './setupVersion.js';
10
10
  interface ParsedArgs {
11
- cloudMode?: 'websocket' | 'qoder';
11
+ cloudMode?: string;
12
12
  force?: boolean;
13
13
  full?: boolean;
14
14
  help: boolean;
15
15
  operation?: MearlLifecycleOperation | 'extension' | 'plugin' | 'provider' | 'preset';
16
- presetCommand?: 'apply' | 'update' | 'status';
16
+ presetCommand?: 'apply' | 'update' | 'status' | 'repair';
17
17
  presetSource?: string;
18
+ presetReplaceSkills?: string[];
18
19
  pluginName?: string;
19
20
  pluginPath?: string;
20
21
  pluginInstallArgs?: string[];
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { isMearlReleaseLine } from './version.js';
6
6
  import { ensureOfficialPlugin, installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
7
7
  import { installProviderPackage, uninstallProviderPackage, updateProviderPackage, } from './provider.js';
8
8
  import { runPresetCommand } from './preset.js';
9
- import { prepareSetupPreset } from './presetUpgrade.js';
9
+ import { prepareSetupPreset } from './setupPreset.js';
10
10
  import { SETUP_VERSION, SETUP_COMMAND } from './setupVersion.js';
11
11
  export { SETUP_VERSION } from './setupVersion.js';
12
12
  const baseParsedArgs = () => ({
@@ -26,11 +26,33 @@ function commanderError(error) {
26
26
  }
27
27
  export function parseSetupArgs(argv) {
28
28
  if (argv[0] === 'preset') {
29
+ if (argv[1] === 'repair') {
30
+ const command = new Command('preset repair')
31
+ .exitOverride()
32
+ .configureOutput({ writeErr: () => { }, writeOut: () => { } })
33
+ .allowExcessArguments(false)
34
+ .option('--replace-skill <name>', 'Replace the named Skill source or conflicting plugin registration', (name, previous) => [...previous, name], []);
35
+ try {
36
+ command.parse(argv.slice(2), { from: 'user' });
37
+ }
38
+ catch (error) {
39
+ if (error instanceof CommanderError) {
40
+ if (error.code === 'commander.helpDisplayed')
41
+ return { ...baseParsedArgs(), help: true };
42
+ throw commanderError(error);
43
+ }
44
+ throw error;
45
+ }
46
+ const names = command.opts().replaceSkill;
47
+ if (names.length > 256 || new Set(names).size !== names.length || names.some(name => !/^[a-z][a-z0-9-]{0,79}$/.test(name)))
48
+ throw new Error('--replace-skill requires unique valid Skill names (up to 256)');
49
+ return { ...baseParsedArgs(), operation: 'preset', presetCommand: 'repair', ...(names.length ? { presetReplaceSkills: names } : {}) };
50
+ }
29
51
  if (argv[1] === 'apply' && argv.length === 3 && argv[2])
30
52
  return { ...baseParsedArgs(), operation: 'preset', presetCommand: 'apply', presetSource: argv[2] };
31
53
  if ((argv[1] === 'update' || argv[1] === 'status') && argv.length === 2)
32
54
  return { ...baseParsedArgs(), operation: 'preset', presetCommand: argv[1] };
33
- throw new Error('preset requires apply <source-url-or-path>, update, or status');
55
+ throw new Error('preset requires apply <source-url-or-path>, update, repair [--replace-skill <name>], or status');
34
56
  }
35
57
  if (argv[0] === 'provider') {
36
58
  const command = argv[1];
@@ -87,7 +109,7 @@ export function parseSetupArgs(argv) {
87
109
  .configureOutput({ writeErr: () => { }, writeOut: () => { } })
88
110
  .option('--local')
89
111
  .option('--cloud')
90
- .option('--qoder-cloud')
112
+ .option('--cloud-mode <id>')
91
113
  .option('--mcp')
92
114
  .option('--target-line <major.minor.x>')
93
115
  .option('--skip-skill')
@@ -98,9 +120,8 @@ export function parseSetupArgs(argv) {
98
120
  command.option('--full');
99
121
  command.action(() => {
100
122
  const options = command.opts();
101
- if (options.cloud && options.qoderCloud) {
102
- throw new Error('--cloud and --qoder-cloud cannot be used together');
103
- }
123
+ if (options.cloudMode !== undefined && !/^[a-z][a-z0-9-]{0,63}$/.test(options.cloudMode))
124
+ throw new Error('--cloud-mode requires a valid installed mode ID');
104
125
  if (options.targetLine && !isMearlReleaseLine(options.targetLine)) {
105
126
  throw new Error('--target-line requires a valid Mearl release line such as 2.3.x');
106
127
  }
@@ -114,15 +135,15 @@ export function parseSetupArgs(argv) {
114
135
  const profiles = [];
115
136
  if (options.local)
116
137
  profiles.push('local');
117
- if (options.cloud || options.qoderCloud)
138
+ if (options.cloud || options.cloudMode)
118
139
  profiles.push('cloud');
119
140
  if (options.mcp)
120
141
  profiles.push('mcp');
121
142
  parsed = {
122
143
  ...baseParsedArgs(),
123
144
  operation,
124
- ...(options.cloud || options.qoderCloud
125
- ? { cloudMode: options.qoderCloud ? 'qoder' : 'websocket' }
145
+ ...(options.cloud || options.cloudMode
146
+ ? { cloudMode: options.cloudMode ?? 'websocket' }
126
147
  : {}),
127
148
  ...(profiles.length > 0 ? { profiles } : {}),
128
149
  ...(options.full ? { full: true } : {}),
@@ -177,7 +198,7 @@ function printHelp() {
177
198
  const output = `Mearl Setup v${SETUP_VERSION}
178
199
 
179
200
  Usage:
180
- npx --@mearl:registry=https://registry.npmjs.org @mearl/setup [command] [options]
201
+ npx @mearl/setup [command] [options]
181
202
 
182
203
  Environment commands:
183
204
  install [options] Install an environment (default command; target: local)
@@ -187,6 +208,7 @@ Environment commands:
187
208
  Integration commands:
188
209
  preset apply <source> Apply a preset, synchronize hosts and install declared components
189
210
  preset update Update the selected preset, preserving user overrides
211
+ preset repair Retry the installed preset using its accepted resources
190
212
  preset status Report actual readiness (exit 3 when synchronization is pending)
191
213
  plugin install <arguments...> Install Skills and register their CLI plugins
192
214
  plugin uninstall <name> Uninstall one CLI plugin
@@ -201,7 +223,7 @@ Browser extension:
201
223
  Environment options:
202
224
  --local Manage @mearl/native-host and @mearl/client
203
225
  --cloud Manage cloud packages in WebSocket server mode
204
- --qoder-cloud Manage cloud packages in Qoder pairing mode
226
+ --cloud-mode <id> Manage cloud packages using an installed mode
205
227
  --mcp Manage @mearl/mcp-server and its configuration
206
228
  --target-line <major.minor.x> Use the latest packages in one release line
207
229
  --preset <source> Apply a preset after installing or updating the environment
@@ -214,6 +236,9 @@ Extension options:
214
236
  --full Install the extension and complete local environment
215
237
  --force Replace an extension managed by another source
216
238
 
239
+ Preset repair options:
240
+ --replace-skill <name> Replace one Skill source or conflicting registration; repeat for more
241
+
217
242
  General options:
218
243
  --yes, -y Accept prompts and detected defaults
219
244
  --non-interactive Alias for --yes, intended for Agents and CI
@@ -221,21 +246,24 @@ General options:
221
246
  --help, -h Show this help
222
247
 
223
248
  Examples:
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));
249
+ npx @mearl/setup
250
+ npx @mearl/setup --cloud
251
+ npx @mearl/setup --local --cloud
252
+ npx @mearl/setup update
253
+ npx @mearl/setup uninstall --full
254
+ npx @mearl/setup plugin uninstall yuque
255
+ npx @mearl/setup provider install @example/mearl-provider-brow
256
+ npx @mearl/setup preset repair --replace-skill example-skill
257
+ npx @mearl/setup extension --full`;
258
+ console.log(output.replaceAll('npx @mearl/setup', SETUP_COMMAND));
233
259
  }
234
260
  async function runSetupLifecycle(options, dependencies) {
235
261
  let preset;
236
262
  const callback = options.operation === 'uninstall' ? undefined : { onPresetStatus: (status) => { preset = status; } };
237
263
  const lifecycle = dependencies.runLifecycle ?? runMearlLifecycle;
238
- const ok = callback ? await lifecycle(options, callback) : await lifecycle(options);
264
+ const defaultPresetSource = process.env.MEARL_SETUP_DEFAULT_PRESET;
265
+ const configured = defaultPresetSource ? { ...options, defaultPresetSource } : options;
266
+ const ok = callback ? await lifecycle(configured, callback) : await lifecycle(configured);
239
267
  if (!ok)
240
268
  return 1;
241
269
  if (!preset) {
@@ -269,7 +297,10 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
269
297
  }
270
298
  if (parsed.operation === 'preset') {
271
299
  try {
272
- const result = await (dependencies.runPreset ?? runPresetCommand)(parsed.presetCommand, parsed.presetSource);
300
+ const run = dependencies.runPreset ?? runPresetCommand;
301
+ const result = parsed.presetReplaceSkills
302
+ ? await run(parsed.presetCommand, parsed.presetSource, { replaceSkills: parsed.presetReplaceSkills })
303
+ : await run(parsed.presetCommand, parsed.presetSource);
273
304
  console.log(JSON.stringify(result, null, 2));
274
305
  return result.ready ? 0 : 3;
275
306
  }
@@ -9,13 +9,16 @@ import { installManagedPlugins, uninstallManagedPlugins } from './managedPlugins
9
9
  import { uninstallPlugin } from './plugin.js';
10
10
  import { uninstallProviderPackage } from './provider.js';
11
11
  import { cleanupUninstalledRuntime } from './runtimeCleanup.js';
12
- import { prepareSetupPreset } from './presetUpgrade.js';
12
+ import { prepareSetupPreset } from './setupPreset.js';
13
+ import { prepareBrowserSetupPreset } from './browserPreset.js';
13
14
  import type { PresetInstallStatus } from './preset.js';
14
15
  export type MearlLifecycleOperation = 'install' | 'update' | 'uninstall';
15
16
  export interface MearlLifecycleOptions {
16
17
  presetSource?: string;
18
+ /** Distribution default used only when neither local nor browser selection exists. */
19
+ defaultPresetSource?: string;
17
20
  operation: MearlLifecycleOperation;
18
- cloudMode?: 'websocket' | 'qoder';
21
+ cloudMode?: string;
19
22
  /** Uninstall every registered CLI plugin and Provider in addition to selected environments. */
20
23
  full?: boolean;
21
24
  /** @deprecated Use profiles when selecting one or more composable environments. */
@@ -27,6 +30,7 @@ export interface MearlLifecycleOptions {
27
30
  env?: NodeJS.ProcessEnv;
28
31
  }
29
32
  export interface MearlLifecycleDependencies {
33
+ prepareBrowserPreset?: typeof prepareBrowserSetupPreset;
30
34
  preparePreset?: typeof prepareSetupPreset;
31
35
  onPresetStatus?: (status: PresetInstallStatus) => void;
32
36
  /** Prepare the selected manager after packages are usable and before Skill operations. */
package/dist/lifecycle.js CHANGED
@@ -16,7 +16,8 @@ import { uninstallPlugin } from './plugin.js';
16
16
  import { uninstallProviderPackage } from './provider.js';
17
17
  import { packageRegistryEnvironment } from './npmRegistry.js';
18
18
  import { cleanupUninstalledRuntime } from './runtimeCleanup.js';
19
- import { prepareSetupPreset } from './presetUpgrade.js';
19
+ import { prepareSetupPreset } from './setupPreset.js';
20
+ import { prepareBrowserSetupPreset } from './browserPreset.js';
20
21
  const LEGACY_CLOUD_CLIENT = '@mearl/cloud-client';
21
22
  const UNIFIED_CLIENT = '@mearl/client';
22
23
  const PROFILE_ORDER = ['local', 'cloud', 'mcp'];
@@ -135,12 +136,12 @@ async function runHook(profile, operation, detected, runner, existsSync, env, wr
135
136
  return false;
136
137
  }
137
138
  const running = /\[CloudServer\]\s+Status:\s+running\b/.test(status.stdout);
138
- const qoderMode = cloudMode === 'qoder' || /\[CloudServer\]\s+Mode:\s+qoder\b/.test(status.stdout);
139
- if (operation === 'update' && !running && !qoderMode) {
139
+ const pluginMode = Boolean(cloudMode && cloudMode !== 'websocket') || /\[CloudServer\]\s+Mode:\s+(?!websocket\b)\S+/.test(status.stdout);
140
+ if (operation === 'update' && !running && !pluginMode) {
140
141
  writeLine('Hook: cloud update skipped because cloud-server is not running.');
141
142
  return true;
142
143
  }
143
- args = [running && !qoderMode ? 'restart' : 'start'];
144
+ args = [running && !pluginMode ? 'restart' : 'start'];
144
145
  }
145
146
  }
146
147
  else {
@@ -236,7 +237,7 @@ export async function runMearlLifecycle(options, dependencies = {}) {
236
237
  writeLine('No installed Mearl environment was detected on the current PATH.');
237
238
  if (options.operation === 'uninstall')
238
239
  return true;
239
- writeLine('Run: npx --@mearl:registry=https://registry.npmjs.org @mearl/setup');
240
+ writeLine('Run: npx @mearl/setup');
240
241
  return false;
241
242
  }
242
243
  if (options.operation === 'update') {
@@ -281,16 +282,13 @@ export async function runMearlLifecycle(options, dependencies = {}) {
281
282
  ? detectPackage(UNIFIED_CLIENT, ['mearl'], packageDetectionOptions)
282
283
  : null;
283
284
  const detectedClient = detectedClientCandidate?.packageName === UNIFIED_CLIENT ? detectedClientCandidate : null;
285
+ const preparesPreset = options.operation !== 'uninstall' && !dependencies.prepareSkills && (!options.targetLine || Number(options.targetLine.split('.')[0]) >= 3);
286
+ const selectsInitialPreset = preparesPreset && !options.skipSkill && !env.MEARL_AUTO_UPDATE_CLIENT_ROOT && !env.MEARL_AUTO_UPDATE_PROFILES;
284
287
  let stagedPreset;
285
- if (options.operation !== 'uninstall' && (options.presetSource || !options.targetLine || Number(options.targetLine.split('.')[0]) >= 3) && !dependencies.prepareSkills) {
288
+ if (preparesPreset || (options.operation !== 'uninstall' && options.presetSource && !dependencies.prepareSkills)) {
286
289
  try {
287
290
  stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({
288
291
  source: options.presetSource, env,
289
- installed: [
290
- ...lifecycleSelected.flatMap(profile => detected.get(profile) ? [detected.get(profile)] : []),
291
- ...(detectedClient ? [detectedClient] : []),
292
- ...(autoUpdateClient && env.MEARL_AUTO_UPDATE_CLIENT_ROOT ? [{ packageName: UNIFIED_CLIENT, packageRoot: env.MEARL_AUTO_UPDATE_CLIENT_ROOT }] : []),
293
- ],
294
292
  });
295
293
  }
296
294
  catch (error) {
@@ -422,6 +420,24 @@ export async function runMearlLifecycle(options, dependencies = {}) {
422
420
  }
423
421
  }
424
422
  }
423
+ if (packagesOk && selectsInitialPreset && !stagedPreset && !options.presetSource && lifecycleSelected.some(profile => profile === 'local' || profile === 'mcp')) {
424
+ try {
425
+ stagedPreset = await (dependencies.prepareBrowserPreset ?? prepareBrowserSetupPreset)({ env, waitMs: 65_000, writeLine });
426
+ }
427
+ catch (error) {
428
+ writeLine(`Browser preset: ${error instanceof Error ? error.message : String(error)}`);
429
+ packagesOk = false;
430
+ }
431
+ }
432
+ if (packagesOk && selectsInitialPreset && !stagedPreset && options.defaultPresetSource) {
433
+ try {
434
+ stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({ fallbackSource: options.defaultPresetSource, env });
435
+ }
436
+ catch (error) {
437
+ writeLine(`Default preset: ${error instanceof Error ? error.message : String(error)}`);
438
+ packagesOk = false;
439
+ }
440
+ }
425
441
  if (packagesOk && options.operation !== 'uninstall' && (stagedPreset || dependencies.prepareSkills)) {
426
442
  try {
427
443
  if (stagedPreset) {
package/dist/preset.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { invoke } from '@mearl/client/local';
2
2
  import { type JsonObject } from '@mearl/preset';
3
- import { type BundleLoader } from '@mearl/preset/bundles';
3
+ import { type BundleLoader, type PreparedPreset } from '@mearl/preset/bundles';
4
4
  import { PresetInstallationStore } from './presetStore.js';
5
+ export declare const MEARL_INSTALLATION_CONFIG_URL: string;
6
+ /** Runtime overrides apply to this local process, never to browser-supplied install requests. */
7
+ export declare function resolveInstallationConfigUrl(value?: string | undefined, allowLoopback?: boolean): URL;
5
8
  export { claimPresetInstallation } from './presetInstallationLease.js';
6
9
  export { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
7
10
  export type { PresetInstallReceipt } from './presetStore.js';
@@ -59,8 +62,9 @@ export interface PresetInstallerOptions {
59
62
  invoke?: typeof invoke;
60
63
  }
61
64
  export interface PresetApplyOptions {
65
+ validatePrepared?: (prepared: PreparedPreset) => Promise<void>;
66
+ skipUnchanged?: boolean;
62
67
  overrides?: JsonObject;
63
- nodeConfig?: Record<string, JsonObject>;
64
68
  signal?: AbortSignal;
65
69
  expectedRevision?: string | null;
66
70
  expectedId?: string;
@@ -87,8 +91,15 @@ export declare class PresetInstaller {
87
91
  /** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
88
92
  stage(source: string, options?: PresetApplyOptions): Promise<StagedPresetInstallation>;
89
93
  apply(source: string, options?: PresetApplyOptions): Promise<PresetInstallStatus>;
90
- update(): Promise<PresetInstallStatus>;
91
- resume(expectedRevision: string): Promise<PresetInstallStatus>;
94
+ update(options?: Pick<PresetApplyOptions, 'validatePrepared' | 'skipUnchanged' | 'signal'>): Promise<PresetInstallStatus>;
95
+ resume(expectedRevision: string, options?: {
96
+ replaceSkills?: readonly string[];
97
+ }): Promise<PresetInstallStatus>;
98
+ repair(options?: {
99
+ replaceSkills?: readonly string[];
100
+ }): Promise<PresetInstallStatus>;
92
101
  configure(value: JsonObject): Promise<PresetInstallStatus>;
93
102
  }
94
- export declare function runPresetCommand(command: 'apply' | 'update' | 'status', source?: string): Promise<PresetInstallStatus>;
103
+ export declare function runPresetCommand(command: 'apply' | 'update' | 'status' | 'repair', source?: string, options?: {
104
+ replaceSkills?: readonly string[];
105
+ }): Promise<PresetInstallStatus>;
package/dist/preset.js CHANGED
@@ -1,11 +1,21 @@
1
1
  import { lstat, open } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import { resolve } from 'node:path';
4
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { invoke, invokeLocalHost, listLiveBrowsers, SOCKET_PATH } from '@mearl/client/local';
6
7
  import { mergePresetDefaults } from '@mearl/preset';
7
8
  import { BundleError, createBundleFetchLoader, preparePreset } from '@mearl/preset/bundles';
8
9
  import { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
10
+ const installationSite = createRequire(import.meta.url)('@mearl/preset/installation-site.json');
11
+ export const MEARL_INSTALLATION_CONFIG_URL = new URL('installation.json', installationSite.url).href;
12
+ /** Runtime overrides apply to this local process, never to browser-supplied install requests. */
13
+ export function resolveInstallationConfigUrl(value = process.env.MEARL_INSTALLATION_CONFIG_URL, allowLoopback = false) {
14
+ const url = new URL(value || MEARL_INSTALLATION_CONFIG_URL);
15
+ if (url.username || url.password || url.hash || (url.protocol !== 'https:' && !(allowLoopback && url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))))
16
+ throw new Error('Invalid installation configuration URL');
17
+ return url;
18
+ }
9
19
  export { claimPresetInstallation } from './presetInstallationLease.js';
10
20
  export { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
11
21
  export { PresetBrowserTransfer } from './presetBrowserTransfer.js';
@@ -102,12 +112,12 @@ export class PresetInstaller {
102
112
  }
103
113
  }));
104
114
  }
105
- async componentStatus(install, receipt) {
115
+ async componentStatus(install, receipt, replaceSkills) {
106
116
  const target = (this.options.hosts?.() ?? localHosts(install))[0];
107
117
  if (!target)
108
118
  return {};
109
119
  try {
110
- const result = await this.invokeHost(install ? 'preset_components_install' : 'preset_components_status', { installationRevision: receipt.revision }, target, install ? 900 : 15, install);
120
+ const result = await this.invokeHost(install ? 'preset_components_install' : 'preset_components_status', { installationRevision: receipt.revision, ...(install && replaceSkills?.length ? { replaceSkills } : {}) }, target, install ? 900 : 15, install);
111
121
  if (result?.installationRevision !== receipt.revision || result?.nodeRevision !== receipt.nodeRevision || !Array.isArray(result?.components))
112
122
  throw new Error('Component installation host requires an update');
113
123
  if (result.catalogReady !== undefined && typeof result.catalogReady !== 'boolean')
@@ -141,7 +151,7 @@ export class PresetInstaller {
141
151
  result.nodeRevision = receipt.nodeRevision;
142
152
  result.browserRevision = receipt.browserRevision;
143
153
  result.browserSyncRequired = receipt.browserSyncRequired === true;
144
- result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.runtimes?.includes('node')).length
154
+ result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.targets?.includes('node')).length
145
155
  + [...Object.values(preset.skills), ...Object.values(preset.providers)].filter(entry => entry.enabled !== false).length;
146
156
  result.hosts = await this.hostStatus(false, receipt.nodeRevision, receipt.revision);
147
157
  const nodeReady = result.hosts.length > 0 && result.hosts.every(host => host.state === 'active');
@@ -151,12 +161,12 @@ export class PresetInstaller {
151
161
  for (const [id, entry] of Object.entries(preset.packages)) {
152
162
  if (entry.enabled === false)
153
163
  continue;
154
- for (const runtime of entry.runtimes ?? []) {
164
+ for (const runtime of entry.targets ?? []) {
155
165
  if (runtime === 'browser') {
156
166
  if (!browserReady)
157
167
  result.pending.push({ kind: 'packages', id, runtime, reason: 'BROWSER_SYNC_REQUIRED' });
158
168
  }
159
- else if (!receipt.managedModules.includes(id))
169
+ else if (!receipt.managedModules.includes(id) && Object.keys(current.prepared.bundles.find(bundle => bundle.manifest.id === id)?.manifest.contributions?.nodeRuntimes ?? {}).length)
160
170
  result.pending.push({ kind: 'packages', id, runtime, reason: 'NODE_HOST_NOT_SUPPORTED' });
161
171
  else if (!nodeReady)
162
172
  result.pending.push({ kind: 'packages', id, runtime, reason: 'HOST_SYNC_REQUIRED' });
@@ -186,7 +196,7 @@ export class PresetInstaller {
186
196
  result.ready = result.pending.length === 0;
187
197
  return result;
188
198
  }
189
- async synchronize(receipt, previousRevision) {
199
+ async synchronize(receipt, previousRevision, replaceSkills) {
190
200
  const hosts = await this.hostStatus(true, receipt.nodeRevision, receipt.revision);
191
201
  const rejection = hosts.find(host => host.state === 'rejected');
192
202
  if (rejection) {
@@ -207,7 +217,7 @@ export class PresetInstaller {
207
217
  if (installed.receipt?.revision !== receipt.revision || !installed.preset)
208
218
  throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before component installation');
209
219
  const hasExternal = [...Object.values(installed.preset.skills), ...Object.values(installed.preset.providers)].some(entry => entry.enabled !== false);
210
- const components = hasExternal || installed.activation?.bindings['plugins.catalog'] ? await this.componentStatus(true, receipt) : {};
220
+ const components = hasExternal || installed.activation?.bindings['plugins.catalog'] ? await this.componentStatus(true, receipt, replaceSkills) : {};
211
221
  const status = await this.status();
212
222
  if (components.error)
213
223
  status.warnings.push(`Component installation failed; completed assets retained for retry: ${components.error}`);
@@ -225,6 +235,20 @@ export class PresetInstaller {
225
235
  const prepared = await preparePreset(sourceUrl(source), { mearlApiVersion: this.store.mearlApiVersion, load: this.options.load ?? createNodePresetLoader(), signal: options.signal });
226
236
  if (options.expectedId !== undefined && prepared.preset.id !== options.expectedId)
227
237
  throw new BundleError('BUNDLE_INVALID', 'The preset identity differs from the accepted installation');
238
+ await options.validatePrepared?.(prepared);
239
+ if (options.skipUnchanged) {
240
+ const current = await this.store.current();
241
+ if (presetSelectionRevision(current) !== expectedRevision)
242
+ throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during update');
243
+ if (current.receipt && current.prepared?.snapshot.revision === prepared.snapshot.revision) {
244
+ return { revision: current.receipt.revision, activate: async () => {
245
+ const status = await this.status();
246
+ if (status.revision !== expectedRevision)
247
+ throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during update');
248
+ return status.ready ? status : this.resume(current.receipt.revision);
249
+ } };
250
+ }
251
+ }
228
252
  const receipt = await this.store.apply(prepared, { ...options, expectedRevision });
229
253
  let activated = false;
230
254
  return { revision: receipt.revision, activate: async () => {
@@ -239,7 +263,7 @@ export class PresetInstaller {
239
263
  }
240
264
  /** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
241
265
  async stage(source, options = {}) {
242
- const accepted = { ...options, ...(options.overrides === undefined ? {} : { overrides: mergePresetDefaults({}, options.overrides) }), ...(options.nodeConfig === undefined ? {} : { nodeConfig: mergePresetDefaults({}, options.nodeConfig) }) };
266
+ const accepted = { ...options, ...(options.overrides === undefined ? {} : { overrides: mergePresetDefaults({}, options.overrides) }) };
243
267
  const current = await this.store.current({ verifyResources: false });
244
268
  const revision = presetSelectionRevision(current);
245
269
  if (accepted.expectedRevision !== undefined && accepted.expectedRevision !== revision)
@@ -249,17 +273,26 @@ export class PresetInstaller {
249
273
  async apply(source, options = {}) {
250
274
  return (await this.stage(source, options)).activate();
251
275
  }
252
- async update() {
276
+ async update(options = {}) {
253
277
  const current = await this.store.current({ verifyResources: false });
254
278
  if (!current.receipt)
255
279
  throw new Error('No preset is installed');
256
- return this.apply(current.receipt.snapshot.source, { expectedRevision: presetSelectionRevision(current) });
280
+ return this.apply(current.receipt.snapshot.source, { ...options, expectedRevision: presetSelectionRevision(current) });
257
281
  }
258
- async resume(expectedRevision) {
282
+ async resume(expectedRevision, options = {}) {
259
283
  const current = await this.store.current();
260
284
  if (!current.receipt || current.receipt.revision !== expectedRevision)
261
285
  throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed before resuming');
262
- return this.synchronize(current.receipt, expectedRevision);
286
+ const names = options.replaceSkills;
287
+ if (names !== undefined && (!Array.isArray(names) || !names.length || names.length > 256 || new Set(names).size !== names.length || names.some(name => typeof name !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(name) || !current.preset || !Object.hasOwn(current.preset.skills, name) || current.preset.skills[name].enabled === false)))
288
+ throw new Error('Invalid Skill replacement selection');
289
+ return this.synchronize(current.receipt, expectedRevision, options.replaceSkills);
290
+ }
291
+ async repair(options = {}) {
292
+ const current = await this.store.current({ verifyResources: false });
293
+ if (!current.receipt)
294
+ throw new Error('No preset is installed');
295
+ return this.resume(current.receipt.revision, options);
263
296
  }
264
297
  async configure(value) {
265
298
  const current = await this.store.current();
@@ -269,7 +302,11 @@ export class PresetInstaller {
269
302
  return this.synchronize(receipt, presetSelectionRevision(current));
270
303
  }
271
304
  }
272
- export async function runPresetCommand(command, source) {
305
+ export async function runPresetCommand(command, source, options = {}) {
306
+ if (command !== 'repair' && options.replaceSkills !== undefined)
307
+ throw new Error('Skill replacements require preset repair');
273
308
  const installer = new PresetInstaller();
309
+ if (command === 'repair')
310
+ return installer.repair(options);
274
311
  return command === 'status' ? installer.status() : command === 'update' ? installer.update() : installer.apply(source);
275
312
  }
@@ -1,4 +1,4 @@
1
- import { type JsonObject, type NodeCapabilityActivation, type ResolvedPreset } from '@mearl/preset';
1
+ import { type JsonObject, type CapabilityActivation, type ResolvedPreset } from '@mearl/preset';
2
2
  import { type PreparedBundle, type PreparedPreset, type PresetSnapshot } from '@mearl/preset/bundles';
3
3
  export interface PresetInstallReceipt {
4
4
  schemaVersion: 1;
@@ -9,13 +9,12 @@ export interface PresetInstallReceipt {
9
9
  managedModules: string[];
10
10
  nodeSyncRequired: boolean;
11
11
  browserSyncRequired?: boolean;
12
- nodeConfig: Record<string, JsonObject>;
13
12
  overrides: JsonObject;
14
13
  revision: string;
15
14
  }
16
15
  interface SelectionState {
17
16
  rawHash: string | null;
18
- activation?: NodeCapabilityActivation;
17
+ activation?: CapabilityActivation;
19
18
  receipt?: PresetInstallReceipt;
20
19
  prepared?: PreparedPreset;
21
20
  preset?: ResolvedPreset;
@@ -36,7 +35,6 @@ export declare class PresetInstallationStore {
36
35
  apply(input: Pick<PreparedPreset, 'snapshot' | 'bundles'>, options: {
37
36
  expectedRevision: string | null;
38
37
  overrides?: JsonObject;
39
- nodeConfig?: Record<string, JsonObject>;
40
38
  signal?: AbortSignal;
41
39
  confirm?: () => Promise<void>;
42
40
  }): Promise<PresetInstallReceipt>;
@@ -3,13 +3,13 @@ import { constants, closeSync, fstatSync, lstatSync, linkSync, mkdirSync, openSy
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { withRecordLock } from '@mearl/daemon-core';
6
- import { mergePresetDefaults, prepareNodeBundleSnapshot, prepareNodeCapabilitySnapshot, verifyNodeCapabilityActivation, validateNodeModuleConfig, resolvePresetModuleConfig, MAX_NODE_SELECTION_BYTES } from '@mearl/preset';
6
+ import { mergePresetDefaults, prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilityActivation, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from '@mearl/preset';
7
7
  import { BundleError, MAX_BROWSER_STORAGE_BYTES, presetRequiresBrowser, validateBundleManifest, verifyPreset, verifyBrowserPreset, verifyPreparedPreset } from '@mearl/preset/bundles';
8
8
  const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
9
9
  const hashPattern = /^[a-f0-9]{64}$/;
10
10
  const MAX_RECEIPT_BYTES = 16 * 1024 * 1024;
11
11
  const MAX_OVERRIDES_BYTES = 262_144;
12
- const POINTER_BYTES = MAX_NODE_SELECTION_BYTES + 128;
12
+ const POINTER_BYTES = MAX_CAPABILITY_SELECTION_BYTES + 128;
13
13
  function readBounded(file, limit) {
14
14
  const before = lstatSync(file);
15
15
  if (!before.isFile() || before.isSymbolicLink() || before.size > limit)
@@ -84,7 +84,7 @@ export class PresetInstallationStore {
84
84
  if (!raw)
85
85
  return { rawHash: null };
86
86
  const rawHash = digest(raw);
87
- const activation = await verifyNodeCapabilityActivation(JSON.parse(raw.toString('utf8')));
87
+ const activation = await verifyCapabilityActivation(JSON.parse(raw.toString('utf8')));
88
88
  if (!activation.installationRevision)
89
89
  return { rawHash, activation };
90
90
  const captured = withRecordLock(this.pointer, () => {
@@ -93,13 +93,12 @@ export class PresetInstallationStore {
93
93
  const receipt = JSON.parse(receiptBytes.toString('utf8'));
94
94
  const { revision: receiptRevision, ...data } = receipt;
95
95
  if (receiptRevision !== activation.installationRevision || digest(JSON.stringify(data)) !== receiptRevision || data.schemaVersion !== 1 ||
96
- Object.keys(data).some(key => !['schemaVersion', 'snapshot', 'bundles', 'browserRevision', 'nodeRevision', 'managedModules', 'nodeSyncRequired', 'browserSyncRequired', 'nodeConfig', 'overrides'].includes(key)) || typeof data.nodeSyncRequired !== 'boolean' || (data.browserSyncRequired !== undefined && typeof data.browserSyncRequired !== 'boolean') ||
96
+ Object.keys(data).some(key => !['schemaVersion', 'snapshot', 'bundles', 'browserRevision', 'nodeRevision', 'managedModules', 'nodeSyncRequired', 'browserSyncRequired', 'overrides'].includes(key)) || typeof data.nodeSyncRequired !== 'boolean' || (data.browserSyncRequired !== undefined && typeof data.browserSyncRequired !== 'boolean') ||
97
97
  data.nodeRevision !== activation.revision || !Array.isArray(data.bundles) || data.bundles.length > 64 ||
98
98
  !Array.isArray(data.managedModules) || new Set(data.managedModules).size !== data.managedModules.length || data.managedModules.some(id => typeof id !== 'string' || !Object.hasOwn(activation.modules, id))) {
99
99
  throw new BundleError('BUNDLE_INTEGRITY', 'Installed preset receipt does not match its selection');
100
100
  }
101
101
  const acceptedOverrides = overrides(data.overrides);
102
- const nodeConfig = validateNodeModuleConfig(data.nodeConfig);
103
102
  let total = 0;
104
103
  const metadata = data.bundles.map(bundle => {
105
104
  if (Object.keys(bundle).some(key => !['source', 'manifest', 'revision'].includes(key)))
@@ -116,18 +115,18 @@ export class PresetInstallationStore {
116
115
  if (bytes.byteLength !== entry.byteLength || digest(bytes) !== entry.sha256)
117
116
  throw new BundleError('BUNDLE_INTEGRITY', 'Installed Node object is missing or corrupt');
118
117
  }
119
- return { receipt: { ...receipt, overrides: acceptedOverrides, nodeConfig }, bundles };
118
+ return { receipt: { ...receipt, overrides: acceptedOverrides }, bundles };
120
119
  });
121
120
  if (options.verifyResources === false) {
122
121
  const graph = await verifyPreset(captured.receipt.snapshot, this.mearlApiVersion);
123
- await verifyBrowserPreset({ source: graph.snapshot.source, manifests: graph.snapshot.manifests,
124
- bundleRevisions: Object.fromEntries(Object.entries(graph.snapshot.bundleRevisions).filter(([id]) => graph.preset.packages[id].runtimes?.includes('browser'))), revision: captured.receipt.browserRevision }, this.mearlApiVersion);
122
+ await verifyBrowserPreset({ source: graph.snapshot.source, manifests: graph.snapshot.manifests, packageTargets: graph.snapshot.packageTargets,
123
+ bundleRevisions: Object.fromEntries(Object.entries(graph.snapshot.bundleRevisions).filter(([id]) => graph.preset.packages[id].targets?.includes('browser'))), revision: captured.receipt.browserRevision }, this.mearlApiVersion);
125
124
  return { rawHash, activation, receipt: captured.receipt, preset: graph.preset };
126
125
  }
127
126
  const prepared = await verifyPreparedPreset({ snapshot: captured.receipt.snapshot, bundles: captured.bundles }, this.mearlApiVersion);
128
127
  if (prepared.browser.snapshot.revision !== captured.receipt.browserRevision)
129
128
  throw new BundleError('BUNDLE_INTEGRITY', 'Installed browser projection does not match');
130
- const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].runtimes?.includes('node')), 'native-host', this.mearlApiVersion, resolvePresetModuleConfig(mergePresetDefaults(prepared.preset.defaults, captured.receipt.overrides), captured.receipt.nodeConfig));
129
+ const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].targets?.includes('node')), 'native-host', this.mearlApiVersion, resolveCapabilityConfig(mergePresetDefaults(prepared.preset.defaults, captured.receipt.overrides)));
131
130
  if (Object.keys(selected.snapshot.modules).length !== captured.receipt.managedModules.length || captured.receipt.managedModules.some(id => !Object.hasOwn(selected.snapshot.modules, id)) ||
132
131
  Object.entries(selected.snapshot.modules).some(([id, entry]) => JSON.stringify(entry) !== JSON.stringify(activation.modules[id])) ||
133
132
  Object.entries(selected.snapshot.bindings).some(([hook, entry]) => JSON.stringify(entry) !== JSON.stringify(activation.bindings[hook])) ||
@@ -139,12 +138,10 @@ export class PresetInstallationStore {
139
138
  async apply(input, options) {
140
139
  options.signal?.throwIfAborted();
141
140
  const acceptedOverrides = options.overrides === undefined ? undefined : overrides(options.overrides);
142
- const nodeConfig = options.nodeConfig === undefined ? undefined : validateNodeModuleConfig(options.nodeConfig);
143
141
  const [current, prepared] = await Promise.all([this.current({ verifyResources: false }), verifyPreparedPreset(input, this.mearlApiVersion)]);
144
142
  if (revision(current) !== options.expectedRevision)
145
143
  throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed');
146
- const configured = nodeConfig ?? current.receipt?.nodeConfig ?? Object.fromEntries(Object.entries(current.activation?.modules ?? {}).filter(([, entry]) => entry.config !== undefined).map(([id, entry]) => [id, entry.config]));
147
- const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].runtimes?.includes('node')), 'native-host', this.mearlApiVersion, resolvePresetModuleConfig(mergePresetDefaults(prepared.preset.defaults, acceptedOverrides ?? current.receipt?.overrides ?? {}), configured));
144
+ const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].targets?.includes('node')), 'native-host', this.mearlApiVersion, resolveCapabilityConfig(mergePresetDefaults(prepared.preset.defaults, acceptedOverrides ?? current.receipt?.overrides ?? {})));
148
145
  const previousManaged = new Set(current.receipt?.managedModules ?? []);
149
146
  const unmanaged = Object.fromEntries(Object.entries(current.activation?.modules ?? {}).filter(([id]) => !previousManaged.has(id)));
150
147
  for (const entry of Object.values(unmanaged)) {
@@ -157,15 +154,14 @@ export class PresetInstallationStore {
157
154
  const bindings = Object.fromEntries(Object.entries(current.activation?.bindings ?? {}).filter(([, entry]) => Object.hasOwn(unmanaged, entry.module)));
158
155
  if (Object.keys(selected.snapshot.bindings).some(hook => Object.hasOwn(bindings, hook)))
159
156
  throw new BundleError('BUNDLE_CONFLICT', 'A preset hook conflicts with an independently installed Node module');
160
- const snapshot = await prepareNodeCapabilitySnapshot({ schemaVersion: 1, modules: { ...unmanaged, ...selected.snapshot.modules }, bindings: { ...bindings, ...selected.snapshot.bindings } });
157
+ const snapshot = await prepareCapabilitySnapshot({ schemaVersion: 1, modules: { ...unmanaged, ...selected.snapshot.modules }, bindings: { ...bindings, ...selected.snapshot.bindings } });
161
158
  const data = {
162
159
  schemaVersion: 1, snapshot: prepared.snapshot,
163
160
  bundles: prepared.bundles.map(({ source, revision, manifest }) => ({ source, revision, manifest })),
164
161
  browserRevision: prepared.browser.snapshot.revision, nodeRevision: snapshot.revision,
165
162
  managedModules: Object.keys(selected.snapshot.modules), overrides: acceptedOverrides ?? current.receipt?.overrides ?? {},
166
- nodeSyncRequired: Boolean(current.receipt?.nodeSyncRequired || current.receipt?.managedModules.length || Object.values(prepared.preset.packages).some(entry => entry.enabled !== false && entry.runtimes?.includes('node'))),
163
+ nodeSyncRequired: Boolean(current.receipt?.nodeSyncRequired || current.receipt?.managedModules.length || Object.values(prepared.preset.packages).some(entry => entry.enabled !== false && entry.targets?.includes('node'))),
167
164
  browserSyncRequired: Boolean(current.receipt?.browserSyncRequired || presetRequiresBrowser(prepared.preset)),
168
- nodeConfig: configured,
169
165
  };
170
166
  const receipt = { ...data, revision: digest(JSON.stringify(data)) };
171
167
  const receiptBytes = JSON.stringify(receipt);
@@ -207,7 +203,7 @@ export class PresetInstallationStore {
207
203
  const record = JSON.parse(readBounded(join(this.directory, 'rollback.json'), POINTER_BYTES + 256).toString('utf8'));
208
204
  if (record.candidate !== expectedRevision)
209
205
  throw new BundleError('BUNDLE_CONFLICT', 'The preset rollback record has changed');
210
- const previous = record.previous === null ? undefined : await verifyNodeCapabilityActivation(record.previous);
206
+ const previous = record.previous === null ? undefined : await verifyCapabilityActivation(record.previous);
211
207
  withRecordLock(this.pointer, () => {
212
208
  this.assertCurrent(current.rawHash);
213
209
  if (previous)
@@ -222,7 +218,7 @@ export class PresetInstallationStore {
222
218
  const current = await this.current({ verifyResources: false });
223
219
  const raw = optionalFile(join(this.directory, 'rollback.json'), POINTER_BYTES + 256);
224
220
  const record = raw && JSON.parse(raw.toString('utf8'));
225
- const previous = record?.candidate === current.activation?.installationRevision && record.previous !== null ? await verifyNodeCapabilityActivation(record.previous) : undefined;
221
+ const previous = record?.candidate === current.activation?.installationRevision && record.previous !== null ? await verifyCapabilityActivation(record.previous) : undefined;
226
222
  withRecordLock(this.pointer, () => {
227
223
  this.assertCurrent(current.rawHash);
228
224
  for (const child of ['blobs', 'objects', 'installations'])
@@ -0,0 +1,9 @@
1
+ import { PresetInstaller, type StagedPresetInstallation } from './preset.js';
2
+ /** Preserve the accepted installation and resume its exact resources before Skill operations. */
3
+ export declare function prepareSetupPreset(options: {
4
+ source?: string;
5
+ fallbackSource?: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ }, dependencies?: {
8
+ installer?: PresetInstaller;
9
+ }): Promise<StagedPresetInstallation | undefined>;
@@ -0,0 +1,14 @@
1
+ import { PresetInstaller } from './preset.js';
2
+ /** Preserve the accepted installation and resume its exact resources before Skill operations. */
3
+ export async function prepareSetupPreset(options, dependencies = {}) {
4
+ const installer = dependencies.installer ?? new PresetInstaller({ directory: (options.env ?? process.env).MEARL_CAPABILITY_DIR });
5
+ if (options.source)
6
+ return installer.stage(options.source);
7
+ const current = await installer.store.current();
8
+ if (current.receipt)
9
+ return { revision: current.receipt.revision, activate: () => installer.resume(current.receipt.revision) };
10
+ if (current.activation)
11
+ return;
12
+ if (options.fallbackSource)
13
+ return installer.stage(options.fallbackSource);
14
+ }
@@ -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:registry=https://registry.npmjs.org @mearl/setup${SETUP_VERSION.includes('-beta.') ? '@' + SETUP_VERSION : ''}`;
4
+ export const SETUP_COMMAND = `npx @mearl/setup${SETUP_VERSION.includes('-beta.') ? '@beta' : ''}`;
package/dist/skills.js CHANGED
@@ -4,11 +4,11 @@ import { homedir } from 'node:os';
4
4
  import { NodeCapabilityRegistry } from '@mearl/daemon-core/capabilities';
5
5
  import { inspectGlobalSkill, readTrackedSkillSources } from './skillSources.js';
6
6
  import { PresetInstallationStore } from './presetStore.js';
7
- import { findExecutable, isExecutable, prependCommandPath, prependNodePath, runCommand, } from '@mearl/daemon-core';
7
+ import { findExecutable, isExecutable, npmExecEnvironment, prependCommandPath, prependNodePath, runCommand, } from '@mearl/daemon-core';
8
8
  export const SKILLS_CLI_PACKAGE = 'skills@1.5.0';
9
9
  export const MEARL_SKILL_SOURCE = SETUP_VERSION.includes('-beta.')
10
- ? `https://github.com/F-loat/mearl/tree/v${SETUP_VERSION}`
11
- : 'https://github.com/F-loat/mearl';
10
+ ? `https://github.com/mearlhq/mearl/tree/v${SETUP_VERSION}`
11
+ : 'https://github.com/mearlhq/mearl';
12
12
  const COMMAND_TIMEOUT_MS = 120_000;
13
13
  const ANSI_ESCAPE_PATTERN = new RegExp(String.raw `\u001B\[[0-?]*[ -/]*[@-~]`, 'g');
14
14
  export function stripAnsi(value) {
@@ -41,7 +41,7 @@ export function resolveSkillsInvocation(options = {}) {
41
41
  return {
42
42
  command: npx,
43
43
  argsPrefix: ['-y', SKILLS_CLI_PACKAGE],
44
- env: prependNodePath(prependCommandPath(npx, env), processExecPath),
44
+ env: npmExecEnvironment(prependNodePath(prependCommandPath(npx, env), processExecPath)),
45
45
  };
46
46
  }
47
47
  /** Only the selected manager module is needed; browser and Provider factories stay in their hosts. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/setup",
3
- "version": "3.0.0-beta.4",
3
+ "version": "3.0.0-beta.7",
4
4
  "description": "One-shot environment setup and configuration for Mearl",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -68,31 +68,32 @@
68
68
  ],
69
69
  "license": "Apache-2.0",
70
70
  "engines": {
71
- "node": ">=22.12.0"
71
+ "node": ">=20.18.0"
72
72
  },
73
73
  "publishConfig": {
74
74
  "registry": "https://registry.npmjs.org"
75
75
  },
76
76
  "dependencies": {
77
- "commander": "^15.0.0",
78
- "extract-zip": "^2.0.1",
77
+ "commander": "^14.0.3",
79
78
  "ws": "^8.18.3",
80
79
  "semver": "^7.8.5",
81
- "@mearl/client": "3.0.0-beta.4",
82
- "@mearl/provider": "3.0.0-beta.4",
83
- "@mearl/daemon-core": "3.0.0-beta.4",
84
- "@mearl/preset": "3.0.0-beta.4",
85
- "@mearl/browser-core": "3.0.0-beta.4"
80
+ "adm-zip": "^0.6.1",
81
+ "@mearl/client": "3.0.0-beta.7",
82
+ "@mearl/provider": "3.0.0-beta.7",
83
+ "@mearl/daemon-core": "3.0.0-beta.7",
84
+ "@mearl/preset": "3.0.0-beta.7",
85
+ "@mearl/browser-core": "3.0.0-beta.7"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@types/node": "^24.9.1",
89
89
  "@types/ws": "^8.18.1",
90
90
  "typescript": "^5.8.3",
91
- "@types/semver": "^7.7.1"
91
+ "@types/semver": "^7.7.1",
92
+ "@types/adm-zip": "^0.5.7"
92
93
  },
93
94
  "repository": {
94
95
  "type": "git",
95
- "url": "git+https://github.com/F-loat/mearl.git",
96
+ "url": "git+https://github.com/mearlhq/mearl.git",
96
97
  "directory": "packages/setup"
97
98
  },
98
99
  "scripts": {
@@ -1,13 +0,0 @@
1
- import { type InstallationConfig } from '@mearl/preset/installation';
2
- import { PresetInstaller, type StagedPresetInstallation } from './preset.js';
3
- import { type DetectedGlobalPackage } from './packageManager.js';
4
- export declare const MEARL_INSTALLATION_CONFIG_URL = "https://f-loat.github.io/mearl/installation.json";
5
- /** Reuse the installation receipt as the retry marker; organization defaults come from deployment data. */
6
- export declare function prepareSetupPreset(options: {
7
- source?: string;
8
- installed?: Array<Pick<DetectedGlobalPackage, 'packageName' | 'packageRoot'>>;
9
- env?: NodeJS.ProcessEnv;
10
- }, dependencies?: {
11
- installer?: PresetInstaller;
12
- configuration?: () => Promise<InstallationConfig>;
13
- }): Promise<StagedPresetInstallation | undefined>;
@@ -1,42 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { loadInstallationConfig } from '@mearl/preset/installation';
4
- import { PresetInstaller } from './preset.js';
5
- import { detectGlobalPackage } from './packageManager.js';
6
- import { detectMearlProfiles } from './profiles.js';
7
- export const MEARL_INSTALLATION_CONFIG_URL = 'https://f-loat.github.io/mearl/installation.json';
8
- function isLegacyPackage(installation) {
9
- try {
10
- const manifest = JSON.parse(readFileSync(join(installation.packageRoot, 'package.json'), 'utf8'));
11
- return manifest.name === installation.packageName && typeof manifest.version === 'string' && /^[0-2]\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(manifest.version);
12
- }
13
- catch {
14
- return false;
15
- }
16
- }
17
- /** Reuse the installation receipt as the retry marker; organization defaults come from deployment data. */
18
- export async function prepareSetupPreset(options, dependencies = {}) {
19
- const env = options.env ?? process.env;
20
- const installer = dependencies.installer ?? new PresetInstaller({ directory: env.MEARL_CAPABILITY_DIR });
21
- if (options.source)
22
- return installer.stage(options.source);
23
- const current = await installer.store.current();
24
- if (current.receipt)
25
- return { revision: current.receipt.revision, activate: () => installer.resume(current.receipt.revision) };
26
- if (current.activation)
27
- return;
28
- const client = options.installed === undefined ? detectGlobalPackage('@mearl/client', ['mearl'], { env }) : null;
29
- const installed = options.installed ?? [...detectMearlProfiles({ env }).values(), ...(client ? [client] : [])];
30
- if (!installed.some(isLegacyPackage))
31
- return;
32
- const configuration = dependencies.configuration ?? (() => {
33
- const url = new URL(env.MEARL_INSTALLATION_CONFIG_URL || MEARL_INSTALLATION_CONFIG_URL);
34
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))
35
- throw new Error('Installation configuration requires HTTPS or loopback HTTP');
36
- return loadInstallationConfig(url, url.origin, AbortSignal.timeout(5_000));
37
- });
38
- const config = await configuration();
39
- if (!config.legacyPreset)
40
- throw new Error('Legacy upgrade preset is unavailable. Retry when the installation catalog is available, or select a preset with --preset.');
41
- return installer.stage(config.presets[config.legacyPreset].source, { expectedRevision: null, expectedId: config.legacyPreset });
42
- }