@mearl/setup 3.0.0-beta.5 → 3.0.0-beta.8
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 +25 -17
- package/dist/browserExtension.js +24 -8
- package/dist/browserPreset.js +1 -1
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +39 -10
- package/dist/lifecycle.d.ts +1 -1
- package/dist/lifecycle.js +3 -3
- package/dist/preset.d.ts +9 -3
- package/dist/preset.js +24 -11
- package/dist/presetStore.d.ts +2 -4
- package/dist/presetStore.js +13 -17
- package/dist/skills.js +4 -4
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,43 +1,52 @@
|
|
|
1
1
|
# @mearl/setup
|
|
2
2
|
|
|
3
|
-
3.0 当前以 beta 发行。使用 `@beta` 入口安装同版本的 setup
|
|
3
|
+
3.0 当前以 beta 发行。使用 `@beta` 入口安装同版本的 setup、运行包和扩展;正式版使用 `latest` 标签。
|
|
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/setup@beta [install] [--local] [--cloud
|
|
9
|
-
npx @mearl/setup@beta update [--local] [--cloud
|
|
10
|
-
npx @mearl/setup@beta uninstall [--local] [--cloud
|
|
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]
|
|
11
14
|
npx @mearl/setup@beta extension [--full] [--force]
|
|
12
15
|
npx @mearl/setup@beta plugin [name] <path>
|
|
13
16
|
npx @mearl/setup@beta plugin install <skills add 参数...>
|
|
14
17
|
npx @mearl/setup@beta plugin uninstall <name>
|
|
15
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
|
-
|
|
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/setup@beta plugin install https://github.com/
|
|
32
|
+
npx @mearl/setup@beta plugin install https://github.com/mearlhq/mearl --skill browser-tab-manager --yes
|
|
32
33
|
mearl tabs summary
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
安装参数传给固定版本的公共 Skills CLI(当前 skills@1.5.0,兼容项目的 Node 最低版本),使用全局共享目录。公共 CLI 的默认 symlink 和复制模式都可注册;复制模式通过 list --global --json 确认本次成功结果对应的 canonical path,不猜测目录,也不扫描其他 Skills。自定义 CLI 可通过 MEARL_SKILLS_PATH 指定。参数不接受截断后置选项的 -- 分隔符。
|
|
36
37
|
|
|
37
|
-
安装宿主提供的本地插件目录决定哪些能力由 preset 管理。install/update
|
|
38
|
+
安装宿主提供的本地插件目录决定哪些能力由 preset 管理。install/update 维护目录中的托管注册,保留用户自行注册的插件。卸载最后一个依赖环境时移除托管插件与 Mearl Skill;其他环境需要时保留。配置准备、宿主同步和实际安装状态共用 [安装合同](../../docs/preset-installation.md)。
|
|
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)。
|
|
@@ -80,7 +89,7 @@ npx @mearl/setup@beta provider update example-device
|
|
|
80
89
|
npx @mearl/setup@beta provider uninstall example-device
|
|
81
90
|
```
|
|
82
91
|
|
|
83
|
-
安装参数接受包名、版本或 dist-tag;更新和卸载可使用 providerId 或完整包名。核心 @mearl 包固定使用公共 npm registry;其余 scope
|
|
92
|
+
安装参数接受包名、版本或 dist-tag;更新和卸载可使用 providerId 或完整包名。核心 @mearl 包固定使用公共 npm registry;其余 scope 沿用调用方的包管理器配置。安装沿用启动 setup 的包管理器,
|
|
84
93
|
更新和卸载沿用当前 Provider 的原包管理器。Provider 登记只保存
|
|
85
94
|
经过校验的包名、安装目录和包内入口;不接受包自行注入启动命令、参数、工作目录或环境变量。
|
|
86
95
|
卸载仅在包管理器成功移除包后清除登记,以便失败时可以重试。
|
|
@@ -96,13 +105,12 @@ npx @mearl/setup@beta provider uninstall example-device
|
|
|
96
105
|
`mearl-native-host`、`mearl-cloud-server`、`mearl-mcp-server` 自动检测,可同时处理
|
|
97
106
|
多个已安装环境。
|
|
98
107
|
- `--local`、`--cloud` 与 `--mcp` 可以组合,显式传入时只处理选中的环境;
|
|
99
|
-
`--
|
|
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
|
|
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/setup@beta provider uninstall example-device
|
|
|
121
129
|
CDP 标签 ID 计数也保留,防止旧请求命中新标签。
|
|
122
130
|
- `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
|
|
123
131
|
- 默认的 `install` / `update` 不处理浏览器扩展。`extension` 是独立命令:从
|
|
124
|
-
`https://github.com/
|
|
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
|
|
@@ -237,9 +245,9 @@ npx @mearl/setup@beta provider install @example/mearl-provider-cloud
|
|
|
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
|
-
|
|
248
|
+
install/update 在替换包前保存选中 Preset 的确切资源和期望选择,运行时初始化后同步宿主并安装 Skills;`extension --full` 在替换扩展前完成同一准备流程。已有本机选择优先保留,显式 `--preset` 用于更新或修复资源。配置或资源不可用时停止包变更;部分升级后的重试使用已保存记录。卸载不准备或安装 Preset。
|
|
241
249
|
|
|
242
|
-
默认目录为公共站点的 `installation.json`,可通过 `MEARL_INSTALLATION_CONFIG_URL` 指定其他 HTTPS 目录(本地验证允许回环 HTTP
|
|
250
|
+
默认目录为公共站点的 `installation.json`,可通过 `MEARL_INSTALLATION_CONFIG_URL` 指定其他 HTTPS 目录(本地验证允许回环 HTTP)。目录采用网页安装的校验合同;本机与浏览器未全部确认时报告 pending。
|
|
243
251
|
|
|
244
252
|
## 浏览器 preset 与发行入口
|
|
245
253
|
|
package/dist/browserExtension.js
CHANGED
|
@@ -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
|
|
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/
|
|
14
|
-
: 'https://github.com/
|
|
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/
|
|
17
|
-
: 'https://github.com/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
489
|
+
extractExtensionArchive;
|
|
474
490
|
const loadUnpacked = dependencies.loadUnpacked ?? loadUnpackedViaCdp;
|
|
475
491
|
const inspectInstallation = dependencies.inspectInstallation ?? inspectMearlExtensionViaCdp;
|
|
476
492
|
const inspectInstalled = dependencies.inspectInstalled ?? inspectSetupManagedExtension;
|
package/dist/browserPreset.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function prepareBrowserSetupPreset(options = {}, dependencies = {})
|
|
|
18
18
|
return loadInstallationConfig(url, url.origin, AbortSignal.timeout(5_000));
|
|
19
19
|
});
|
|
20
20
|
const read = async (timeoutSec = 5) => {
|
|
21
|
-
const targets = browsers().filter(browser => !browser.provider && browser.transport === 'extension' && browser.status === 'connected' && (!env.MEARL_SOCKET_PATH || browser.socketPath === env.MEARL_SOCKET_PATH));
|
|
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
22
|
if (!targets.length)
|
|
23
23
|
return;
|
|
24
24
|
return (await Promise.all(targets.map(async (browser) => {
|
package/dist/cli.d.ts
CHANGED
|
@@ -8,13 +8,14 @@ import { runPresetCommand } from './preset.js';
|
|
|
8
8
|
import { prepareSetupPreset } from './setupPreset.js';
|
|
9
9
|
export { SETUP_VERSION } from './setupVersion.js';
|
|
10
10
|
interface ParsedArgs {
|
|
11
|
-
cloudMode?:
|
|
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
|
@@ -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('--
|
|
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.
|
|
102
|
-
throw new Error('--cloud
|
|
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.
|
|
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.
|
|
125
|
-
? { cloudMode: options.
|
|
145
|
+
...(options.cloud || options.cloudMode
|
|
146
|
+
? { cloudMode: options.cloudMode ?? 'websocket' }
|
|
126
147
|
: {}),
|
|
127
148
|
...(profiles.length > 0 ? { profiles } : {}),
|
|
128
149
|
...(options.full ? { full: true } : {}),
|
|
@@ -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
|
-
--
|
|
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
|
|
@@ -228,6 +253,7 @@ Examples:
|
|
|
228
253
|
npx @mearl/setup uninstall --full
|
|
229
254
|
npx @mearl/setup plugin uninstall yuque
|
|
230
255
|
npx @mearl/setup provider install @example/mearl-provider-brow
|
|
256
|
+
npx @mearl/setup preset repair --replace-skill example-skill
|
|
231
257
|
npx @mearl/setup extension --full`;
|
|
232
258
|
console.log(output.replaceAll('npx @mearl/setup', SETUP_COMMAND));
|
|
233
259
|
}
|
|
@@ -271,7 +297,10 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
|
|
|
271
297
|
}
|
|
272
298
|
if (parsed.operation === 'preset') {
|
|
273
299
|
try {
|
|
274
|
-
const
|
|
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);
|
|
275
304
|
console.log(JSON.stringify(result, null, 2));
|
|
276
305
|
return result.ready ? 0 : 3;
|
|
277
306
|
}
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface MearlLifecycleOptions {
|
|
|
18
18
|
/** Distribution default used only when neither local nor browser selection exists. */
|
|
19
19
|
defaultPresetSource?: string;
|
|
20
20
|
operation: MearlLifecycleOperation;
|
|
21
|
-
cloudMode?:
|
|
21
|
+
cloudMode?: string;
|
|
22
22
|
/** Uninstall every registered CLI plugin and Provider in addition to selected environments. */
|
|
23
23
|
full?: boolean;
|
|
24
24
|
/** @deprecated Use profiles when selecting one or more composable environments. */
|
package/dist/lifecycle.js
CHANGED
|
@@ -136,12 +136,12 @@ async function runHook(profile, operation, detected, runner, existsSync, env, wr
|
|
|
136
136
|
return false;
|
|
137
137
|
}
|
|
138
138
|
const running = /\[CloudServer\]\s+Status:\s+running\b/.test(status.stdout);
|
|
139
|
-
const
|
|
140
|
-
if (operation === 'update' && !running && !
|
|
139
|
+
const pluginMode = Boolean(cloudMode && cloudMode !== 'websocket') || /\[CloudServer\]\s+Mode:\s+(?!websocket\b)\S+/.test(status.stdout);
|
|
140
|
+
if (operation === 'update' && !running && !pluginMode) {
|
|
141
141
|
writeLine('Hook: cloud update skipped because cloud-server is not running.');
|
|
142
142
|
return true;
|
|
143
143
|
}
|
|
144
|
-
args = [running && !
|
|
144
|
+
args = [running && !pluginMode ? 'restart' : 'start'];
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
else {
|
package/dist/preset.d.ts
CHANGED
|
@@ -65,7 +65,6 @@ export interface PresetApplyOptions {
|
|
|
65
65
|
validatePrepared?: (prepared: PreparedPreset) => Promise<void>;
|
|
66
66
|
skipUnchanged?: boolean;
|
|
67
67
|
overrides?: JsonObject;
|
|
68
|
-
nodeConfig?: Record<string, JsonObject>;
|
|
69
68
|
signal?: AbortSignal;
|
|
70
69
|
expectedRevision?: string | null;
|
|
71
70
|
expectedId?: string;
|
|
@@ -93,7 +92,14 @@ export declare class PresetInstaller {
|
|
|
93
92
|
stage(source: string, options?: PresetApplyOptions): Promise<StagedPresetInstallation>;
|
|
94
93
|
apply(source: string, options?: PresetApplyOptions): Promise<PresetInstallStatus>;
|
|
95
94
|
update(options?: Pick<PresetApplyOptions, 'validatePrepared' | 'skipUnchanged' | 'signal'>): Promise<PresetInstallStatus>;
|
|
96
|
-
resume(expectedRevision: string
|
|
95
|
+
resume(expectedRevision: string, options?: {
|
|
96
|
+
replaceSkills?: readonly string[];
|
|
97
|
+
}): Promise<PresetInstallStatus>;
|
|
98
|
+
repair(options?: {
|
|
99
|
+
replaceSkills?: readonly string[];
|
|
100
|
+
}): Promise<PresetInstallStatus>;
|
|
97
101
|
configure(value: JsonObject): Promise<PresetInstallStatus>;
|
|
98
102
|
}
|
|
99
|
-
export declare function runPresetCommand(command: 'apply' | 'update' | 'status', source?: string
|
|
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
|
@@ -112,12 +112,12 @@ export class PresetInstaller {
|
|
|
112
112
|
}
|
|
113
113
|
}));
|
|
114
114
|
}
|
|
115
|
-
async componentStatus(install, receipt) {
|
|
115
|
+
async componentStatus(install, receipt, replaceSkills) {
|
|
116
116
|
const target = (this.options.hosts?.() ?? localHosts(install))[0];
|
|
117
117
|
if (!target)
|
|
118
118
|
return {};
|
|
119
119
|
try {
|
|
120
|
-
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);
|
|
121
121
|
if (result?.installationRevision !== receipt.revision || result?.nodeRevision !== receipt.nodeRevision || !Array.isArray(result?.components))
|
|
122
122
|
throw new Error('Component installation host requires an update');
|
|
123
123
|
if (result.catalogReady !== undefined && typeof result.catalogReady !== 'boolean')
|
|
@@ -151,7 +151,7 @@ export class PresetInstaller {
|
|
|
151
151
|
result.nodeRevision = receipt.nodeRevision;
|
|
152
152
|
result.browserRevision = receipt.browserRevision;
|
|
153
153
|
result.browserSyncRequired = receipt.browserSyncRequired === true;
|
|
154
|
-
result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.
|
|
154
|
+
result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.targets?.includes('node')).length
|
|
155
155
|
+ [...Object.values(preset.skills), ...Object.values(preset.providers)].filter(entry => entry.enabled !== false).length;
|
|
156
156
|
result.hosts = await this.hostStatus(false, receipt.nodeRevision, receipt.revision);
|
|
157
157
|
const nodeReady = result.hosts.length > 0 && result.hosts.every(host => host.state === 'active');
|
|
@@ -161,12 +161,12 @@ export class PresetInstaller {
|
|
|
161
161
|
for (const [id, entry] of Object.entries(preset.packages)) {
|
|
162
162
|
if (entry.enabled === false)
|
|
163
163
|
continue;
|
|
164
|
-
for (const runtime of entry.
|
|
164
|
+
for (const runtime of entry.targets ?? []) {
|
|
165
165
|
if (runtime === 'browser') {
|
|
166
166
|
if (!browserReady)
|
|
167
167
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'BROWSER_SYNC_REQUIRED' });
|
|
168
168
|
}
|
|
169
|
-
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)
|
|
170
170
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'NODE_HOST_NOT_SUPPORTED' });
|
|
171
171
|
else if (!nodeReady)
|
|
172
172
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'HOST_SYNC_REQUIRED' });
|
|
@@ -196,7 +196,7 @@ export class PresetInstaller {
|
|
|
196
196
|
result.ready = result.pending.length === 0;
|
|
197
197
|
return result;
|
|
198
198
|
}
|
|
199
|
-
async synchronize(receipt, previousRevision) {
|
|
199
|
+
async synchronize(receipt, previousRevision, replaceSkills) {
|
|
200
200
|
const hosts = await this.hostStatus(true, receipt.nodeRevision, receipt.revision);
|
|
201
201
|
const rejection = hosts.find(host => host.state === 'rejected');
|
|
202
202
|
if (rejection) {
|
|
@@ -217,7 +217,7 @@ export class PresetInstaller {
|
|
|
217
217
|
if (installed.receipt?.revision !== receipt.revision || !installed.preset)
|
|
218
218
|
throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before component installation');
|
|
219
219
|
const hasExternal = [...Object.values(installed.preset.skills), ...Object.values(installed.preset.providers)].some(entry => entry.enabled !== false);
|
|
220
|
-
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) : {};
|
|
221
221
|
const status = await this.status();
|
|
222
222
|
if (components.error)
|
|
223
223
|
status.warnings.push(`Component installation failed; completed assets retained for retry: ${components.error}`);
|
|
@@ -263,7 +263,7 @@ export class PresetInstaller {
|
|
|
263
263
|
}
|
|
264
264
|
/** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
|
|
265
265
|
async stage(source, options = {}) {
|
|
266
|
-
const accepted = { ...options, ...(options.overrides === undefined ? {} : { overrides: mergePresetDefaults({}, options.overrides) })
|
|
266
|
+
const accepted = { ...options, ...(options.overrides === undefined ? {} : { overrides: mergePresetDefaults({}, options.overrides) }) };
|
|
267
267
|
const current = await this.store.current({ verifyResources: false });
|
|
268
268
|
const revision = presetSelectionRevision(current);
|
|
269
269
|
if (accepted.expectedRevision !== undefined && accepted.expectedRevision !== revision)
|
|
@@ -279,11 +279,20 @@ export class PresetInstaller {
|
|
|
279
279
|
throw new Error('No preset is installed');
|
|
280
280
|
return this.apply(current.receipt.snapshot.source, { ...options, expectedRevision: presetSelectionRevision(current) });
|
|
281
281
|
}
|
|
282
|
-
async resume(expectedRevision) {
|
|
282
|
+
async resume(expectedRevision, options = {}) {
|
|
283
283
|
const current = await this.store.current();
|
|
284
284
|
if (!current.receipt || current.receipt.revision !== expectedRevision)
|
|
285
285
|
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed before resuming');
|
|
286
|
-
|
|
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);
|
|
287
296
|
}
|
|
288
297
|
async configure(value) {
|
|
289
298
|
const current = await this.store.current();
|
|
@@ -293,7 +302,11 @@ export class PresetInstaller {
|
|
|
293
302
|
return this.synchronize(receipt, presetSelectionRevision(current));
|
|
294
303
|
}
|
|
295
304
|
}
|
|
296
|
-
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');
|
|
297
308
|
const installer = new PresetInstaller();
|
|
309
|
+
if (command === 'repair')
|
|
310
|
+
return installer.repair(options);
|
|
298
311
|
return command === 'status' ? installer.status() : command === 'update' ? installer.update() : installer.apply(source);
|
|
299
312
|
}
|
package/dist/presetStore.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JsonObject, type
|
|
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?:
|
|
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>;
|
package/dist/presetStore.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
|
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', '
|
|
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
|
|
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].
|
|
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].
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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'])
|
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/
|
|
11
|
-
: 'https://github.com/
|
|
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.
|
|
3
|
+
"version": "3.0.0-beta.8",
|
|
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": ">=
|
|
71
|
+
"node": ">=20.18.0"
|
|
72
72
|
},
|
|
73
73
|
"publishConfig": {
|
|
74
74
|
"registry": "https://registry.npmjs.org"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"commander": "^
|
|
78
|
-
"extract-zip": "^2.0.1",
|
|
77
|
+
"commander": "^14.0.3",
|
|
79
78
|
"ws": "^8.18.3",
|
|
80
79
|
"semver": "^7.8.5",
|
|
81
|
-
"
|
|
82
|
-
"@mearl/
|
|
83
|
-
"@mearl/
|
|
84
|
-
"@mearl/
|
|
85
|
-
"@mearl/
|
|
80
|
+
"adm-zip": "^0.6.1",
|
|
81
|
+
"@mearl/client": "3.0.0-beta.8",
|
|
82
|
+
"@mearl/provider": "3.0.0-beta.8",
|
|
83
|
+
"@mearl/daemon-core": "3.0.0-beta.8",
|
|
84
|
+
"@mearl/browser-core": "3.0.0-beta.8",
|
|
85
|
+
"@mearl/preset": "3.0.0-beta.8"
|
|
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/
|
|
96
|
+
"url": "git+https://github.com/mearlhq/mearl.git",
|
|
96
97
|
"directory": "packages/setup"
|
|
97
98
|
},
|
|
98
99
|
"scripts": {
|