@mearl/setup 2.8.1 → 2.9.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 +21 -7
- package/dist/browserExtension.d.ts +33 -0
- package/dist/browserExtension.js +315 -108
- package/dist/cli.js +124 -112
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/version.d.ts +1 -0
- package/dist/version.js +22 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -47,8 +47,10 @@ npx @mearl/setup agentbay
|
|
|
47
47
|
- `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
|
|
48
48
|
- 默认的 `install` / `update` 不处理浏览器扩展。`extension` 是独立命令:从
|
|
49
49
|
`https://mearl.alibaba-inc.com/` 识别并下载最新的 `mearl-chrome-v*.zip`,原子解压到
|
|
50
|
-
`~/.mearl/chrome-extension
|
|
51
|
-
|
|
50
|
+
`~/.mearl/chrome-extension`。首次安装通过浏览器级 CDP 的 `Extensions.loadUnpacked`
|
|
51
|
+
加载;后续更新由 `mearl check --local --json` 确认当前扩展仍来自 setup 管理目录后,直接替换
|
|
52
|
+
文件并通过 Native Host 触发扩展 reload。固定目录和 manifest 中的 `key` 保证扩展 ID
|
|
53
|
+
与浏览器存储保持稳定。
|
|
52
54
|
- 下载前会通过 CDP 检查同 ID 插件的来源。未安装,或来源为固定目录中的 `UNPACKED`
|
|
53
55
|
插件时可继续;Chrome 商店、企业策略或其他本地目录安装的版本默认保持不变。只有显式
|
|
54
56
|
传入 `--force` 才会接管并替换这些版本。
|
|
@@ -59,14 +61,26 @@ npx @mearl/setup agentbay
|
|
|
59
61
|
npx @mearl/setup extension
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
首次安装、旧版 setup 扩展首次迁移,或 Native Host reload 不可用时才需要 CDP。如果
|
|
65
|
+
Chrome 尚未开放 CDP,交互命令会提示打开
|
|
63
66
|
`chrome://inspect/#remote-debugging`、启用远程调试并允许连接,然后等待安装完成。无 TTY
|
|
64
|
-
环境使用 `npx @mearl/setup extension --yes
|
|
65
|
-
|
|
67
|
+
环境使用 `npx @mearl/setup extension --yes`;它只尝试一次。无法确认 setup 管理来源且
|
|
68
|
+
CDP 不可用时不会下载或修改插件目录,打开 CDP 后重新执行同一命令即可。
|
|
66
69
|
|
|
67
70
|
该命令每次都读取下载页上的最新插件版本,不要求与 setup、CLI 或 Native Host 同版本。
|
|
68
|
-
|
|
69
|
-
|
|
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()` 主动检查。
|
|
70
84
|
|
|
71
85
|
## 跟随浏览器扩展自动更新
|
|
72
86
|
|
|
@@ -1,15 +1,32 @@
|
|
|
1
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";
|
|
2
3
|
export declare const MEARL_EXTENSION_DIRECTORY_NAME = "chrome-extension";
|
|
3
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
|
+
}
|
|
4
11
|
export interface BrowserExtensionInstallOptions {
|
|
5
12
|
force?: boolean;
|
|
6
13
|
targetDirectory?: string;
|
|
7
14
|
chromeUserDataDirectory?: string;
|
|
8
15
|
waitForCdpMs?: number;
|
|
9
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;
|
|
10
25
|
}
|
|
11
26
|
export interface BrowserExtensionInstallResult {
|
|
27
|
+
activation?: 'cdp' | 'native-host';
|
|
12
28
|
extensionId?: string;
|
|
29
|
+
filesUpdated?: boolean;
|
|
13
30
|
loaded: boolean;
|
|
14
31
|
path: string;
|
|
15
32
|
version: string;
|
|
@@ -17,10 +34,13 @@ export interface BrowserExtensionInstallResult {
|
|
|
17
34
|
cdpError?: string;
|
|
18
35
|
}
|
|
19
36
|
export interface BrowserExtensionDependencies {
|
|
37
|
+
connectCdp?: typeof connectBrowserExtensionViaCdp;
|
|
20
38
|
extractArchive?: (archivePath: string, destination: string) => Promise<void>;
|
|
21
39
|
fetch?: typeof fetch;
|
|
40
|
+
inspectInstalled?: typeof inspectSetupManagedExtension;
|
|
22
41
|
inspectInstallation?: typeof inspectMearlExtensionViaCdp;
|
|
23
42
|
loadUnpacked?: typeof loadUnpackedViaCdp;
|
|
43
|
+
reloadInstalled?: typeof reloadInstalledExtension;
|
|
24
44
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
25
45
|
}
|
|
26
46
|
export interface InstalledBrowserExtension {
|
|
@@ -38,8 +58,21 @@ export declare function resolveChromeUserDataDirectory(platform?: NodeJS.Platfor
|
|
|
38
58
|
export declare function resolveMearlExtensionDirectory(home?: string): string;
|
|
39
59
|
export declare function findLatestExtensionVersion(html: string): string | null;
|
|
40
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>;
|
|
41
70
|
export declare function loadUnpackedViaCdp(extensionDirectory: string, chromeUserDataDirectory?: string): Promise<{
|
|
42
71
|
extensionId: string;
|
|
43
72
|
}>;
|
|
44
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>;
|
|
45
77
|
export declare function installMearlBrowserExtension(options?: BrowserExtensionInstallOptions, dependencies?: BrowserExtensionDependencies): Promise<BrowserExtensionInstallResult>;
|
|
78
|
+
export {};
|
package/dist/browserExtension.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
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';
|
|
5
7
|
import extractZip from 'extract-zip';
|
|
6
8
|
import WebSocket from 'ws';
|
|
7
|
-
import {
|
|
9
|
+
import { compareMearlReleaseVersions, isMearlReleaseVersion } from './version.js';
|
|
8
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`;
|
|
9
12
|
export const MEARL_EXTENSION_DIRECTORY_NAME = 'chrome-extension';
|
|
10
13
|
export const MEARL_EXTENSION_ID = 'aoehhjnofngknnjefamjbplchbolghkm';
|
|
14
|
+
export const MEARL_EXTENSION_SETUP_MARKER = '.mearl-setup.json';
|
|
11
15
|
const ARCHIVE_PATTERN = /mearl-chrome-v(\d+\.\d+\.\d+(?:-beta\.\d+)?)\.zip/g;
|
|
12
16
|
const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
const SHA256_PATTERN = /^[a-f\d]{64}$/i;
|
|
13
18
|
export class BrowserExtensionConflictError extends Error {
|
|
14
19
|
installed;
|
|
15
20
|
constructor(installed) {
|
|
@@ -18,17 +23,6 @@ export class BrowserExtensionConflictError extends Error {
|
|
|
18
23
|
this.name = 'BrowserExtensionConflictError';
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
|
-
function compareVersions(left, right) {
|
|
22
|
-
const parsedLeft = parseMearlReleaseVersion(left);
|
|
23
|
-
const parsedRight = parseMearlReleaseVersion(right);
|
|
24
|
-
for (const key of ['major', 'minor', 'patch']) {
|
|
25
|
-
if (parsedLeft[key] !== parsedRight[key])
|
|
26
|
-
return parsedLeft[key] - parsedRight[key];
|
|
27
|
-
}
|
|
28
|
-
if (parsedLeft.channel !== parsedRight.channel)
|
|
29
|
-
return parsedLeft.channel === 'stable' ? 1 : -1;
|
|
30
|
-
return (parsedLeft.beta ?? 0) - (parsedRight.beta ?? 0);
|
|
31
|
-
}
|
|
32
26
|
export function resolveChromeUserDataDirectory(platform = process.platform, home = os.homedir(), env = process.env) {
|
|
33
27
|
if (platform === 'darwin') {
|
|
34
28
|
return path.join(home, 'Library', 'Application Support', 'Google', 'Chrome');
|
|
@@ -43,7 +37,7 @@ export function resolveMearlExtensionDirectory(home = os.homedir()) {
|
|
|
43
37
|
}
|
|
44
38
|
export function findLatestExtensionVersion(html) {
|
|
45
39
|
const versions = Array.from(html.matchAll(ARCHIVE_PATTERN), match => match[1]).filter(isMearlReleaseVersion);
|
|
46
|
-
return versions.sort(
|
|
40
|
+
return versions.sort(compareMearlReleaseVersions).at(-1) ?? null;
|
|
47
41
|
}
|
|
48
42
|
export function extensionArchiveUrl(version) {
|
|
49
43
|
if (!isMearlReleaseVersion(version)) {
|
|
@@ -51,6 +45,42 @@ export function extensionArchiveUrl(version) {
|
|
|
51
45
|
}
|
|
52
46
|
return new URL(`mearl-chrome-v${version}.zip`, MEARL_EXTENSION_DOWNLOAD_PAGE).toString();
|
|
53
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
|
+
}
|
|
54
84
|
async function resolveExtensionArchive(fetchImpl) {
|
|
55
85
|
const response = await fetchImpl(MEARL_EXTENSION_DOWNLOAD_PAGE, { redirect: 'follow' });
|
|
56
86
|
if (!response.ok) {
|
|
@@ -189,36 +219,38 @@ function sameExtensionPath(left, right) {
|
|
|
189
219
|
function isSetupManagedInstallation(installed, targetDirectory) {
|
|
190
220
|
return installed.location === 'UNPACKED' && sameExtensionPath(installed.path, targetDirectory);
|
|
191
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
|
+
}
|
|
192
247
|
function browserCdpEndpoint(chromeUserDataDirectory) {
|
|
193
248
|
const activePort = readDevToolsActivePort(chromeUserDataDirectory);
|
|
194
249
|
if (!activePort)
|
|
195
250
|
throw new Error('Chrome remote debugging is not enabled');
|
|
196
251
|
return `ws://127.0.0.1:${activePort.port}${activePort.browserWsPath}`;
|
|
197
252
|
}
|
|
198
|
-
|
|
199
|
-
const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
|
|
200
|
-
try {
|
|
201
|
-
const loaded = await connection.send('Extensions.loadUnpacked', {
|
|
202
|
-
path: path.resolve(extensionDirectory),
|
|
203
|
-
});
|
|
204
|
-
if (loaded.id !== MEARL_EXTENSION_ID) {
|
|
205
|
-
throw new Error(`Extensions.loadUnpacked returned unexpected extension ID: ${String(loaded.id)}`);
|
|
206
|
-
}
|
|
207
|
-
const inventory = await connection.send('Extensions.getExtensions');
|
|
208
|
-
const extension = Array.isArray(inventory.extensions)
|
|
209
|
-
? inventory.extensions.find(candidate => candidate?.id === loaded.id)
|
|
210
|
-
: null;
|
|
211
|
-
if (!extension?.enabled) {
|
|
212
|
-
throw new Error('Chrome loaded the extension but did not report it as enabled');
|
|
213
|
-
}
|
|
214
|
-
return { extensionId: loaded.id };
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
connection.close();
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
export async function inspectMearlExtensionViaCdp(chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
|
|
221
|
-
const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
|
|
253
|
+
async function inspectInstallationWithConnection(connection) {
|
|
222
254
|
const target = await connection.send('Target.createTarget', {
|
|
223
255
|
background: true,
|
|
224
256
|
url: 'chrome://extensions-internals',
|
|
@@ -259,9 +291,82 @@ export async function inspectMearlExtensionViaCdp(chromeUserDataDirectory = reso
|
|
|
259
291
|
if (typeof target.targetId === 'string') {
|
|
260
292
|
await connection.send('Target.closeTarget', { targetId: target.targetId }).catch(() => { });
|
|
261
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 {
|
|
262
326
|
connection.close();
|
|
263
327
|
}
|
|
264
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
|
+
}
|
|
265
370
|
async function replaceDirectory(stagingDirectory, targetDirectory) {
|
|
266
371
|
const backupDirectory = `${targetDirectory}.backup-${randomUUID()}`;
|
|
267
372
|
const targetExists = fs.existsSync(targetDirectory);
|
|
@@ -300,63 +405,11 @@ function readExtensionManifest(directory) {
|
|
|
300
405
|
}
|
|
301
406
|
return parsed;
|
|
302
407
|
}
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const extractArchive = dependencies.extractArchive ??
|
|
306
|
-
((archivePath, destination) => extractZip(archivePath, { dir: destination }));
|
|
307
|
-
const loadUnpacked = dependencies.loadUnpacked ?? loadUnpackedViaCdp;
|
|
308
|
-
const inspectInstallation = dependencies.inspectInstallation ?? inspectMearlExtensionViaCdp;
|
|
309
|
-
const sleep = dependencies.sleep ??
|
|
310
|
-
(milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
311
|
-
const writeLine = options.writeLine ?? (line => console.error(line));
|
|
312
|
-
const targetDirectory = path.resolve(options.targetDirectory ?? resolveMearlExtensionDirectory());
|
|
408
|
+
async function downloadAndReplaceExtension(releaseInput, targetDirectory, fetchImpl, extractArchive) {
|
|
409
|
+
const release = validateExtensionRelease(releaseInput);
|
|
313
410
|
const parentDirectory = path.dirname(targetDirectory);
|
|
314
411
|
const stagingDirectory = `${targetDirectory}.staging-${randomUUID()}`;
|
|
315
412
|
const archivePath = path.join(parentDirectory, `.mearl-extension-${randomUUID()}.zip`);
|
|
316
|
-
const release = await resolveExtensionArchive(fetchImpl);
|
|
317
|
-
const chromeUserDataDirectory = options.chromeUserDataDirectory ?? resolveChromeUserDataDirectory();
|
|
318
|
-
const waitForCdpMs = Math.max(0, options.waitForCdpMs ?? 0);
|
|
319
|
-
const deadline = Date.now() + waitForCdpMs;
|
|
320
|
-
let lastFailure;
|
|
321
|
-
let lastNoticeAt = 0;
|
|
322
|
-
do {
|
|
323
|
-
try {
|
|
324
|
-
const installed = await inspectInstallation(chromeUserDataDirectory);
|
|
325
|
-
if (installed && !isSetupManagedInstallation(installed, targetDirectory) && !options.force) {
|
|
326
|
-
throw new BrowserExtensionConflictError(installed);
|
|
327
|
-
}
|
|
328
|
-
lastFailure = undefined;
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
if (error instanceof BrowserExtensionConflictError)
|
|
333
|
-
throw error;
|
|
334
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
335
|
-
lastFailure = message;
|
|
336
|
-
if (waitForCdpMs === 0 || /Method not (?:available|found)|wasn't found/i.test(message)) {
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
if (lastNoticeAt === 0) {
|
|
340
|
-
writeLine('Waiting for Chrome remote debugging before checking the installed extension...');
|
|
341
|
-
writeLine('Open chrome://inspect/#remote-debugging, enable remote debugging, and approve the connection.');
|
|
342
|
-
lastNoticeAt = Date.now();
|
|
343
|
-
}
|
|
344
|
-
else if (Date.now() - lastNoticeAt >= 15_000) {
|
|
345
|
-
writeLine('Still waiting for Chrome remote debugging...');
|
|
346
|
-
lastNoticeAt = Date.now();
|
|
347
|
-
}
|
|
348
|
-
await sleep(Math.min(1_000, Math.max(0, deadline - Date.now())));
|
|
349
|
-
}
|
|
350
|
-
} while (Date.now() < deadline);
|
|
351
|
-
if (lastFailure) {
|
|
352
|
-
return {
|
|
353
|
-
loaded: false,
|
|
354
|
-
path: targetDirectory,
|
|
355
|
-
version: release.version,
|
|
356
|
-
archiveUrl: release.archiveUrl,
|
|
357
|
-
cdpError: lastFailure,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
413
|
await fs.promises.mkdir(parentDirectory, { recursive: true });
|
|
361
414
|
try {
|
|
362
415
|
const response = await fetchImpl(release.archiveUrl, { redirect: 'follow' });
|
|
@@ -371,6 +424,12 @@ export async function installMearlBrowserExtension(options = {}, dependencies =
|
|
|
371
424
|
if (archive.length === 0 || archive.length > MAX_ARCHIVE_BYTES) {
|
|
372
425
|
throw new Error('Extension archive is empty or too large');
|
|
373
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
|
+
}
|
|
374
433
|
await fs.promises.writeFile(archivePath, archive, { mode: 0o600 });
|
|
375
434
|
await fs.promises.mkdir(stagingDirectory, { recursive: true });
|
|
376
435
|
await extractArchive(archivePath, stagingDirectory);
|
|
@@ -378,29 +437,177 @@ export async function installMearlBrowserExtension(options = {}, dependencies =
|
|
|
378
437
|
if (manifest.version !== release.version) {
|
|
379
438
|
throw new Error(`Extension archive version ${String(manifest.version)} does not match ${release.version}`);
|
|
380
439
|
}
|
|
440
|
+
await writeSetupMarker(stagingDirectory, targetDirectory);
|
|
381
441
|
await replaceDirectory(stagingDirectory, targetDirectory);
|
|
442
|
+
return release;
|
|
382
443
|
}
|
|
383
444
|
finally {
|
|
384
445
|
await fs.promises.rm(archivePath, { force: true });
|
|
385
446
|
await fs.promises.rm(stagingDirectory, { force: true, recursive: true });
|
|
386
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 options.force ? null : inspectInstallation(chromeUserDataDirectory);
|
|
493
|
+
}
|
|
494
|
+
const candidate = await connectCdp(chromeUserDataDirectory);
|
|
495
|
+
try {
|
|
496
|
+
const installed = options.force ? null : await candidate.inspectInstallation();
|
|
497
|
+
cdpSession?.close();
|
|
498
|
+
cdpSession = candidate;
|
|
499
|
+
return installed;
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
candidate.close();
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const waitForCdpInspection = async () => {
|
|
507
|
+
const deadline = Date.now() + waitForCdpMs;
|
|
508
|
+
let lastFailure;
|
|
509
|
+
let lastNoticeAt = 0;
|
|
510
|
+
do {
|
|
511
|
+
try {
|
|
512
|
+
const installed = await inspectOnce();
|
|
513
|
+
if (installed &&
|
|
514
|
+
!isSetupManagedInstallation(installed, targetDirectory) &&
|
|
515
|
+
!options.force) {
|
|
516
|
+
throw new BrowserExtensionConflictError(installed);
|
|
517
|
+
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (error instanceof BrowserExtensionConflictError)
|
|
522
|
+
throw error;
|
|
523
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
524
|
+
lastFailure = message;
|
|
525
|
+
if (waitForCdpMs === 0 || /Method not (?:available|found)|wasn't found/i.test(message)) {
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
if (lastNoticeAt === 0) {
|
|
529
|
+
writeLine('Waiting for Chrome remote debugging before checking the installed extension...');
|
|
530
|
+
writeLine('Open chrome://inspect/#remote-debugging, enable remote debugging, and approve the connection.');
|
|
531
|
+
lastNoticeAt = Date.now();
|
|
532
|
+
}
|
|
533
|
+
else if (Date.now() - lastNoticeAt >= 15_000) {
|
|
534
|
+
writeLine('Still waiting for Chrome remote debugging...');
|
|
535
|
+
lastNoticeAt = Date.now();
|
|
536
|
+
}
|
|
537
|
+
await sleep(Math.min(1_000, Math.max(0, deadline - Date.now())));
|
|
538
|
+
}
|
|
539
|
+
} while (Date.now() < deadline);
|
|
540
|
+
return lastFailure;
|
|
541
|
+
};
|
|
387
542
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
543
|
+
if (!setupManagedAtStart) {
|
|
544
|
+
const cdpError = await waitForCdpInspection();
|
|
545
|
+
if (cdpError) {
|
|
546
|
+
return {
|
|
547
|
+
filesUpdated: false,
|
|
548
|
+
loaded: false,
|
|
549
|
+
path: targetDirectory,
|
|
550
|
+
version: release.version,
|
|
551
|
+
archiveUrl: release.archiveUrl,
|
|
552
|
+
cdpError,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
await downloadAndReplaceExtension(release, targetDirectory, fetchImpl, extractArchive);
|
|
557
|
+
if (setupManagedAtStart) {
|
|
558
|
+
try {
|
|
559
|
+
await reloadInstalled(setupManagedBrowserIds);
|
|
560
|
+
return {
|
|
561
|
+
activation: 'native-host',
|
|
562
|
+
extensionId: MEARL_EXTENSION_ID,
|
|
563
|
+
filesUpdated: true,
|
|
564
|
+
loaded: true,
|
|
565
|
+
path: targetDirectory,
|
|
566
|
+
version: release.version,
|
|
567
|
+
archiveUrl: release.archiveUrl,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
catch (reloadError) {
|
|
571
|
+
const cdpError = await waitForCdpInspection();
|
|
572
|
+
if (cdpError) {
|
|
573
|
+
const reloadMessage = reloadError instanceof Error ? reloadError.message : String(reloadError);
|
|
574
|
+
return {
|
|
575
|
+
filesUpdated: true,
|
|
576
|
+
loaded: false,
|
|
577
|
+
path: targetDirectory,
|
|
578
|
+
version: release.version,
|
|
579
|
+
archiveUrl: release.archiveUrl,
|
|
580
|
+
cdpError: `Native Host reload failed: ${reloadMessage}; CDP fallback failed: ${cdpError}`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const loaded = cdpSession
|
|
587
|
+
? await cdpSession.loadUnpacked(targetDirectory)
|
|
588
|
+
: await loadUnpacked(targetDirectory, chromeUserDataDirectory);
|
|
589
|
+
return {
|
|
590
|
+
activation: 'cdp',
|
|
591
|
+
extensionId: loaded.extensionId,
|
|
592
|
+
filesUpdated: true,
|
|
593
|
+
loaded: true,
|
|
594
|
+
path: targetDirectory,
|
|
595
|
+
version: release.version,
|
|
596
|
+
archiveUrl: release.archiveUrl,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
return {
|
|
601
|
+
filesUpdated: true,
|
|
602
|
+
loaded: false,
|
|
603
|
+
path: targetDirectory,
|
|
604
|
+
version: release.version,
|
|
605
|
+
archiveUrl: release.archiveUrl,
|
|
606
|
+
cdpError: error instanceof Error ? error.message : String(error),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
396
609
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
loaded: false,
|
|
400
|
-
path: targetDirectory,
|
|
401
|
-
version: release.version,
|
|
402
|
-
archiveUrl: release.archiveUrl,
|
|
403
|
-
cdpError: error instanceof Error ? error.message : String(error),
|
|
404
|
-
};
|
|
610
|
+
finally {
|
|
611
|
+
cdpSession?.close();
|
|
405
612
|
}
|
|
406
613
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,132 +1,140 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { Command, CommanderError } from 'commander';
|
|
3
4
|
import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
|
|
4
5
|
import { installMearlBrowserExtension } from './browserExtension.js';
|
|
5
6
|
import { runMearlLifecycle } from './lifecycle.js';
|
|
6
7
|
import { isMearlReleaseLine } from './version.js';
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
export const SETUP_VERSION = require('../package.json').version;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const baseParsedArgs = () => ({
|
|
11
|
+
agentBayReadStdin: false,
|
|
12
|
+
help: false,
|
|
13
|
+
skipSkill: false,
|
|
14
|
+
yes: false,
|
|
15
|
+
});
|
|
16
|
+
function commanderError(error) {
|
|
17
|
+
const message = error.message.replace(/^error:\s*/i, '');
|
|
18
|
+
if (error.code === 'commander.unknownCommand') {
|
|
19
|
+
return new Error(message.replace(/^unknown command/i, 'Unknown operation'));
|
|
12
20
|
}
|
|
13
|
-
if (
|
|
14
|
-
return
|
|
15
|
-
agentBayReadStdin: false,
|
|
16
|
-
help: false,
|
|
17
|
-
skipSkill: false,
|
|
18
|
-
version: true,
|
|
19
|
-
yes: false,
|
|
20
|
-
};
|
|
21
|
+
if (error.code === 'commander.unknownOption') {
|
|
22
|
+
return new Error(message.replace(/^unknown option/i, 'Unknown option'));
|
|
21
23
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
return new Error(message);
|
|
25
|
+
}
|
|
26
|
+
export function parseSetupArgs(argv) {
|
|
27
|
+
let parsed;
|
|
28
|
+
const program = new Command()
|
|
29
|
+
.name('mearl-setup')
|
|
30
|
+
.version(SETUP_VERSION, '-v, --version')
|
|
31
|
+
.exitOverride()
|
|
32
|
+
.configureOutput({ writeErr: () => { }, writeOut: () => { } });
|
|
33
|
+
const addLifecycleCommand = (operation) => {
|
|
34
|
+
const command = new Command(operation)
|
|
35
|
+
.exitOverride()
|
|
36
|
+
.configureOutput({ writeErr: () => { }, writeOut: () => { } })
|
|
37
|
+
.option('--local')
|
|
38
|
+
.option('--cloud')
|
|
39
|
+
.option('--qoder-cloud')
|
|
40
|
+
.option('--mcp')
|
|
41
|
+
.option('--target-line <major.minor.x>')
|
|
42
|
+
.option('--skip-skill')
|
|
43
|
+
.option('-y, --yes')
|
|
44
|
+
.option('--non-interactive')
|
|
45
|
+
.action(() => {
|
|
46
|
+
const options = command.opts();
|
|
47
|
+
if (options.cloud && options.qoderCloud) {
|
|
48
|
+
throw new Error('--cloud and --qoder-cloud cannot be used together');
|
|
49
|
+
}
|
|
50
|
+
if (options.targetLine && !isMearlReleaseLine(options.targetLine)) {
|
|
51
|
+
throw new Error('--target-line requires a valid Mearl release line such as 2.3.x');
|
|
52
|
+
}
|
|
53
|
+
if (operation === 'uninstall' && options.targetLine) {
|
|
54
|
+
throw new Error('--target-line is not available for uninstall');
|
|
55
|
+
}
|
|
56
|
+
const profiles = [];
|
|
57
|
+
if (options.local)
|
|
58
|
+
profiles.push('local');
|
|
59
|
+
if (options.cloud || options.qoderCloud)
|
|
60
|
+
profiles.push('cloud');
|
|
61
|
+
if (options.mcp)
|
|
62
|
+
profiles.push('mcp');
|
|
63
|
+
parsed = {
|
|
64
|
+
...baseParsedArgs(),
|
|
30
65
|
operation,
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
...(options.cloud || options.qoderCloud
|
|
67
|
+
? { cloudMode: options.qoderCloud ? 'qoder' : 'websocket' }
|
|
68
|
+
: {}),
|
|
69
|
+
...(profiles.length > 0 ? { profiles } : {}),
|
|
70
|
+
skipSkill: options.skipSkill === true,
|
|
71
|
+
targetLine: options.targetLine,
|
|
72
|
+
yes: options.yes === true || options.nonInteractive === true,
|
|
33
73
|
};
|
|
74
|
+
});
|
|
75
|
+
program.addCommand(command);
|
|
76
|
+
};
|
|
77
|
+
addLifecycleCommand('install');
|
|
78
|
+
addLifecycleCommand('update');
|
|
79
|
+
addLifecycleCommand('uninstall');
|
|
80
|
+
const extension = new Command('extension')
|
|
81
|
+
.exitOverride()
|
|
82
|
+
.configureOutput({ writeErr: () => { }, writeOut: () => { } })
|
|
83
|
+
.option('--force')
|
|
84
|
+
.option('-y, --yes')
|
|
85
|
+
.option('--non-interactive')
|
|
86
|
+
.action(() => {
|
|
87
|
+
const options = extension.opts();
|
|
88
|
+
parsed = {
|
|
89
|
+
...baseParsedArgs(),
|
|
90
|
+
...(options.force ? { force: true } : {}),
|
|
91
|
+
operation: 'extension',
|
|
92
|
+
yes: options.yes === true || options.nonInteractive === true,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
program.addCommand(extension);
|
|
96
|
+
const agentbay = new Command('agentbay')
|
|
97
|
+
.exitOverride()
|
|
98
|
+
.configureOutput({ writeErr: () => { }, writeOut: () => { } })
|
|
99
|
+
.argument('[api-key]')
|
|
100
|
+
.option('--stdin')
|
|
101
|
+
.option('--key-file <path>')
|
|
102
|
+
.action((apiKey) => {
|
|
103
|
+
const options = agentbay.opts();
|
|
104
|
+
if (apiKey) {
|
|
105
|
+
throw new Error('API keys are not accepted as command-line arguments');
|
|
34
106
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
agentBayReadStdin: true,
|
|
38
|
-
help: false,
|
|
39
|
-
operation,
|
|
40
|
-
skipSkill: false,
|
|
41
|
-
yes: false,
|
|
42
|
-
};
|
|
107
|
+
if (options.stdin && options.keyFile) {
|
|
108
|
+
throw new Error('--stdin and --key-file cannot be used together');
|
|
43
109
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
agentBayKeyFile: argv[2],
|
|
47
|
-
agentBayReadStdin: false,
|
|
48
|
-
help: false,
|
|
49
|
-
operation,
|
|
50
|
-
skipSkill: false,
|
|
51
|
-
yes: false,
|
|
52
|
-
};
|
|
110
|
+
if (options.keyFile !== undefined && !options.keyFile.trim()) {
|
|
111
|
+
throw new Error('--key-file requires a non-empty path');
|
|
53
112
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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)),
|
|
113
|
+
parsed = {
|
|
114
|
+
...baseParsedArgs(),
|
|
115
|
+
...(options.keyFile ? { agentBayKeyFile: options.keyFile } : {}),
|
|
116
|
+
agentBayReadStdin: options.stdin === true,
|
|
117
|
+
operation: 'agentbay',
|
|
69
118
|
};
|
|
119
|
+
});
|
|
120
|
+
program.addCommand(agentbay);
|
|
121
|
+
const normalizedArgv = argv[0] === undefined || argv[0].startsWith('-') ? ['install', ...argv] : argv;
|
|
122
|
+
try {
|
|
123
|
+
program.parse(['node', 'mearl-setup', ...normalizedArgv]);
|
|
70
124
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let cloudMode;
|
|
79
|
-
for (let index = optionStart; index < argv.length; index += 1) {
|
|
80
|
-
const arg = argv[index];
|
|
81
|
-
if (arg === '--yes' || arg === '-y' || arg === '--non-interactive') {
|
|
82
|
-
yes = true;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (arg === '--skip-skill') {
|
|
86
|
-
skipSkill = true;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (arg === '--target-line') {
|
|
90
|
-
const value = argv[index + 1];
|
|
91
|
-
if (!isMearlReleaseLine(value)) {
|
|
92
|
-
throw new Error('--target-line requires a valid Mearl release line such as 2.3.x');
|
|
93
|
-
}
|
|
94
|
-
targetLine = value;
|
|
95
|
-
index += 1;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (arg === '--cloud' || arg === '--qoder-cloud') {
|
|
99
|
-
const requestedMode = arg === '--qoder-cloud' ? 'qoder' : 'websocket';
|
|
100
|
-
if (cloudMode && cloudMode !== requestedMode) {
|
|
101
|
-
throw new Error('--cloud and --qoder-cloud cannot be used together');
|
|
102
|
-
}
|
|
103
|
-
cloudMode = requestedMode;
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof CommanderError) {
|
|
127
|
+
if (error.code === 'commander.helpDisplayed')
|
|
128
|
+
return { ...baseParsedArgs(), help: true };
|
|
129
|
+
if (error.code === 'commander.version')
|
|
130
|
+
return { ...baseParsedArgs(), version: true };
|
|
131
|
+
throw commanderError(error);
|
|
104
132
|
}
|
|
105
|
-
|
|
106
|
-
? 'local'
|
|
107
|
-
: arg === '--cloud' || arg === '--qoder-cloud'
|
|
108
|
-
? 'cloud'
|
|
109
|
-
: arg === '--mcp'
|
|
110
|
-
? 'mcp'
|
|
111
|
-
: undefined;
|
|
112
|
-
if (!candidate)
|
|
113
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
114
|
-
if (!profiles.includes(candidate))
|
|
115
|
-
profiles.push(candidate);
|
|
116
|
-
}
|
|
117
|
-
if (operation === 'uninstall' && targetLine) {
|
|
118
|
-
throw new Error('--target-line is not available for uninstall');
|
|
133
|
+
throw error;
|
|
119
134
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
operation,
|
|
124
|
-
...(cloudMode ? { cloudMode } : {}),
|
|
125
|
-
...(profiles.length > 0 ? { profiles } : {}),
|
|
126
|
-
skipSkill,
|
|
127
|
-
targetLine,
|
|
128
|
-
yes,
|
|
129
|
-
};
|
|
135
|
+
if (!parsed)
|
|
136
|
+
throw new Error('No setup operation was selected');
|
|
137
|
+
return parsed;
|
|
130
138
|
}
|
|
131
139
|
function printHelp(toError = false) {
|
|
132
140
|
const output = `Mearl Setup v${SETUP_VERSION}
|
|
@@ -235,12 +243,16 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
|
|
|
235
243
|
writeLine: line => console.error(line),
|
|
236
244
|
});
|
|
237
245
|
if (result.loaded) {
|
|
238
|
-
console.error(
|
|
246
|
+
console.error(result.activation === 'native-host'
|
|
247
|
+
? `Browser extension v${result.version} updated and reloaded without CDP.`
|
|
248
|
+
: `Browser extension v${result.version} installed or reloaded through CDP.`);
|
|
239
249
|
console.error(`Extension ID: ${result.extensionId}`);
|
|
240
250
|
console.error(`Extension directory: ${result.path}`);
|
|
241
251
|
return 0;
|
|
242
252
|
}
|
|
243
|
-
console.error(
|
|
253
|
+
console.error(result.filesUpdated
|
|
254
|
+
? `Browser extension v${result.version} files were updated, but the extension was not reloaded.`
|
|
255
|
+
: `Browser extension v${result.version} was not installed.`);
|
|
244
256
|
if (result.cdpError)
|
|
245
257
|
console.error(`CDP installation pending: ${result.cdpError}`);
|
|
246
258
|
console.error('Open chrome://inspect/#remote-debugging, enable remote debugging, approve the connection, then rerun: npx @mearl/setup extension');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +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, findLatestExtensionVersion, installMearlBrowserExtension, inspectMearlExtensionViaCdp, loadUnpackedViaCdp, MEARL_EXTENSION_DIRECTORY_NAME, MEARL_EXTENSION_DOWNLOAD_PAGE, MEARL_EXTENSION_ID, resolveChromeUserDataDirectory, resolveMearlExtensionDirectory, } from './browserExtension.js';
|
|
4
|
-
export type { BrowserExtensionDependencies, BrowserExtensionInstallOptions, BrowserExtensionInstallResult, InstalledBrowserExtension, } from './browserExtension.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';
|
|
5
5
|
export type { AgentBayConfigurationOptions, AgentBayConfigurationResult, AgentBaySecretInput, AgentBaySecretOutput, } from './agentBay.js';
|
|
6
6
|
export type { DetectedGlobalPackage, GlobalPackageCommand, GlobalPackageManager, PackageManagerInstallation, PackageOperation, PackageDetectionOptions, } from './packageManager.js';
|
|
7
7
|
export { runMearlLifecycle } from './lifecycle.js';
|
|
@@ -9,6 +9,6 @@ export type { MearlLifecycleDependencies, MearlLifecycleOperation, MearlLifecycl
|
|
|
9
9
|
export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
|
|
10
10
|
export type { DetectedMearlProfiles, MearlProfile, MearlProfileConfig } from './profiles.js';
|
|
11
11
|
export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
|
|
12
|
-
export { compareMearlReleaseLines, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
|
|
12
|
+
export { compareMearlReleaseLines, compareMearlReleaseVersions, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
|
|
13
13
|
export type { MearlReleaseLine, MearlReleaseVersion } from './version.js';
|
|
14
14
|
export type { NativeHostCleanupDependencies, NativeHostCleanupOptions, NativeHostIntegrationMode, } from './nativeHost.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +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, findLatestExtensionVersion, installMearlBrowserExtension, inspectMearlExtensionViaCdp, loadUnpackedViaCdp, MEARL_EXTENSION_DIRECTORY_NAME, MEARL_EXTENSION_DOWNLOAD_PAGE, MEARL_EXTENSION_ID, resolveChromeUserDataDirectory, resolveMearlExtensionDirectory, } from './browserExtension.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
4
|
export { runMearlLifecycle } from './lifecycle.js';
|
|
5
5
|
export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
|
|
6
6
|
export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
|
|
7
|
-
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.
|
|
3
|
+
"version": "2.9.0",
|
|
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": {
|
|
@@ -52,14 +56,16 @@
|
|
|
52
56
|
],
|
|
53
57
|
"license": "ISC",
|
|
54
58
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
59
|
+
"node": ">=22.12.0"
|
|
56
60
|
},
|
|
57
61
|
"publishConfig": {
|
|
58
62
|
"registry": "https://registry.npmjs.org"
|
|
59
63
|
},
|
|
60
64
|
"dependencies": {
|
|
65
|
+
"commander": "^15.0.0",
|
|
61
66
|
"extract-zip": "^2.0.1",
|
|
62
|
-
"ws": "^8.18.3"
|
|
67
|
+
"ws": "^8.18.3",
|
|
68
|
+
"@mearl/client": "2.9.0"
|
|
63
69
|
},
|
|
64
70
|
"devDependencies": {
|
|
65
71
|
"@types/node": "^24.9.1",
|