@mearl/client 2.18.0 → 2.19.1
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 +38 -9
- 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 +8 -1
- package/dist/commands.d.ts +31 -5
- package/dist/commands.js +68 -13
- package/dist/generated-browser-action-protocol.d.ts +27 -9
- package/dist/generated-browser-action-protocol.js +12 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +9 -6
- package/dist/officialPluginCatalog.d.ts +5 -1
- package/dist/officialPluginCatalog.js +5 -1
- package/dist/pluginPageBundle.js +265 -5
- package/dist/unifiedConfig.d.ts +2 -0
- package/dist/unifiedConfig.js +23 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ pnpm add @mearl/client
|
|
|
18
18
|
import * as client from '@mearl/client';
|
|
19
19
|
|
|
20
20
|
// 具名方法(类型安全)
|
|
21
|
-
const requests = await client.getRequests({ count: 5 });
|
|
21
|
+
const requests = await client.getRequests({ tabId: 12345, count: 5 });
|
|
22
22
|
const result = await client.sendRequest({
|
|
23
23
|
url: 'https://api.example.com',
|
|
24
24
|
method: 'GET',
|
|
@@ -30,7 +30,7 @@ const data = await client.invoke('send_request', { url: '...' });
|
|
|
30
30
|
|
|
31
31
|
// browser_list 默认聚合本机和所有远端 connector
|
|
32
32
|
const browsers = await client.browserList();
|
|
33
|
-
await client.getLogs({}, { browser: browsers.browsers[0].browserId });
|
|
33
|
+
await client.getLogs({ tabId: 12345 }, { browser: browsers.browsers[0].browserId });
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## CLI 使用
|
|
@@ -39,7 +39,7 @@ await client.getLogs({}, { browser: browsers.browsers[0].browserId });
|
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
# 获取最近 5 个请求
|
|
42
|
-
mearl get_requests --payload '{"count":5}'
|
|
42
|
+
mearl get_requests --payload '{"tabId":12345,"count":5}'
|
|
43
43
|
|
|
44
44
|
# 代理一个 HTTP 请求
|
|
45
45
|
mearl send_request --payload '{"url":"https://api.example.com","method":"GET"}'
|
|
@@ -50,7 +50,7 @@ mearl send_request --payload '{"url":"https://api.example.com/upload","method":"
|
|
|
50
50
|
# 截图并保存(tabId 来自 tab_open 或 tab_list)
|
|
51
51
|
mearl page_screenshot --payload '{"tabId":12345}' --output ./screenshot.png
|
|
52
52
|
|
|
53
|
-
#
|
|
53
|
+
# 从文件读取大体积参数(mock-data.json 需包含 tabId)
|
|
54
54
|
mearl set_mock --payload-file ./mock-data.json
|
|
55
55
|
|
|
56
56
|
# 环境自检
|
|
@@ -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,8 @@ 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 install trip/mearl --skill authx-request --yes
|
|
162
|
+
npx @mearl/setup plugin /absolute/path/to/yuque-doc-fetch
|
|
140
163
|
npx @mearl/setup plugin uninstall yuque
|
|
141
164
|
mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
|
|
142
165
|
mearl yuque --payload '{"command":"read","input":{"url":"https://aliyuque.antfin.com/group/book/doc"}}'
|
|
@@ -151,7 +174,7 @@ Provider 与插件仍可独立安装、更新和卸载,帮助聚合不会启
|
|
|
151
174
|
目录默认使用 `scripts/main.mjs` 或 `scripts/main.py`;两者同时存在时使用 JavaScript 入口。JavaScript 入口默认导出 `register(app)`,Python 入口导出 `register(app)` 函数;两种入口都可声明简短的 `name` 和 `description`,并通过 `app.command(signature, handler, description?)`
|
|
152
175
|
注册动作、`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
176
|
`--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
|
|
154
|
-
|
|
177
|
+
注册名优先使用显式名称,其次读取入口的 `name` metadata,最后回退到 skill 目录名;常规注册只传路径。
|
|
155
178
|
setup 会补齐缺失或低于自身版本的 client、native-host;安装参数透传给 Ali Skills,并固定全局共享安装。
|
|
156
179
|
|
|
157
180
|
需要单独检查插件时运行 `mearl <name> --version`。命令会解析注册记录并加载入口,但不连接浏览器;
|
|
@@ -176,7 +199,7 @@ const result = await invokePlugin(
|
|
|
176
199
|
);
|
|
177
200
|
```
|
|
178
201
|
|
|
179
|
-
MCP Server 通过固定工具 `
|
|
202
|
+
MCP Server 通过固定工具 `plugin_invoke` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP 不接受任意入口路径;调用尚未注册的 `buc`、`authx` 或 `tdbank` 时,会通过匹配当前 client 版本的 setup 自动安装并重试一次。其他未注册名称返回 `PLUGIN_NOT_REGISTERED`。
|
|
180
203
|
|
|
181
204
|
注册记录在 `~/.mearl/plugins/<name>.json`,保存入口路径和可选的简短描述;skill 业务代码原地更新直接生效,换路径或更新描述后重新注册。
|
|
182
205
|
`plugin uninstall <name>` 会对 `~/.agents/skills` 中的全局 Skill 调用 `ali-skills remove --global --yes`,
|
|
@@ -223,8 +246,14 @@ export default function register(app) {
|
|
|
223
246
|
```
|
|
224
247
|
|
|
225
248
|
声明 `agent` 后工具页可以把任务交给对话 Agent;`layout` 默认为 `drawer`,宿主会提供顶部入口。
|
|
226
|
-
页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace
|
|
249
|
+
页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace` / `inline`;
|
|
250
|
+
Agent 对话会保存在 Codex 会话历史中。带固定
|
|
227
251
|
18rem 左侧导航的工作台可默认使用 `workspace`,由导航项切换 Agent,并占满导航栏以外的主内容区;详情页仍可按需唤起右侧抽屉。
|
|
252
|
+
`inline` 用于把 Agent 嵌入工具页中的卡片或分区,调用时同时传入作为占位区域的 `anchor` 元素和稳定的
|
|
253
|
+
`key`;宿主会跟随该元素的滚动与尺寸变化同步面板位置。`key` 也可用于抽屉或工作区布局,为不同业务对象
|
|
254
|
+
隔离会话与输入草稿;`setAgentPanelVisible` 可同时传入绝对路径 `cwd`,让首次创建的会话直接绑定该目录。
|
|
255
|
+
传入 `context` 可为面板提供不显示为聊天消息的背景上下文;它会在用户首次发送消息、创建会话时作为
|
|
256
|
+
developer instructions 交给 Agent。
|
|
228
257
|
|
|
229
258
|
页面通过 `await window.mearl.ready()` 取得 `{ locale, theme, title, fontSize }`,通过
|
|
230
259
|
`window.mearl.invoke(command, input?)` 调用 `commands` 白名单中的同一插件命令。页面运行在无扩展
|
|
@@ -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;
|