@mearl/setup 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,7 @@ Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全
5
5
  ```bash
6
6
  npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
7
7
  npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
8
+ npx @mearl/setup extension [--force] [--yes]
8
9
  ```
9
10
 
10
11
  配置 AgentBay API Key 时只需执行一次下面的命令,然后在隐藏输入提示中粘贴 Key。
@@ -44,6 +45,42 @@ npx @mearl/setup agentbay
44
45
  可用 `--skip-skill` 跳过。
45
46
  - 卸载一个环境但另一个仍依赖全局 mearl skill 时会保留 skill。
46
47
  - `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
48
+ - 默认的 `install` / `update` 不处理浏览器扩展。`extension` 是独立命令:从
49
+ `https://mearl.alibaba-inc.com/` 识别并下载最新的 `mearl-chrome-v*.zip`,原子解压到
50
+ `~/.mearl/chrome-extension`。首次安装通过浏览器级 CDP 的 `Extensions.loadUnpacked`
51
+ 加载;后续更新由 `mearl check --local --json` 确认当前扩展仍来自 setup 管理目录后,直接替换
52
+ 文件并通过 Native Host 触发扩展 reload。固定目录和 manifest 中的 `key` 保证扩展 ID
53
+ 与浏览器存储保持稳定。
54
+ - 下载前会通过 CDP 检查同 ID 插件的来源。未安装,或来源为固定目录中的 `UNPACKED`
55
+ 插件时可继续;Chrome 商店、企业策略或其他本地目录安装的版本默认保持不变。只有显式
56
+ 传入 `--force` 才会接管并替换这些版本。
57
+
58
+ ## 安装或更新浏览器扩展
59
+
60
+ ```bash
61
+ npx @mearl/setup extension
62
+ ```
63
+
64
+ 首次安装、旧版 setup 扩展首次迁移,或 Native Host reload 不可用时才需要 CDP。如果
65
+ Chrome 尚未开放 CDP,交互命令会提示打开
66
+ `chrome://inspect/#remote-debugging`、启用远程调试并允许连接,然后等待安装完成。无 TTY
67
+ 环境使用 `npx @mearl/setup extension --yes`;它只尝试一次。无法确认 setup 管理来源且
68
+ CDP 不可用时不会下载或修改插件目录,打开 CDP 后重新执行同一命令即可。
69
+
70
+ 该命令每次都读取下载页上的最新插件版本,不要求与 setup、CLI 或 Native Host 同版本。
71
+ CDP 来源检查和 `Extensions.loadUnpacked` 复用同一条连接,避免一次安装重复请求授权。
72
+ 安装成功后目录内会保存 setup 管理标记;`mearl check --local --json` 同时从当前运行扩展读取该
73
+ 标记,只有两侧路径一致时才允许无 CDP 更新。如果用户后来改装 Chrome 商店版本,运行中
74
+ 的扩展不会返回 setup 标记,命令会重新通过 CDP 检查来源,并在未传 `--force` 时保持商店
75
+ 版本不变。
76
+
77
+ setup 管理的扩展会读取远端 `config.json` 检查新版本,但不会静默更新。发现更高版本后,
78
+ DevTools 面板的设置按钮会显示红点,快捷设置中提供“立即更新”和对应版本的“更新日志”。
79
+ 只有用户点击“立即更新”后,Native Host 才会重新校验远端版本、setup 管理标记、下载地址、
80
+ 可选的 SHA256 和压缩包 manifest,再原子替换目录并 reload。该入口不会回退到 CDP;商店版、
81
+ 企业策略版和其他本地目录版本不会进入 setup 更新链路。对于 Chrome 自身管理并已下载完成的
82
+ 更新,扩展只被动监听 `runtime.onUpdateAvailable`,沿用相同的更新提示并由用户点击 reload;不会
83
+ 调用 `runtime.requestUpdateCheck()` 主动检查。
47
84
 
48
85
  ## 跟随浏览器扩展自动更新
49
86
 
@@ -87,6 +124,9 @@ npx @mearl/setup --local --cloud
87
124
  # 自动更新当前 PATH 中检测到的全部 Mearl 环境
88
125
  npx @mearl/setup update
89
126
 
127
+ # 独立安装或更新到最新浏览器扩展
128
+ npx @mearl/setup extension
129
+
90
130
  # 只卸载本地环境
91
131
  npx @mearl/setup uninstall --local
92
132
 
@@ -0,0 +1,78 @@
1
+ export declare const MEARL_EXTENSION_DOWNLOAD_PAGE = "https://mearl.alibaba-inc.com/";
2
+ export declare const MEARL_EXTENSION_REMOTE_CONFIG_URL = "https://mearl.alibaba-inc.com/config.json";
3
+ export declare const MEARL_EXTENSION_DIRECTORY_NAME = "chrome-extension";
4
+ export declare const MEARL_EXTENSION_ID = "aoehhjnofngknnjefamjbplchbolghkm";
5
+ export declare const MEARL_EXTENSION_SETUP_MARKER = ".mearl-setup.json";
6
+ export interface BrowserExtensionRelease {
7
+ archiveUrl: string;
8
+ sha256?: string;
9
+ version: string;
10
+ }
11
+ export interface BrowserExtensionInstallOptions {
12
+ force?: boolean;
13
+ targetDirectory?: string;
14
+ chromeUserDataDirectory?: string;
15
+ waitForCdpMs?: number;
16
+ writeLine?: (line: string) => void;
17
+ release?: BrowserExtensionRelease;
18
+ }
19
+ export interface SetupManagedBrowserExtensionUpdateOptions {
20
+ release: BrowserExtensionRelease;
21
+ targetDirectory: string;
22
+ }
23
+ export interface SetupManagedBrowserExtensionUpdateResult extends BrowserExtensionRelease {
24
+ path: string;
25
+ }
26
+ export interface BrowserExtensionInstallResult {
27
+ activation?: 'cdp' | 'native-host';
28
+ extensionId?: string;
29
+ filesUpdated?: boolean;
30
+ loaded: boolean;
31
+ path: string;
32
+ version: string;
33
+ archiveUrl: string;
34
+ cdpError?: string;
35
+ }
36
+ export interface BrowserExtensionDependencies {
37
+ connectCdp?: typeof connectBrowserExtensionViaCdp;
38
+ extractArchive?: (archivePath: string, destination: string) => Promise<void>;
39
+ fetch?: typeof fetch;
40
+ inspectInstalled?: typeof inspectSetupManagedExtension;
41
+ inspectInstallation?: typeof inspectMearlExtensionViaCdp;
42
+ loadUnpacked?: typeof loadUnpackedViaCdp;
43
+ reloadInstalled?: typeof reloadInstalledExtension;
44
+ sleep?: (milliseconds: number) => Promise<void>;
45
+ }
46
+ export interface InstalledBrowserExtension {
47
+ id: string;
48
+ location: string;
49
+ name: string;
50
+ path: string;
51
+ version: string;
52
+ }
53
+ export declare class BrowserExtensionConflictError extends Error {
54
+ readonly installed: InstalledBrowserExtension;
55
+ constructor(installed: InstalledBrowserExtension);
56
+ }
57
+ export declare function resolveChromeUserDataDirectory(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string;
58
+ export declare function resolveMearlExtensionDirectory(home?: string): string;
59
+ export declare function findLatestExtensionVersion(html: string): string | null;
60
+ export declare function extensionArchiveUrl(version: string): string;
61
+ export declare function fetchBrowserExtensionRelease(fetchImpl?: typeof fetch): Promise<BrowserExtensionRelease>;
62
+ interface BrowserExtensionCdpSession {
63
+ close(): void;
64
+ inspectInstallation(): Promise<InstalledBrowserExtension | null>;
65
+ loadUnpacked(extensionDirectory: string): Promise<{
66
+ extensionId: string;
67
+ }>;
68
+ }
69
+ declare function connectBrowserExtensionViaCdp(chromeUserDataDirectory?: string): Promise<BrowserExtensionCdpSession>;
70
+ export declare function loadUnpackedViaCdp(extensionDirectory: string, chromeUserDataDirectory?: string): Promise<{
71
+ extensionId: string;
72
+ }>;
73
+ export declare function inspectMearlExtensionViaCdp(chromeUserDataDirectory?: string): Promise<InstalledBrowserExtension | null>;
74
+ declare function inspectSetupManagedExtension(targetDirectory: string): Promise<string[]>;
75
+ declare function reloadInstalledExtension(browserIds: string[]): Promise<void>;
76
+ export declare function updateSetupManagedBrowserExtensionFiles(options: SetupManagedBrowserExtensionUpdateOptions, dependencies?: Pick<BrowserExtensionDependencies, 'extractArchive' | 'fetch'>): Promise<SetupManagedBrowserExtensionUpdateResult>;
77
+ export declare function installMearlBrowserExtension(options?: BrowserExtensionInstallOptions, dependencies?: BrowserExtensionDependencies): Promise<BrowserExtensionInstallResult>;
78
+ export {};
@@ -0,0 +1,612 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { runUnifiedCheck } from '@mearl/client/check';
6
+ import { CLIENT_VERSION, invoke as invokeLocalMearl } from '@mearl/client/local';
7
+ import extractZip from 'extract-zip';
8
+ import WebSocket from 'ws';
9
+ import { compareMearlReleaseVersions, isMearlReleaseVersion } from './version.js';
10
+ export const MEARL_EXTENSION_DOWNLOAD_PAGE = 'https://mearl.alibaba-inc.com/';
11
+ export const MEARL_EXTENSION_REMOTE_CONFIG_URL = `${MEARL_EXTENSION_DOWNLOAD_PAGE}config.json`;
12
+ export const MEARL_EXTENSION_DIRECTORY_NAME = 'chrome-extension';
13
+ export const MEARL_EXTENSION_ID = 'aoehhjnofngknnjefamjbplchbolghkm';
14
+ export const MEARL_EXTENSION_SETUP_MARKER = '.mearl-setup.json';
15
+ const ARCHIVE_PATTERN = /mearl-chrome-v(\d+\.\d+\.\d+(?:-beta\.\d+)?)\.zip/g;
16
+ const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
17
+ const SHA256_PATTERN = /^[a-f\d]{64}$/i;
18
+ export class BrowserExtensionConflictError extends Error {
19
+ installed;
20
+ constructor(installed) {
21
+ super(`Mearl extension ${installed.id} is already installed from ${installed.location} at ${installed.path || '(managed location)'}. Re-run with --force to replace it.`);
22
+ this.installed = installed;
23
+ this.name = 'BrowserExtensionConflictError';
24
+ }
25
+ }
26
+ export function resolveChromeUserDataDirectory(platform = process.platform, home = os.homedir(), env = process.env) {
27
+ if (platform === 'darwin') {
28
+ return path.join(home, 'Library', 'Application Support', 'Google', 'Chrome');
29
+ }
30
+ if (platform === 'win32') {
31
+ return path.join(env.LOCALAPPDATA ?? home, 'Google', 'Chrome', 'User Data');
32
+ }
33
+ return path.join(home, '.config', 'google-chrome');
34
+ }
35
+ export function resolveMearlExtensionDirectory(home = os.homedir()) {
36
+ return path.join(home, '.mearl', MEARL_EXTENSION_DIRECTORY_NAME);
37
+ }
38
+ export function findLatestExtensionVersion(html) {
39
+ const versions = Array.from(html.matchAll(ARCHIVE_PATTERN), match => match[1]).filter(isMearlReleaseVersion);
40
+ return versions.sort(compareMearlReleaseVersions).at(-1) ?? null;
41
+ }
42
+ export function extensionArchiveUrl(version) {
43
+ if (!isMearlReleaseVersion(version)) {
44
+ throw new Error(`Invalid Mearl browser extension version: ${version}`);
45
+ }
46
+ return new URL(`mearl-chrome-v${version}.zip`, MEARL_EXTENSION_DOWNLOAD_PAGE).toString();
47
+ }
48
+ function validateExtensionRelease(release) {
49
+ if (!isMearlReleaseVersion(release.version)) {
50
+ throw new Error('Remote extension config has an invalid latestVersion');
51
+ }
52
+ const expectedArchiveUrl = extensionArchiveUrl(release.version);
53
+ let archiveUrl;
54
+ try {
55
+ archiveUrl = new URL(release.archiveUrl).toString();
56
+ }
57
+ catch {
58
+ throw new Error('Remote extension config has an invalid downloadUrl');
59
+ }
60
+ if (archiveUrl !== expectedArchiveUrl) {
61
+ throw new Error('Remote extension downloadUrl does not match latestVersion');
62
+ }
63
+ if (release.sha256 !== undefined && !SHA256_PATTERN.test(release.sha256)) {
64
+ throw new Error('Remote extension config has an invalid downloadSha256');
65
+ }
66
+ return {
67
+ archiveUrl,
68
+ ...(release.sha256 ? { sha256: release.sha256.toLowerCase() } : {}),
69
+ version: release.version,
70
+ };
71
+ }
72
+ export async function fetchBrowserExtensionRelease(fetchImpl = fetch) {
73
+ const response = await fetchImpl(MEARL_EXTENSION_REMOTE_CONFIG_URL, { cache: 'no-store' });
74
+ if (!response.ok) {
75
+ throw new Error(`Extension config returned HTTP ${response.status}`);
76
+ }
77
+ const config = (await response.json());
78
+ return validateExtensionRelease({
79
+ archiveUrl: typeof config.downloadUrl === 'string' ? config.downloadUrl : '',
80
+ ...(typeof config.downloadSha256 === 'string' ? { sha256: config.downloadSha256 } : {}),
81
+ version: typeof config.latestVersion === 'string' ? config.latestVersion : '',
82
+ });
83
+ }
84
+ async function resolveExtensionArchive(fetchImpl) {
85
+ const response = await fetchImpl(MEARL_EXTENSION_DOWNLOAD_PAGE, { redirect: 'follow' });
86
+ if (!response.ok) {
87
+ throw new Error(`Extension release page returned HTTP ${response.status}`);
88
+ }
89
+ const version = findLatestExtensionVersion(await response.text());
90
+ if (!version)
91
+ throw new Error('No Mearl browser extension archive was found on the release page');
92
+ return { version, archiveUrl: extensionArchiveUrl(version) };
93
+ }
94
+ function readDevToolsActivePort(userDataDirectory) {
95
+ try {
96
+ const content = fs
97
+ .readFileSync(path.join(userDataDirectory, 'DevToolsActivePort'), 'utf8')
98
+ .trim();
99
+ const [portValue, browserWsPath = '/devtools/browser'] = content
100
+ .split(/\r?\n/)
101
+ .map(value => value.trim());
102
+ const port = Number(portValue);
103
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535)
104
+ return null;
105
+ if (!browserWsPath.startsWith('/'))
106
+ return null;
107
+ return { port, browserWsPath };
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ function rawDataToString(data) {
114
+ if (Array.isArray(data))
115
+ return Buffer.concat(data).toString('utf8');
116
+ if (Buffer.isBuffer(data))
117
+ return data.toString('utf8');
118
+ return Buffer.from(data).toString('utf8');
119
+ }
120
+ class CdpConnection {
121
+ socket;
122
+ closed = false;
123
+ nextId = 1;
124
+ pending = new Map();
125
+ constructor(socket) {
126
+ this.socket = socket;
127
+ socket.on('message', data => this.handleMessage(data));
128
+ socket.on('close', () => this.close(new Error('CDP WebSocket closed before responding')));
129
+ socket.on('error', error => this.close(error));
130
+ }
131
+ static connect(endpoint) {
132
+ return new Promise((resolve, reject) => {
133
+ const socket = new WebSocket(endpoint, {
134
+ handshakeTimeout: 5_000,
135
+ maxPayload: 8 * 1024 * 1024,
136
+ perMessageDeflate: false,
137
+ });
138
+ let settled = false;
139
+ const finish = (error) => {
140
+ if (settled)
141
+ return;
142
+ settled = true;
143
+ if (error) {
144
+ socket.terminate();
145
+ reject(error);
146
+ }
147
+ else {
148
+ resolve(new CdpConnection(socket));
149
+ }
150
+ };
151
+ socket.once('open', () => finish());
152
+ socket.once('error', error => finish(error));
153
+ socket.once('unexpected-response', (_request, response) => {
154
+ response.resume();
155
+ finish(new Error(`CDP WebSocket returned HTTP ${response.statusCode ?? 0}`));
156
+ });
157
+ });
158
+ }
159
+ send(method, params = {}, sessionId, timeoutMs = 10_000) {
160
+ return new Promise((resolve, reject) => {
161
+ const id = this.nextId++;
162
+ const timer = setTimeout(() => {
163
+ this.pending.delete(id);
164
+ reject(new Error(`CDP command timed out: ${method}`));
165
+ }, timeoutMs);
166
+ this.pending.set(id, { reject, resolve, timer });
167
+ this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }), error => {
168
+ if (!error)
169
+ return;
170
+ const pending = this.pending.get(id);
171
+ if (!pending)
172
+ return;
173
+ clearTimeout(pending.timer);
174
+ this.pending.delete(id);
175
+ pending.reject(error);
176
+ });
177
+ });
178
+ }
179
+ close(error = new Error('CDP connection closed')) {
180
+ if (this.closed)
181
+ return;
182
+ this.closed = true;
183
+ for (const pending of this.pending.values()) {
184
+ clearTimeout(pending.timer);
185
+ pending.reject(error);
186
+ }
187
+ this.pending.clear();
188
+ this.socket.terminate();
189
+ }
190
+ handleMessage(data) {
191
+ try {
192
+ const message = JSON.parse(rawDataToString(data));
193
+ if (message.id === undefined)
194
+ return;
195
+ const pending = this.pending.get(message.id);
196
+ if (!pending)
197
+ return;
198
+ clearTimeout(pending.timer);
199
+ this.pending.delete(message.id);
200
+ if (message.error) {
201
+ pending.reject(new Error(message.error.message ?? 'CDP command failed'));
202
+ }
203
+ else {
204
+ pending.resolve(message.result ?? {});
205
+ }
206
+ }
207
+ catch {
208
+ // Ignore unrelated malformed protocol events and let command timeouts report failure.
209
+ }
210
+ }
211
+ }
212
+ function sameExtensionPath(left, right) {
213
+ const normalizedLeft = path.normalize(path.resolve(left));
214
+ const normalizedRight = path.normalize(path.resolve(right));
215
+ return process.platform === 'win32'
216
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
217
+ : normalizedLeft === normalizedRight;
218
+ }
219
+ function isSetupManagedInstallation(installed, targetDirectory) {
220
+ return installed.location === 'UNPACKED' && sameExtensionPath(installed.path, targetDirectory);
221
+ }
222
+ function setupMarkerPath(directory) {
223
+ return path.join(directory, MEARL_EXTENSION_SETUP_MARKER);
224
+ }
225
+ function hasSetupMarker(targetDirectory) {
226
+ try {
227
+ const marker = JSON.parse(fs.readFileSync(setupMarkerPath(targetDirectory), 'utf8'));
228
+ return (marker.schemaVersion === 1 &&
229
+ marker.extensionId === MEARL_EXTENSION_ID &&
230
+ typeof marker.path === 'string' &&
231
+ sameExtensionPath(marker.path, targetDirectory));
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ async function writeSetupMarker(directory, targetDirectory) {
238
+ const marker = {
239
+ extensionId: MEARL_EXTENSION_ID,
240
+ path: path.resolve(targetDirectory),
241
+ schemaVersion: 1,
242
+ };
243
+ await fs.promises.writeFile(setupMarkerPath(directory), `${JSON.stringify(marker, null, 2)}\n`, {
244
+ mode: 0o600,
245
+ });
246
+ }
247
+ function browserCdpEndpoint(chromeUserDataDirectory) {
248
+ const activePort = readDevToolsActivePort(chromeUserDataDirectory);
249
+ if (!activePort)
250
+ throw new Error('Chrome remote debugging is not enabled');
251
+ return `ws://127.0.0.1:${activePort.port}${activePort.browserWsPath}`;
252
+ }
253
+ async function inspectInstallationWithConnection(connection) {
254
+ const target = await connection.send('Target.createTarget', {
255
+ background: true,
256
+ url: 'chrome://extensions-internals',
257
+ });
258
+ try {
259
+ const attached = await connection.send('Target.attachToTarget', {
260
+ flatten: true,
261
+ targetId: target.targetId,
262
+ });
263
+ const evaluated = await connection.send('Runtime.evaluate', {
264
+ awaitPromise: true,
265
+ expression: "document.readyState === 'complete' ? document.body.innerText : new Promise(resolve => addEventListener('load', () => resolve(document.body.innerText), {once:true}))",
266
+ returnByValue: true,
267
+ }, attached.sessionId);
268
+ const text = evaluated.result?.value;
269
+ if (typeof text !== 'string') {
270
+ throw new Error('chrome://extensions-internals did not return extension inventory');
271
+ }
272
+ const inventory = JSON.parse(text);
273
+ const installed = inventory.find(extension => extension.id === MEARL_EXTENSION_ID);
274
+ if (!installed)
275
+ return null;
276
+ if (typeof installed.location !== 'string' ||
277
+ typeof installed.name !== 'string' ||
278
+ typeof installed.path !== 'string' ||
279
+ typeof installed.version !== 'string') {
280
+ throw new Error('Chrome returned incomplete Mearl extension metadata');
281
+ }
282
+ return {
283
+ id: MEARL_EXTENSION_ID,
284
+ location: installed.location,
285
+ name: installed.name,
286
+ path: installed.path,
287
+ version: installed.version,
288
+ };
289
+ }
290
+ finally {
291
+ if (typeof target.targetId === 'string') {
292
+ await connection.send('Target.closeTarget', { targetId: target.targetId }).catch(() => { });
293
+ }
294
+ }
295
+ }
296
+ async function loadUnpackedWithConnection(connection, extensionDirectory) {
297
+ const loaded = await connection.send('Extensions.loadUnpacked', {
298
+ path: path.resolve(extensionDirectory),
299
+ });
300
+ if (loaded.id !== MEARL_EXTENSION_ID) {
301
+ throw new Error(`Extensions.loadUnpacked returned unexpected extension ID: ${String(loaded.id)}`);
302
+ }
303
+ const inventory = await connection.send('Extensions.getExtensions');
304
+ const extension = Array.isArray(inventory.extensions)
305
+ ? inventory.extensions.find(candidate => candidate?.id === loaded.id)
306
+ : null;
307
+ if (!extension?.enabled) {
308
+ throw new Error('Chrome loaded the extension but did not report it as enabled');
309
+ }
310
+ return { extensionId: loaded.id };
311
+ }
312
+ async function connectBrowserExtensionViaCdp(chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
313
+ const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
314
+ return {
315
+ close: () => connection.close(),
316
+ inspectInstallation: () => inspectInstallationWithConnection(connection),
317
+ loadUnpacked: extensionDirectory => loadUnpackedWithConnection(connection, extensionDirectory),
318
+ };
319
+ }
320
+ export async function loadUnpackedViaCdp(extensionDirectory, chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
321
+ const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
322
+ try {
323
+ return await loadUnpackedWithConnection(connection, extensionDirectory);
324
+ }
325
+ finally {
326
+ connection.close();
327
+ }
328
+ }
329
+ export async function inspectMearlExtensionViaCdp(chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
330
+ const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
331
+ try {
332
+ return await inspectInstallationWithConnection(connection);
333
+ }
334
+ finally {
335
+ connection.close();
336
+ }
337
+ }
338
+ async function inspectSetupManagedExtension(targetDirectory) {
339
+ const result = await runUnifiedCheck({ clientVersion: CLIENT_VERSION, json: true, local: true, timeoutSec: 5 }, { writeJson: () => { } });
340
+ const matches = result.browsers
341
+ .filter(browser => browser.status === 'connected' &&
342
+ browser.transport === 'extension' &&
343
+ browser.extensionInstallation?.extensionId === MEARL_EXTENSION_ID &&
344
+ typeof browser.extensionInstallation.setupManagedPath === 'string' &&
345
+ sameExtensionPath(browser.extensionInstallation.setupManagedPath, targetDirectory))
346
+ .map(browser => browser.browserId);
347
+ return matches;
348
+ }
349
+ async function reloadInstalledExtension(browserIds) {
350
+ if (browserIds.length === 0)
351
+ throw new Error('No setup-managed Mearl extension was found');
352
+ const failures = [];
353
+ let reloaded = 0;
354
+ for (const browserId of browserIds) {
355
+ try {
356
+ await invokeLocalMearl('__extension_reload', {}, {
357
+ browser: browserId,
358
+ timeoutSec: 5,
359
+ });
360
+ reloaded += 1;
361
+ }
362
+ catch (error) {
363
+ failures.push(error instanceof Error ? error.message : String(error));
364
+ }
365
+ }
366
+ if (reloaded === 0) {
367
+ throw new Error(`Failed to reload the connected extension: ${failures.join('; ')}`);
368
+ }
369
+ }
370
+ async function replaceDirectory(stagingDirectory, targetDirectory) {
371
+ const backupDirectory = `${targetDirectory}.backup-${randomUUID()}`;
372
+ const targetExists = fs.existsSync(targetDirectory);
373
+ try {
374
+ if (targetExists)
375
+ await fs.promises.rename(targetDirectory, backupDirectory);
376
+ await fs.promises.rename(stagingDirectory, targetDirectory);
377
+ }
378
+ catch (error) {
379
+ if (!fs.existsSync(targetDirectory) && fs.existsSync(backupDirectory)) {
380
+ await fs.promises.rename(backupDirectory, targetDirectory);
381
+ }
382
+ throw error;
383
+ }
384
+ if (targetExists) {
385
+ await fs.promises.rm(backupDirectory, { force: true, recursive: true });
386
+ }
387
+ }
388
+ function readExtensionManifest(directory) {
389
+ const manifestPath = path.join(directory, 'manifest.json');
390
+ let parsed;
391
+ try {
392
+ parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
393
+ }
394
+ catch (error) {
395
+ throw new Error(`Downloaded extension has no valid manifest.json: ${error instanceof Error ? error.message : error}`, { cause: error });
396
+ }
397
+ if (parsed.manifest_version !== 3 ||
398
+ typeof parsed.name !== 'string' ||
399
+ typeof parsed.key !== 'string' ||
400
+ parsed.key.length === 0) {
401
+ throw new Error('Downloaded archive is not a valid Manifest V3 extension');
402
+ }
403
+ if (!isMearlReleaseVersion(parsed.version)) {
404
+ throw new Error('Downloaded extension manifest has an invalid version');
405
+ }
406
+ return parsed;
407
+ }
408
+ async function downloadAndReplaceExtension(releaseInput, targetDirectory, fetchImpl, extractArchive) {
409
+ const release = validateExtensionRelease(releaseInput);
410
+ const parentDirectory = path.dirname(targetDirectory);
411
+ const stagingDirectory = `${targetDirectory}.staging-${randomUUID()}`;
412
+ const archivePath = path.join(parentDirectory, `.mearl-extension-${randomUUID()}.zip`);
413
+ await fs.promises.mkdir(parentDirectory, { recursive: true });
414
+ try {
415
+ const response = await fetchImpl(release.archiveUrl, { redirect: 'follow' });
416
+ if (!response.ok) {
417
+ throw new Error(`Extension archive returned HTTP ${response.status}: ${release.archiveUrl}`);
418
+ }
419
+ const contentLength = Number(response.headers.get('content-length') ?? 0);
420
+ if (contentLength > MAX_ARCHIVE_BYTES) {
421
+ throw new Error(`Extension archive exceeds ${MAX_ARCHIVE_BYTES} bytes`);
422
+ }
423
+ const archive = Buffer.from(await response.arrayBuffer());
424
+ if (archive.length === 0 || archive.length > MAX_ARCHIVE_BYTES) {
425
+ throw new Error('Extension archive is empty or too large');
426
+ }
427
+ if (release.sha256) {
428
+ const actualSha256 = createHash('sha256').update(archive).digest('hex');
429
+ if (actualSha256 !== release.sha256) {
430
+ throw new Error('Extension archive SHA256 does not match remote config');
431
+ }
432
+ }
433
+ await fs.promises.writeFile(archivePath, archive, { mode: 0o600 });
434
+ await fs.promises.mkdir(stagingDirectory, { recursive: true });
435
+ await extractArchive(archivePath, stagingDirectory);
436
+ const manifest = readExtensionManifest(stagingDirectory);
437
+ if (manifest.version !== release.version) {
438
+ throw new Error(`Extension archive version ${String(manifest.version)} does not match ${release.version}`);
439
+ }
440
+ await writeSetupMarker(stagingDirectory, targetDirectory);
441
+ await replaceDirectory(stagingDirectory, targetDirectory);
442
+ return release;
443
+ }
444
+ finally {
445
+ await fs.promises.rm(archivePath, { force: true });
446
+ await fs.promises.rm(stagingDirectory, { force: true, recursive: true });
447
+ }
448
+ }
449
+ export async function updateSetupManagedBrowserExtensionFiles(options, dependencies = {}) {
450
+ const targetDirectory = path.resolve(options.targetDirectory);
451
+ if (!hasSetupMarker(targetDirectory)) {
452
+ throw new Error('The running extension is not managed by Mearl setup');
453
+ }
454
+ const fetchImpl = dependencies.fetch ?? fetch;
455
+ const extractArchive = dependencies.extractArchive ??
456
+ ((archivePath, destination) => extractZip(archivePath, { dir: destination }));
457
+ const release = await downloadAndReplaceExtension(options.release, targetDirectory, fetchImpl, extractArchive);
458
+ return { ...release, path: targetDirectory };
459
+ }
460
+ export async function installMearlBrowserExtension(options = {}, dependencies = {}) {
461
+ const connectCdp = dependencies.connectCdp ?? connectBrowserExtensionViaCdp;
462
+ const fetchImpl = dependencies.fetch ?? fetch;
463
+ const extractArchive = dependencies.extractArchive ??
464
+ ((archivePath, destination) => extractZip(archivePath, { dir: destination }));
465
+ const loadUnpacked = dependencies.loadUnpacked ?? loadUnpackedViaCdp;
466
+ const inspectInstallation = dependencies.inspectInstallation ?? inspectMearlExtensionViaCdp;
467
+ const inspectInstalled = dependencies.inspectInstalled ?? inspectSetupManagedExtension;
468
+ const reloadInstalled = dependencies.reloadInstalled ?? reloadInstalledExtension;
469
+ const sleep = dependencies.sleep ??
470
+ (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)));
471
+ const writeLine = options.writeLine ?? (line => console.error(line));
472
+ const targetDirectory = path.resolve(options.targetDirectory ?? resolveMearlExtensionDirectory());
473
+ const release = options.release
474
+ ? validateExtensionRelease(options.release)
475
+ : await resolveExtensionArchive(fetchImpl);
476
+ const chromeUserDataDirectory = options.chromeUserDataDirectory ?? resolveChromeUserDataDirectory();
477
+ const waitForCdpMs = Math.max(0, options.waitForCdpMs ?? 0);
478
+ const useSharedCdpSession = !dependencies.inspectInstallation && !dependencies.loadUnpacked;
479
+ let setupManagedBrowserIds = [];
480
+ if (hasSetupMarker(targetDirectory)) {
481
+ try {
482
+ setupManagedBrowserIds = await inspectInstalled(targetDirectory);
483
+ }
484
+ catch {
485
+ // A missing Native Host connection cannot prove ownership; CDP performs the safe fallback.
486
+ }
487
+ }
488
+ const setupManagedAtStart = setupManagedBrowserIds.length > 0;
489
+ let cdpSession;
490
+ const inspectOnce = async () => {
491
+ if (!useSharedCdpSession)
492
+ return inspectInstallation(chromeUserDataDirectory);
493
+ const candidate = await connectCdp(chromeUserDataDirectory);
494
+ try {
495
+ const installed = await candidate.inspectInstallation();
496
+ cdpSession?.close();
497
+ cdpSession = candidate;
498
+ return installed;
499
+ }
500
+ catch (error) {
501
+ candidate.close();
502
+ throw error;
503
+ }
504
+ };
505
+ const waitForCdpInspection = async () => {
506
+ const deadline = Date.now() + waitForCdpMs;
507
+ let lastFailure;
508
+ let lastNoticeAt = 0;
509
+ do {
510
+ try {
511
+ const installed = await inspectOnce();
512
+ if (installed &&
513
+ !isSetupManagedInstallation(installed, targetDirectory) &&
514
+ !options.force) {
515
+ throw new BrowserExtensionConflictError(installed);
516
+ }
517
+ return undefined;
518
+ }
519
+ catch (error) {
520
+ if (error instanceof BrowserExtensionConflictError)
521
+ throw error;
522
+ const message = error instanceof Error ? error.message : String(error);
523
+ lastFailure = message;
524
+ if (waitForCdpMs === 0 || /Method not (?:available|found)|wasn't found/i.test(message)) {
525
+ break;
526
+ }
527
+ if (lastNoticeAt === 0) {
528
+ writeLine('Waiting for Chrome remote debugging before checking the installed extension...');
529
+ writeLine('Open chrome://inspect/#remote-debugging, enable remote debugging, and approve the connection.');
530
+ lastNoticeAt = Date.now();
531
+ }
532
+ else if (Date.now() - lastNoticeAt >= 15_000) {
533
+ writeLine('Still waiting for Chrome remote debugging...');
534
+ lastNoticeAt = Date.now();
535
+ }
536
+ await sleep(Math.min(1_000, Math.max(0, deadline - Date.now())));
537
+ }
538
+ } while (Date.now() < deadline);
539
+ return lastFailure;
540
+ };
541
+ try {
542
+ if (!setupManagedAtStart) {
543
+ const cdpError = await waitForCdpInspection();
544
+ if (cdpError) {
545
+ return {
546
+ filesUpdated: false,
547
+ loaded: false,
548
+ path: targetDirectory,
549
+ version: release.version,
550
+ archiveUrl: release.archiveUrl,
551
+ cdpError,
552
+ };
553
+ }
554
+ }
555
+ await downloadAndReplaceExtension(release, targetDirectory, fetchImpl, extractArchive);
556
+ if (setupManagedAtStart) {
557
+ try {
558
+ await reloadInstalled(setupManagedBrowserIds);
559
+ return {
560
+ activation: 'native-host',
561
+ extensionId: MEARL_EXTENSION_ID,
562
+ filesUpdated: true,
563
+ loaded: true,
564
+ path: targetDirectory,
565
+ version: release.version,
566
+ archiveUrl: release.archiveUrl,
567
+ };
568
+ }
569
+ catch (reloadError) {
570
+ const cdpError = await waitForCdpInspection();
571
+ if (cdpError) {
572
+ const reloadMessage = reloadError instanceof Error ? reloadError.message : String(reloadError);
573
+ return {
574
+ filesUpdated: true,
575
+ loaded: false,
576
+ path: targetDirectory,
577
+ version: release.version,
578
+ archiveUrl: release.archiveUrl,
579
+ cdpError: `Native Host reload failed: ${reloadMessage}; CDP fallback failed: ${cdpError}`,
580
+ };
581
+ }
582
+ }
583
+ }
584
+ try {
585
+ const loaded = cdpSession
586
+ ? await cdpSession.loadUnpacked(targetDirectory)
587
+ : await loadUnpacked(targetDirectory, chromeUserDataDirectory);
588
+ return {
589
+ activation: 'cdp',
590
+ extensionId: loaded.extensionId,
591
+ filesUpdated: true,
592
+ loaded: true,
593
+ path: targetDirectory,
594
+ version: release.version,
595
+ archiveUrl: release.archiveUrl,
596
+ };
597
+ }
598
+ catch (error) {
599
+ return {
600
+ filesUpdated: true,
601
+ loaded: false,
602
+ path: targetDirectory,
603
+ version: release.version,
604
+ archiveUrl: release.archiveUrl,
605
+ cdpError: error instanceof Error ? error.message : String(error),
606
+ };
607
+ }
608
+ }
609
+ finally {
610
+ cdpSession?.close();
611
+ }
612
+ }
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin } from './agentBay.js';
3
+ import { installMearlBrowserExtension } from './browserExtension.js';
3
4
  import { runMearlLifecycle, type MearlLifecycleOperation } from './lifecycle.js';
4
5
  import type { MearlProfile } from './profiles.js';
5
6
  export declare const SETUP_VERSION: string;
@@ -7,8 +8,9 @@ interface ParsedArgs {
7
8
  agentBayKeyFile?: string;
8
9
  agentBayReadStdin: boolean;
9
10
  cloudMode?: 'websocket' | 'qoder';
11
+ force?: boolean;
10
12
  help: boolean;
11
- operation?: MearlLifecycleOperation | 'agentbay';
13
+ operation?: MearlLifecycleOperation | 'agentbay' | 'extension';
12
14
  profiles?: MearlProfile[];
13
15
  skipSkill: boolean;
14
16
  targetLine?: string;
@@ -20,6 +22,7 @@ export interface SetupCliDependencies {
20
22
  promptAgentBayKey?: typeof promptAgentBayApiKey;
21
23
  readAgentBayKeyFile?: typeof readAgentBayApiKeyFile;
22
24
  readAgentBayKeyStdin?: typeof readAgentBayApiKeyStdin;
25
+ installBrowserExtension?: typeof installMearlBrowserExtension;
23
26
  runLifecycle?: typeof runMearlLifecycle;
24
27
  }
25
28
  export declare function parseSetupArgs(argv: string[]): ParsedArgs;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
4
+ import { installMearlBrowserExtension } from './browserExtension.js';
4
5
  import { runMearlLifecycle } from './lifecycle.js';
5
6
  import { isMearlReleaseLine } from './version.js';
6
7
  const require = createRequire(import.meta.url);
@@ -52,6 +53,21 @@ export function parseSetupArgs(argv) {
52
53
  }
53
54
  throw new Error('Usage: npx @mearl/setup agentbay [--stdin | --key-file <path>]. API keys are not accepted as command-line arguments.');
54
55
  }
56
+ if (operation === 'extension') {
57
+ const accepted = new Set(['--force', '--yes', '-y', '--non-interactive']);
58
+ const confirmationFlags = new Set(['--yes', '-y', '--non-interactive']);
59
+ const unknown = argv.slice(1).find(arg => !accepted.has(arg));
60
+ if (unknown)
61
+ throw new Error(`Unknown option: ${unknown}`);
62
+ return {
63
+ agentBayReadStdin: false,
64
+ help: false,
65
+ ...(argv.slice(1).includes('--force') ? { force: true } : {}),
66
+ operation,
67
+ skipSkill: false,
68
+ yes: argv.slice(1).some(arg => confirmationFlags.has(arg)),
69
+ };
70
+ }
55
71
  if (operation !== 'install' && operation !== 'update' && operation !== 'uninstall') {
56
72
  throw new Error(`Unknown operation: ${operation}`);
57
73
  }
@@ -118,12 +134,14 @@ function printHelp(toError = false) {
118
134
  Usage:
119
135
  npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
120
136
  npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
137
+ npx @mearl/setup extension [--force] [--yes]
121
138
  npx @mearl/setup agentbay [--stdin | --key-file <path>]
122
139
 
123
140
  Behavior:
124
141
  install Default command; defaults to --local when no environment is specified
125
142
  update Auto-detects installed environments from the current PATH
126
143
  uninstall Auto-detects installed environments and asks for confirmation
144
+ extension Download the latest browser extension and install or reload it through CDP
127
145
  agentbay Save or update AGENTBAY_KEY without changing other Mearl settings
128
146
 
129
147
  Options:
@@ -137,6 +155,7 @@ Options:
137
155
  --target-line <major.minor.x>
138
156
  Install the latest packages from one release line, such as 2.3.x
139
157
  --skip-skill Keep the independently managed Mearl Skill unchanged
158
+ --force Replace an existing store, managed, or other local Mearl extension
140
159
  --yes, -y Run without prompts and accept detected defaults
141
160
  --non-interactive
142
161
  Alias for --yes, intended for Agents and CI
@@ -151,6 +170,7 @@ Examples:
151
170
  npx @mearl/setup --cloud
152
171
  npx @mearl/setup --local --cloud
153
172
  npx @mearl/setup update
173
+ npx @mearl/setup extension
154
174
  npx @mearl/setup agentbay
155
175
  npx @mearl/setup --qoder-cloud --yes
156
176
  npx @mearl/setup update --local --target-line 2.3.x --skip-skill
@@ -203,6 +223,38 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
203
223
  return 1;
204
224
  }
205
225
  }
226
+ if (parsed.operation === 'extension') {
227
+ if (!interactiveTerminal && !parsed.yes) {
228
+ console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
229
+ return 2;
230
+ }
231
+ try {
232
+ const result = await (dependencies.installBrowserExtension ?? installMearlBrowserExtension)({
233
+ force: parsed.force,
234
+ waitForCdpMs: interactiveTerminal && !parsed.yes ? 120_000 : 0,
235
+ writeLine: line => console.error(line),
236
+ });
237
+ if (result.loaded) {
238
+ console.error(result.activation === 'native-host'
239
+ ? `Browser extension v${result.version} updated and reloaded without CDP.`
240
+ : `Browser extension v${result.version} installed or reloaded through CDP.`);
241
+ console.error(`Extension ID: ${result.extensionId}`);
242
+ console.error(`Extension directory: ${result.path}`);
243
+ return 0;
244
+ }
245
+ console.error(result.filesUpdated
246
+ ? `Browser extension v${result.version} files were updated, but the extension was not reloaded.`
247
+ : `Browser extension v${result.version} was not installed.`);
248
+ if (result.cdpError)
249
+ console.error(`CDP installation pending: ${result.cdpError}`);
250
+ console.error('Open chrome://inspect/#remote-debugging, enable remote debugging, approve the connection, then rerun: npx @mearl/setup extension');
251
+ return 1;
252
+ }
253
+ catch (error) {
254
+ console.error(error instanceof Error ? error.message : String(error));
255
+ return 1;
256
+ }
257
+ }
206
258
  if (!interactiveTerminal && !parsed.yes && parsed.operation !== 'update') {
207
259
  console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
208
260
  return 2;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
2
  export { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
3
+ export { BrowserExtensionConflictError, extensionArchiveUrl, fetchBrowserExtensionRelease, findLatestExtensionVersion, installMearlBrowserExtension, inspectMearlExtensionViaCdp, loadUnpackedViaCdp, MEARL_EXTENSION_DIRECTORY_NAME, MEARL_EXTENSION_DOWNLOAD_PAGE, MEARL_EXTENSION_ID, MEARL_EXTENSION_REMOTE_CONFIG_URL, resolveChromeUserDataDirectory, resolveMearlExtensionDirectory, updateSetupManagedBrowserExtensionFiles, } from './browserExtension.js';
4
+ export type { BrowserExtensionDependencies, BrowserExtensionInstallOptions, BrowserExtensionInstallResult, BrowserExtensionRelease, InstalledBrowserExtension, SetupManagedBrowserExtensionUpdateOptions, SetupManagedBrowserExtensionUpdateResult, } from './browserExtension.js';
3
5
  export type { AgentBayConfigurationOptions, AgentBayConfigurationResult, AgentBaySecretInput, AgentBaySecretOutput, } from './agentBay.js';
4
6
  export type { DetectedGlobalPackage, GlobalPackageCommand, GlobalPackageManager, PackageManagerInstallation, PackageOperation, PackageDetectionOptions, } from './packageManager.js';
5
7
  export { runMearlLifecycle } from './lifecycle.js';
@@ -7,6 +9,6 @@ export type { MearlLifecycleDependencies, MearlLifecycleOperation, MearlLifecycl
7
9
  export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
8
10
  export type { DetectedMearlProfiles, MearlProfile, MearlProfileConfig } from './profiles.js';
9
11
  export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
10
- export { compareMearlReleaseLines, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
12
+ export { compareMearlReleaseLines, compareMearlReleaseVersions, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
11
13
  export type { MearlReleaseLine, MearlReleaseVersion } from './version.js';
12
14
  export type { NativeHostCleanupDependencies, NativeHostCleanupOptions, NativeHostIntegrationMode, } from './nativeHost.js';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
2
  export { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
3
+ export { BrowserExtensionConflictError, extensionArchiveUrl, fetchBrowserExtensionRelease, findLatestExtensionVersion, installMearlBrowserExtension, inspectMearlExtensionViaCdp, loadUnpackedViaCdp, MEARL_EXTENSION_DIRECTORY_NAME, MEARL_EXTENSION_DOWNLOAD_PAGE, MEARL_EXTENSION_ID, MEARL_EXTENSION_REMOTE_CONFIG_URL, resolveChromeUserDataDirectory, resolveMearlExtensionDirectory, updateSetupManagedBrowserExtensionFiles, } from './browserExtension.js';
3
4
  export { runMearlLifecycle } from './lifecycle.js';
4
5
  export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
5
6
  export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
6
- export { compareMearlReleaseLines, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
7
+ export { compareMearlReleaseLines, compareMearlReleaseVersions, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
package/dist/version.d.ts CHANGED
@@ -19,3 +19,4 @@ export declare function isMearlReleaseLine(value: unknown): value is string;
19
19
  export declare function mearlReleaseLineForVersion(version: string): string;
20
20
  export declare function isMearlReleaseVersionInLine(version: string, releaseLine: string): boolean;
21
21
  export declare function compareMearlReleaseLines(leftVersion: string, rightVersion: string): -1 | 0 | 1;
22
+ export declare function compareMearlReleaseVersions(leftVersion: string, rightVersion: string): -1 | 0 | 1;
package/dist/version.js CHANGED
@@ -75,3 +75,25 @@ export function compareMearlReleaseLines(leftVersion, rightVersion) {
75
75
  }
76
76
  return 0;
77
77
  }
78
+ export function compareMearlReleaseVersions(leftVersion, rightVersion) {
79
+ const left = parseMearlReleaseVersion(leftVersion);
80
+ const right = parseMearlReleaseVersion(rightVersion);
81
+ if (!left || !right) {
82
+ throw new Error('Mearl release comparison requires valid release versions');
83
+ }
84
+ for (const key of ['major', 'minor', 'patch']) {
85
+ if (left[key] < right[key])
86
+ return -1;
87
+ if (left[key] > right[key])
88
+ return 1;
89
+ }
90
+ if (left.channel !== right.channel)
91
+ return left.channel === 'stable' ? 1 : -1;
92
+ const leftBeta = left.beta ?? 0;
93
+ const rightBeta = right.beta ?? 0;
94
+ if (leftBeta < rightBeta)
95
+ return -1;
96
+ if (leftBeta > rightBeta)
97
+ return 1;
98
+ return 0;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/setup",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "One-shot environment setup and configuration for Mearl",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,10 @@
33
33
  "./version": {
34
34
  "types": "./dist/version.d.ts",
35
35
  "import": "./dist/version.js"
36
+ },
37
+ "./browser-extension": {
38
+ "types": "./dist/browserExtension.d.ts",
39
+ "import": "./dist/browserExtension.js"
36
40
  }
37
41
  },
38
42
  "bin": {
@@ -51,11 +55,20 @@
51
55
  "uninstall"
52
56
  ],
53
57
  "license": "ISC",
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
54
61
  "publishConfig": {
55
62
  "registry": "https://registry.npmjs.org"
56
63
  },
64
+ "dependencies": {
65
+ "extract-zip": "^2.0.1",
66
+ "ws": "^8.18.3",
67
+ "@mearl/client": "2.8.2"
68
+ },
57
69
  "devDependencies": {
58
70
  "@types/node": "^24.9.1",
71
+ "@types/ws": "^8.18.1",
59
72
  "typescript": "^5.8.3"
60
73
  },
61
74
  "scripts": {