@mearl/client 2.17.1 → 2.19.0
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 +86 -5
- package/dist/autoUpdate.d.ts +33 -0
- package/dist/autoUpdate.js +359 -0
- package/dist/autoUpdateWorker.d.ts +1 -0
- package/dist/autoUpdateWorker.js +5 -0
- package/dist/bin.js +3 -0
- package/dist/check.d.ts +11 -0
- package/dist/check.js +106 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -1
- package/dist/generated-browser-action-protocol.d.ts +4 -2
- package/dist/generated-browser-action-protocol.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +11 -3
- package/dist/plugin.js +44 -7
- package/dist/pluginPageBundle.d.ts +8 -0
- package/dist/pluginPageBundle.js +680 -0
- package/dist/pluginRegistry.d.ts +39 -0
- package/dist/pluginRegistry.js +261 -1
- package/dist/pythonPlugin.d.ts +1 -0
- package/dist/pythonPlugin.js +6 -0
- package/dist/socket.js +8 -13
- package/dist/unifiedConfig.d.ts +2 -0
- package/dist/unifiedConfig.js +23 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -127,7 +127,29 @@ Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通
|
|
|
127
127
|
| `--server <url>` | 覆盖自动读取的 cloud-server 地址 |
|
|
128
128
|
|
|
129
129
|
`browser_list --help` 和 `browser_launch --help` 会按场景展示宿主选择参数;连接或传输排障运行
|
|
130
|
-
`mearl check --help
|
|
130
|
+
`mearl check --help`。`mearl check` 检查连接与浏览器状态;`mearl check --updates` 还会并发查询
|
|
131
|
+
client、native-host、cloud-connector 和扩展的最新发布版本。发布版本查询最长等待约 1.5 秒,
|
|
132
|
+
无法访问发布源时会提示无法确认更新,不影响浏览器可用性检查。`--json` 输出机器可读的检查结果,
|
|
133
|
+
与 `--updates` 同用时包含更新状态。发现更新时会提示适用的命令:client/native-host 使用
|
|
134
|
+
`npx @mearl/setup update`,setup 管理的扩展使用 `npx @mearl/setup extension`。
|
|
135
|
+
|
|
136
|
+
全局安装的 `mearl` CLI 默认在每天首次使用后启动独立后台进程,查询最新发布版本,并比较
|
|
137
|
+
client、native-host、cloud-server 和 MCP server 的已安装版本。发现更新时调用
|
|
138
|
+
`@mearl/setup update --yes --skip-skill` 更新版本落后的运行环境及其共享 client,也支持独立安装的 CLI。
|
|
139
|
+
当前命令不等待网络或安装;检查和安装结果记录在
|
|
140
|
+
`~/.mearl/cli-auto-update-state.json`。项目依赖和临时 `npx` 副本不会被自动修改。
|
|
141
|
+
浏览器扩展沿用其独立的更新入口。
|
|
142
|
+
在 `~/.mearl/config.json` 中设置间隔天数或关闭自动更新:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{ "cliAutoUpdate": { "intervalDays": 7 } }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{ "cliAutoUpdate": false }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
间隔按本地日历日计算。需要立即检查各组件的发布版本时,仍可运行 `mearl check --updates`。
|
|
131
153
|
|
|
132
154
|
## Skill 插件
|
|
133
155
|
|
|
@@ -136,7 +158,7 @@ Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通
|
|
|
136
158
|
```bash
|
|
137
159
|
npx @mearl/setup plugin install trip/mearl --skill yuque-doc-fetch --yes
|
|
138
160
|
npx @mearl/setup plugin install trip/mearl --skill buc-request --yes
|
|
139
|
-
npx @mearl/setup plugin
|
|
161
|
+
npx @mearl/setup plugin /absolute/path/to/yuque-doc-fetch
|
|
140
162
|
npx @mearl/setup plugin uninstall yuque
|
|
141
163
|
mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
|
|
142
164
|
mearl yuque --payload '{"command":"read","input":{"url":"https://aliyuque.antfin.com/group/book/doc"}}'
|
|
@@ -149,9 +171,9 @@ mearl yuque --version
|
|
|
149
171
|
Provider 与插件仍可独立安装、更新和卸载,帮助聚合不会启动 Provider 或执行业务命令。
|
|
150
172
|
|
|
151
173
|
目录默认使用 `scripts/main.mjs` 或 `scripts/main.py`;两者同时存在时使用 JavaScript 入口。JavaScript 入口默认导出 `register(app)`,Python 入口导出 `register(app)` 函数;两种入口都可声明简短的 `name` 和 `description`,并通过 `app.command(signature, handler, description?)`
|
|
152
|
-
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。Python 的 `app.invoke` 是同步调用,handler 可以是普通函数或 `async def`。Python 插件要求 `python3 >= 3.9`,可用 `MEARL_PYTHON` 指定解释器路径;Mearl 不安装 Python 或执行 `pip install`。调用另一个插件时,payload 使用 `{ command, input }` envelope;嵌套调用继承宿主选项并拒绝循环依赖。CLI 默认只输出最终结果,传入
|
|
174
|
+
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。JavaScript 插件还可传入 `{ message, data }`,让工具页通过可 JSON 序列化的 `data` 获取稳定的阶段、对象 ID 等机器信息;CLI 仍只显示 `message`。Python 的 `app.invoke` 是同步调用,handler 可以是普通函数或 `async def`。Python 插件要求 `python3 >= 3.9`,可用 `MEARL_PYTHON` 指定解释器路径;Mearl 不安装 Python 或执行 `pip install`。调用另一个插件时,payload 使用 `{ command, input }` envelope;嵌套调用继承宿主选项并拒绝循环依赖。CLI 默认只输出最终结果,传入
|
|
153
175
|
`--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
|
|
154
|
-
|
|
176
|
+
注册名优先使用显式名称,其次读取入口的 `name` metadata,最后回退到 skill 目录名;常规注册只传路径。
|
|
155
177
|
setup 会补齐缺失或低于自身版本的 client、native-host;安装参数透传给 Ali Skills,并固定全局共享安装。
|
|
156
178
|
|
|
157
179
|
需要单独检查插件时运行 `mearl <name> --version`。命令会解析注册记录并加载入口,但不连接浏览器;
|
|
@@ -176,7 +198,7 @@ const result = await invokePlugin(
|
|
|
176
198
|
);
|
|
177
199
|
```
|
|
178
200
|
|
|
179
|
-
MCP Server 通过固定工具 `
|
|
201
|
+
MCP Server 通过固定工具 `plugin_invoke` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP 不接受任意入口路径;调用尚未注册的 `buc` 或 `tdbank` 时,会通过匹配当前 client 版本的 setup 自动安装并重试一次。其他未注册名称返回 `PLUGIN_NOT_REGISTERED`。
|
|
180
202
|
|
|
181
203
|
注册记录在 `~/.mearl/plugins/<name>.json`,保存入口路径和可选的简短描述;skill 业务代码原地更新直接生效,换路径或更新描述后重新注册。
|
|
182
204
|
`plugin uninstall <name>` 会对 `~/.agents/skills` 中的全局 Skill 调用 `ali-skills remove --global --yes`,
|
|
@@ -186,6 +208,65 @@ MCP Server 通过固定工具 `mearl_plugin` 接受同样的 `plugin`、`command
|
|
|
186
208
|
卸载清理命令。其他错误按实际原因处理。
|
|
187
209
|
编写规范见 [Mearl Skill Creator 的 Skill 插件规范](../skills/mearl-skill-creator/references/cli-plugin.md)。
|
|
188
210
|
|
|
211
|
+
插件可选导出 `toolbox`,注册后会出现在扩展 Popup 的工具箱中。页面资源放在 Skill 目录内,注册时由
|
|
212
|
+
Mearl 将入口 HTML 引用的本地 CSS、JavaScript 和图片内联为不可变页面包,不需要启动 HTTP 服务,
|
|
213
|
+
也不要求普通 HTML/CSS/JS 页面自行构建:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
export const toolbox = {
|
|
217
|
+
version: 1,
|
|
218
|
+
title: '迭代管理',
|
|
219
|
+
description: '查看多个仓库的改动与发布状态',
|
|
220
|
+
icon: 'git-branch',
|
|
221
|
+
locales: {
|
|
222
|
+
en: {
|
|
223
|
+
title: 'Iteration Management',
|
|
224
|
+
description: 'Track changes and release status across repositories',
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
agent: {
|
|
228
|
+
skill: 'iteration-manager',
|
|
229
|
+
layout: 'workspace',
|
|
230
|
+
},
|
|
231
|
+
page: {
|
|
232
|
+
type: 'sandbox',
|
|
233
|
+
runtime: 'preact',
|
|
234
|
+
entry: 'assets/toolbox/index.html',
|
|
235
|
+
commands: ['list', 'create', 'delete'],
|
|
236
|
+
},
|
|
237
|
+
order: 100,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export default function register(app) {
|
|
241
|
+
app.command('list', async () => ({ items: await listItems() }));
|
|
242
|
+
app.command('create <name>', async input => ({ item: await createItem(input.name) }));
|
|
243
|
+
app.command('delete <id>', async input => ({ item: await deleteItem(input.id) }));
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
声明 `agent` 后工具页可以把任务交给对话 Agent;`layout` 默认为 `drawer`,宿主会提供顶部入口。
|
|
248
|
+
页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace` / `inline`;
|
|
249
|
+
Agent 对话会保存在 Codex 会话历史中。带固定
|
|
250
|
+
18rem 左侧导航的工作台可默认使用 `workspace`,由导航项切换 Agent,并占满导航栏以外的主内容区;详情页仍可按需唤起右侧抽屉。
|
|
251
|
+
`inline` 用于把 Agent 嵌入工具页中的卡片或分区,调用时同时传入作为占位区域的 `anchor` 元素和稳定的
|
|
252
|
+
`key`;宿主会跟随该元素的滚动与尺寸变化同步面板位置。`key` 也可用于抽屉或工作区布局,为不同业务对象
|
|
253
|
+
隔离会话与输入草稿;`setAgentPanelVisible` 可同时传入绝对路径 `cwd`,让首次创建的会话直接绑定该目录。
|
|
254
|
+
传入 `context` 可为面板提供不显示为聊天消息的背景上下文;它会在用户首次发送消息、创建会话时作为
|
|
255
|
+
developer instructions 交给 Agent。
|
|
256
|
+
|
|
257
|
+
页面通过 `await window.mearl.ready()` 取得 `{ locale, theme, title, fontSize }`,通过
|
|
258
|
+
`window.mearl.invoke(command, input?)` 调用 `commands` 白名单中的同一插件命令。页面运行在无扩展
|
|
259
|
+
API、默认不可联网的 sandbox iframe 中;Native Host 校验插件身份、命令白名单和消息大小。页面包按
|
|
260
|
+
内容哈希缓存并分块传输,因此不会增加 Popup 的启动开销。使用模块脚本时需先打包为不再包含 `import`
|
|
261
|
+
的单文件;React、Vue 等工程可以把构建后的 `index.html` 作为 `entry`,注册流程仍负责最终资源封装。
|
|
262
|
+
需要无构建的组件与状态管理时可声明 `page.runtime: 'preact'`,扩展会在页面完整性校验后注入固定的
|
|
263
|
+
Preact、Hooks 和 HTM 运行时。页面从 `window.mearl.ui` 读取 `html`、`render`、`useState` 等 API;
|
|
264
|
+
`window.mearl.ui.version` 仅用于诊断,插件不能选择运行时版本。
|
|
265
|
+
宿主会同步扩展主题、字号,并注入 `--mearl-bg-primary`、`--mearl-bg-secondary`、
|
|
266
|
+
`--mearl-text-primary`、`--mearl-text-secondary`、`--mearl-border`、`--mearl-bg-hover` 和
|
|
267
|
+
`--mearl-accent` 等语义变量;主操作按钮使用 `--mearl-action-primary`、
|
|
268
|
+
`--mearl-action-primary-hover` 和 `--mearl-action-primary-text`,以保持与扩展按钮一致。
|
|
269
|
+
|
|
189
270
|
需要 BUC 身份的内部 HTTP 接口通过官方 `buc` 插件调用。CLI 使用 `mearl buc request`,业务插件使用
|
|
190
271
|
`app.invoke('buc', { command: 'request', input })`;当前身份和人员资料分别使用 `whoami` 与 `profile`
|
|
191
272
|
子命令。BUC 插件按请求创建一次性凭据,只接受无 URL 凭据的 HTTPS 地址,过滤认证字段与 Cookie;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type InstalledMearlEnvironment = {
|
|
2
|
+
profile: 'local' | 'cloud' | 'mcp';
|
|
3
|
+
version: string;
|
|
4
|
+
};
|
|
5
|
+
interface ScheduleOptions {
|
|
6
|
+
intervalDays?: number | null;
|
|
7
|
+
packageRoot?: string | null;
|
|
8
|
+
now?: Date;
|
|
9
|
+
statePath?: string;
|
|
10
|
+
lockPath?: string;
|
|
11
|
+
workerPath?: string;
|
|
12
|
+
launch?: (worker: string, installedVersion: string, packageRoot: string, token: string) => void;
|
|
13
|
+
}
|
|
14
|
+
interface WorkerOptions {
|
|
15
|
+
fetchLatestVersion?: () => Promise<string>;
|
|
16
|
+
getInstalledEnvironments?: () => InstalledMearlEnvironment[];
|
|
17
|
+
update?: (version: string, packageRoot: string, profiles: InstalledMearlEnvironment['profile'][], updateClient: boolean) => Promise<void>;
|
|
18
|
+
now?: Date;
|
|
19
|
+
statePath?: string;
|
|
20
|
+
lockPath?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function isCliUpdateDue(lastCheckDate: string | undefined, now: Date, intervalDays: number): boolean;
|
|
23
|
+
export declare function isGlobalClientPackageRoot(packageRoot: string, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
|
|
24
|
+
/** Queue a detached check after the CLI command has finished. */
|
|
25
|
+
export declare function scheduleCliAutoUpdate(installedVersion: string, options?: ScheduleOptions): void;
|
|
26
|
+
export declare function setupUpdateCommand(version: string, packageRoot: string, profiles: InstalledMearlEnvironment['profile'][], updateClient: boolean): {
|
|
27
|
+
command: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
};
|
|
31
|
+
/** Runs only in the detached worker process. */
|
|
32
|
+
export declare function runCliAutoUpdate(installedVersion: string, packageRoot: string, token: string, options?: WorkerOptions): Promise<void>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { delimiter, dirname, join, resolve, win32 } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { isNewerRelease, parseReleaseVersion } from './check.js';
|
|
8
|
+
import { loadCliAutoUpdateIntervalDays } from './unifiedConfig.js';
|
|
9
|
+
const CONFIG_DIR = join(homedir(), '.mearl');
|
|
10
|
+
const STATE_PATH = join(CONFIG_DIR, 'cli-auto-update-state.json');
|
|
11
|
+
const LOCK_PATH = join(CONFIG_DIR, 'cli-auto-update.lock');
|
|
12
|
+
const LOCK_STALE_MS = 10 * 60_000;
|
|
13
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
14
|
+
const VERSION_URL = 'https://registry.npmjs.org/@mearl%2Fclient/latest';
|
|
15
|
+
function localDate(date) {
|
|
16
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
17
|
+
}
|
|
18
|
+
function dayNumber(value) {
|
|
19
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
20
|
+
if (!match)
|
|
21
|
+
return null;
|
|
22
|
+
const year = Number(match[1]);
|
|
23
|
+
const month = Number(match[2]);
|
|
24
|
+
const day = Number(match[3]);
|
|
25
|
+
const timestamp = Date.UTC(year, month - 1, day);
|
|
26
|
+
const date = new Date(timestamp);
|
|
27
|
+
return date.getUTCFullYear() === year &&
|
|
28
|
+
date.getUTCMonth() + 1 === month &&
|
|
29
|
+
date.getUTCDate() === day
|
|
30
|
+
? timestamp / 86_400_000
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
export function isCliUpdateDue(lastCheckDate, now, intervalDays) {
|
|
34
|
+
const previous = typeof lastCheckDate === 'string' ? dayNumber(lastCheckDate) : null;
|
|
35
|
+
const today = dayNumber(localDate(now));
|
|
36
|
+
return (previous === null || today === null || today < previous || today - previous >= intervalDays);
|
|
37
|
+
}
|
|
38
|
+
export function isGlobalClientPackageRoot(packageRoot, platform = process.platform, env = process.env) {
|
|
39
|
+
const resolvedRoot = platform === 'win32' ? win32.resolve(packageRoot) : resolve(packageRoot);
|
|
40
|
+
const normalized = resolvedRoot.replaceAll('\\', '/').toLowerCase();
|
|
41
|
+
if (!normalized.endsWith('/node_modules/@mearl/client'))
|
|
42
|
+
return false;
|
|
43
|
+
const pnpmGlobalRoot = /\/global\/\d+\/(?:\.pnpm\/.*\/)?node_modules\//.exec(normalized);
|
|
44
|
+
if (pnpmGlobalRoot) {
|
|
45
|
+
const pnpmHome = resolvedRoot.slice(0, pnpmGlobalRoot.index);
|
|
46
|
+
return existsSync(join(pnpmHome, platform === 'win32' ? 'mearl.cmd' : 'mearl'));
|
|
47
|
+
}
|
|
48
|
+
if (normalized.includes('/.volta/tools/image/packages/'))
|
|
49
|
+
return true;
|
|
50
|
+
const npmSuffix = '/lib/node_modules/@mearl/client';
|
|
51
|
+
if (platform !== 'win32' && normalized.endsWith(npmSuffix)) {
|
|
52
|
+
const prefix = resolvedRoot.slice(0, -npmSuffix.length);
|
|
53
|
+
try {
|
|
54
|
+
return (realpathSync(join(prefix, 'bin', 'mearl')) ===
|
|
55
|
+
realpathSync(join(packageRoot, 'dist', 'bin.js')));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const windowsNpmRoot = platform === 'win32' && env.APPDATA
|
|
62
|
+
? `${win32.resolve(env.APPDATA).replaceAll('\\', '/').toLowerCase()}/npm/node_modules/@mearl/client`
|
|
63
|
+
: null;
|
|
64
|
+
return (normalized.includes('/.bun/install/global/') ||
|
|
65
|
+
/\/(?:\.config\/yarn|\.yarn|yarn\/data|yarn)\/global\//.test(normalized) ||
|
|
66
|
+
(platform === 'win32' && normalized === windowsNpmRoot));
|
|
67
|
+
}
|
|
68
|
+
function globalSearchPath(pathValue) {
|
|
69
|
+
return pathValue
|
|
70
|
+
?.split(delimiter)
|
|
71
|
+
.filter(directory => !/(?:^|[\\/])node_modules[\\/]\.bin[\\/]?$/i.test(directory))
|
|
72
|
+
.join(delimiter);
|
|
73
|
+
}
|
|
74
|
+
function currentGlobalClientRoot() {
|
|
75
|
+
let directory = dirname(realpathSync(fileURLToPath(import.meta.url)));
|
|
76
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
77
|
+
try {
|
|
78
|
+
const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
79
|
+
if (manifest.name === '@mearl/client') {
|
|
80
|
+
return isGlobalClientPackageRoot(directory) ? directory : null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Continue toward the package root.
|
|
85
|
+
}
|
|
86
|
+
const parent = dirname(directory);
|
|
87
|
+
if (parent === directory)
|
|
88
|
+
break;
|
|
89
|
+
directory = parent;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function readState(statePath) {
|
|
94
|
+
try {
|
|
95
|
+
const value = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
96
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
97
|
+
? value
|
|
98
|
+
: {};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function writeState(statePath, state) {
|
|
105
|
+
mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
|
|
106
|
+
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
109
|
+
renameSync(temporary, statePath);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
try {
|
|
113
|
+
unlinkSync(temporary);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* No temporary file to remove. */
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function acquireLock(lockPath, token) {
|
|
122
|
+
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
123
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
124
|
+
try {
|
|
125
|
+
const descriptor = openSync(lockPath, 'wx', 0o600);
|
|
126
|
+
try {
|
|
127
|
+
writeFileSync(descriptor, token);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
closeSync(descriptor);
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'EEXIST')
|
|
136
|
+
throw error;
|
|
137
|
+
try {
|
|
138
|
+
if (Date.now() - statSync(lockPath).mtimeMs <= LOCK_STALE_MS)
|
|
139
|
+
return false;
|
|
140
|
+
unlinkSync(lockPath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
function releaseLock(lockPath, token) {
|
|
150
|
+
try {
|
|
151
|
+
if (readFileSync(lockPath, 'utf8') === token)
|
|
152
|
+
unlinkSync(lockPath);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// A missing lock does not affect the current command or update result.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function launchWorker(worker, version, packageRoot, token) {
|
|
159
|
+
const child = spawn(process.execPath, [worker, version, packageRoot, token], {
|
|
160
|
+
detached: true,
|
|
161
|
+
stdio: 'ignore',
|
|
162
|
+
windowsHide: true,
|
|
163
|
+
});
|
|
164
|
+
child.once('error', () => releaseLock(LOCK_PATH, token));
|
|
165
|
+
child.unref();
|
|
166
|
+
}
|
|
167
|
+
/** Queue a detached check after the CLI command has finished. */
|
|
168
|
+
export function scheduleCliAutoUpdate(installedVersion, options = {}) {
|
|
169
|
+
try {
|
|
170
|
+
const intervalDays = options.intervalDays === undefined ? loadCliAutoUpdateIntervalDays() : options.intervalDays;
|
|
171
|
+
if (intervalDays === null)
|
|
172
|
+
return;
|
|
173
|
+
const packageRoot = options.packageRoot === undefined ? currentGlobalClientRoot() : options.packageRoot;
|
|
174
|
+
if (!packageRoot)
|
|
175
|
+
return;
|
|
176
|
+
const now = options.now ?? new Date();
|
|
177
|
+
const statePath = options.statePath ?? STATE_PATH;
|
|
178
|
+
const lockPath = options.lockPath ?? LOCK_PATH;
|
|
179
|
+
if (!isCliUpdateDue(readState(statePath).lastCheckDate, now, intervalDays))
|
|
180
|
+
return;
|
|
181
|
+
const token = randomUUID();
|
|
182
|
+
if (!acquireLock(lockPath, token))
|
|
183
|
+
return;
|
|
184
|
+
let launched = false;
|
|
185
|
+
try {
|
|
186
|
+
if (!isCliUpdateDue(readState(statePath).lastCheckDate, now, intervalDays))
|
|
187
|
+
return;
|
|
188
|
+
const worker = options.workerPath ?? fileURLToPath(new URL('./autoUpdateWorker.js', import.meta.url));
|
|
189
|
+
if (!existsSync(worker))
|
|
190
|
+
return;
|
|
191
|
+
(options.launch ?? launchWorker)(worker, installedVersion, packageRoot, token);
|
|
192
|
+
launched = true;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
if (!launched)
|
|
196
|
+
releaseLock(lockPath, token);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Update scheduling must never change CLI behavior.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export function setupUpdateCommand(version, packageRoot, profiles, updateClient) {
|
|
204
|
+
if (profiles.length === 0 && !updateClient) {
|
|
205
|
+
throw new Error('An update target is required');
|
|
206
|
+
}
|
|
207
|
+
if (!parseReleaseVersion(version))
|
|
208
|
+
throw new Error('A valid Mearl release is required');
|
|
209
|
+
const [major, minor] = version.split('.');
|
|
210
|
+
const env = { ...process.env };
|
|
211
|
+
delete env.MEARL_AUTO_UPDATE_CLIENT_ROOT;
|
|
212
|
+
delete env.MEARL_AUTO_UPDATE_CLIENT_ONLY;
|
|
213
|
+
delete env.MEARL_AUTO_UPDATE_PROFILES;
|
|
214
|
+
env.MEARL_AUTO_UPDATE_SEARCH_PATH = globalSearchPath(env.PATH);
|
|
215
|
+
if (updateClient || profiles.some(profile => profile === 'local' || profile === 'cloud')) {
|
|
216
|
+
env.MEARL_AUTO_UPDATE_CLIENT_ROOT = packageRoot;
|
|
217
|
+
}
|
|
218
|
+
if (profiles.length === 0)
|
|
219
|
+
env.MEARL_AUTO_UPDATE_CLIENT_ONLY = '1';
|
|
220
|
+
else
|
|
221
|
+
env.MEARL_AUTO_UPDATE_PROFILES = profiles.join(',');
|
|
222
|
+
return {
|
|
223
|
+
command: 'npx',
|
|
224
|
+
args: [
|
|
225
|
+
'--yes',
|
|
226
|
+
`@mearl/setup@${version}`,
|
|
227
|
+
'update',
|
|
228
|
+
'--yes',
|
|
229
|
+
'--target-line',
|
|
230
|
+
`${major}.${minor}.x`,
|
|
231
|
+
'--skip-skill',
|
|
232
|
+
],
|
|
233
|
+
env,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function updateWithSetup(version, packageRoot, profiles, updateClient) {
|
|
237
|
+
const { command, args, env } = setupUpdateCommand(version, packageRoot, profiles, updateClient);
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const child = spawn(command, args, {
|
|
240
|
+
env,
|
|
241
|
+
stdio: 'ignore',
|
|
242
|
+
windowsHide: true,
|
|
243
|
+
shell: process.platform === 'win32',
|
|
244
|
+
});
|
|
245
|
+
const timeout = setTimeout(() => child.kill(), UPDATE_TIMEOUT_MS);
|
|
246
|
+
child.once('error', error => {
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
reject(error);
|
|
249
|
+
});
|
|
250
|
+
child.once('close', code => {
|
|
251
|
+
clearTimeout(timeout);
|
|
252
|
+
if (code === 0)
|
|
253
|
+
resolve();
|
|
254
|
+
else
|
|
255
|
+
reject(new Error(`Setup exited with code ${code ?? 'unknown'}`));
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function installedEnvironments() {
|
|
260
|
+
const versions = [];
|
|
261
|
+
const env = { ...process.env, PATH: globalSearchPath(process.env.PATH) };
|
|
262
|
+
const executables = [
|
|
263
|
+
{ profile: 'local', executable: 'mearl-native-host' },
|
|
264
|
+
{ profile: 'cloud', executable: 'mearl-cloud-server' },
|
|
265
|
+
{ profile: 'mcp', executable: 'mearl-mcp-server' },
|
|
266
|
+
];
|
|
267
|
+
for (const { profile, executable } of executables) {
|
|
268
|
+
try {
|
|
269
|
+
const result = spawnSync(executable, ['--version'], {
|
|
270
|
+
encoding: 'utf8',
|
|
271
|
+
timeout: 5_000,
|
|
272
|
+
maxBuffer: 1_024,
|
|
273
|
+
windowsHide: true,
|
|
274
|
+
shell: process.platform === 'win32',
|
|
275
|
+
env,
|
|
276
|
+
});
|
|
277
|
+
const version = result.stdout?.trim();
|
|
278
|
+
if (result.status === 0 && typeof version === 'string' && parseReleaseVersion(version)) {
|
|
279
|
+
versions.push({ profile, version });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Other installed environments can still be checked and updated.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return versions;
|
|
287
|
+
}
|
|
288
|
+
async function fetchLatestClientVersion() {
|
|
289
|
+
const response = await fetch(VERSION_URL, { signal: AbortSignal.timeout(2_000) });
|
|
290
|
+
if (!response.ok)
|
|
291
|
+
throw new Error(`Registry returned HTTP ${response.status}`);
|
|
292
|
+
const payload = (await response.json());
|
|
293
|
+
const version = payload.version;
|
|
294
|
+
if (typeof version !== 'string') {
|
|
295
|
+
throw new Error('Registry returned an invalid release version');
|
|
296
|
+
}
|
|
297
|
+
return version;
|
|
298
|
+
}
|
|
299
|
+
/** Runs only in the detached worker process. */
|
|
300
|
+
export async function runCliAutoUpdate(installedVersion, packageRoot, token, options = {}) {
|
|
301
|
+
const statePath = options.statePath ?? STATE_PATH;
|
|
302
|
+
const lockPath = options.lockPath ?? LOCK_PATH;
|
|
303
|
+
try {
|
|
304
|
+
writeState(statePath, {
|
|
305
|
+
...readState(statePath),
|
|
306
|
+
lastCheckDate: localDate(options.now ?? new Date()),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
releaseLock(lockPath, token);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let result;
|
|
314
|
+
try {
|
|
315
|
+
const version = await (options.fetchLatestVersion ?? fetchLatestClientVersion)();
|
|
316
|
+
if (!parseReleaseVersion(version))
|
|
317
|
+
throw new Error('Registry returned an invalid release version');
|
|
318
|
+
const environments = (options.getInstalledEnvironments ?? installedEnvironments)();
|
|
319
|
+
const installedVersions = [
|
|
320
|
+
installedVersion,
|
|
321
|
+
...environments.map(environment => environment.version),
|
|
322
|
+
];
|
|
323
|
+
const updateClient = isNewerRelease(version, installedVersion);
|
|
324
|
+
const staleProfiles = environments
|
|
325
|
+
.filter(environment => isNewerRelease(version, environment.version))
|
|
326
|
+
.map(environment => environment.profile);
|
|
327
|
+
const updateAvailable = updateClient || staleProfiles.length > 0;
|
|
328
|
+
const newerInstalled = installedVersions.some(installed => isNewerRelease(installed, version));
|
|
329
|
+
if (updateAvailable && newerInstalled) {
|
|
330
|
+
result = {
|
|
331
|
+
checkedAt: new Date().toISOString(),
|
|
332
|
+
status: 'skipped',
|
|
333
|
+
version,
|
|
334
|
+
error: 'An installed component is newer than the latest published release',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
else if (updateAvailable) {
|
|
338
|
+
await (options.update ?? updateWithSetup)(version, packageRoot, staleProfiles, updateClient);
|
|
339
|
+
result = { checkedAt: new Date().toISOString(), status: 'updated', version };
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
result = { checkedAt: new Date().toISOString(), status: 'current', version };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
result = {
|
|
347
|
+
checkedAt: new Date().toISOString(),
|
|
348
|
+
status: 'failed',
|
|
349
|
+
error: error instanceof Error ? error.message : String(error),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
writeState(statePath, { ...readState(statePath), lastResult: result });
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* The result is best effort. */
|
|
357
|
+
}
|
|
358
|
+
releaseLock(lockPath, token);
|
|
359
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runMearlCli } from './cli.js';
|
|
3
|
+
import { CLIENT_VERSION } from './socket.js';
|
|
4
|
+
import { scheduleCliAutoUpdate } from './autoUpdate.js';
|
|
3
5
|
process.exitCode = await runMearlCli(process.argv.slice(2));
|
|
6
|
+
scheduleCliAutoUpdate(CLIENT_VERSION);
|
package/dist/check.d.ts
CHANGED
|
@@ -67,6 +67,13 @@ export interface UnifiedCheckResult {
|
|
|
67
67
|
connectors: CloudConnectorInfo[];
|
|
68
68
|
environment: CheckItem[];
|
|
69
69
|
ok: boolean;
|
|
70
|
+
updates?: UnifiedCheckUpdate[];
|
|
71
|
+
}
|
|
72
|
+
export interface UnifiedCheckUpdate {
|
|
73
|
+
component: string;
|
|
74
|
+
installed: string;
|
|
75
|
+
latest?: string;
|
|
76
|
+
status: 'current' | 'available' | 'unavailable';
|
|
70
77
|
}
|
|
71
78
|
interface CheckClient {
|
|
72
79
|
invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
@@ -82,12 +89,16 @@ export interface UnifiedCheckOptions {
|
|
|
82
89
|
local?: boolean;
|
|
83
90
|
transport?: 'cdp';
|
|
84
91
|
json?: boolean;
|
|
92
|
+
checkUpdates?: boolean;
|
|
85
93
|
}
|
|
86
94
|
export interface UnifiedCheckDependencies {
|
|
87
95
|
createClient?: (options: UnifiedClientOptions) => CheckClient;
|
|
88
96
|
writeJson?: (json: string) => void;
|
|
89
97
|
writeLine?: (line: string) => void;
|
|
98
|
+
fetchLatestVersion?: (component: string) => Promise<string>;
|
|
90
99
|
}
|
|
100
|
+
export declare function parseReleaseVersion(value: unknown): number[] | null;
|
|
101
|
+
export declare function isNewerRelease(latest: string, installed: string): boolean;
|
|
91
102
|
export declare function parseCheckTimeout(value: string | undefined): number | null;
|
|
92
103
|
export declare function runUnifiedCheck(options: UnifiedCheckOptions & {
|
|
93
104
|
json: true;
|