@mearl/setup 2.7.3 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/browserExtension.d.ts +45 -0
- package/dist/browserExtension.js +406 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +9 -1
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,28 @@ 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
|
+
安装或后续重载。固定目录和 manifest 中的 `key` 保证扩展 ID 与浏览器存储保持稳定。
|
|
52
|
+
- 下载前会通过 CDP 检查同 ID 插件的来源。未安装,或来源为固定目录中的 `UNPACKED`
|
|
53
|
+
插件时可继续;Chrome 商店、企业策略或其他本地目录安装的版本默认保持不变。只有显式
|
|
54
|
+
传入 `--force` 才会接管并替换这些版本。
|
|
55
|
+
|
|
56
|
+
## 安装或更新浏览器扩展
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @mearl/setup extension
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
如果 Chrome 尚未开放 CDP,交互命令会提示打开
|
|
63
|
+
`chrome://inspect/#remote-debugging`、启用远程调试并允许连接,然后等待安装完成。无 TTY
|
|
64
|
+
环境使用 `npx @mearl/setup extension --yes`;它只尝试一次。CDP 不可用时不会下载或修改
|
|
65
|
+
插件目录,打开 CDP 后重新执行同一命令即可。
|
|
66
|
+
|
|
67
|
+
该命令每次都读取下载页上的最新插件版本,不要求与 setup、CLI 或 Native Host 同版本。
|
|
68
|
+
已经从固定目录安装过时,新版本会先替换目录,再对同一路径调用
|
|
69
|
+
`Extensions.loadUnpacked`,Chrome 会重载现有 unpacked 扩展。
|
|
47
70
|
|
|
48
71
|
## 跟随浏览器扩展自动更新
|
|
49
72
|
|
|
@@ -87,6 +110,9 @@ npx @mearl/setup --local --cloud
|
|
|
87
110
|
# 自动更新当前 PATH 中检测到的全部 Mearl 环境
|
|
88
111
|
npx @mearl/setup update
|
|
89
112
|
|
|
113
|
+
# 独立安装或更新到最新浏览器扩展
|
|
114
|
+
npx @mearl/setup extension
|
|
115
|
+
|
|
90
116
|
# 只卸载本地环境
|
|
91
117
|
npx @mearl/setup uninstall --local
|
|
92
118
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const MEARL_EXTENSION_DOWNLOAD_PAGE = "https://mearl.alibaba-inc.com/";
|
|
2
|
+
export declare const MEARL_EXTENSION_DIRECTORY_NAME = "chrome-extension";
|
|
3
|
+
export declare const MEARL_EXTENSION_ID = "aoehhjnofngknnjefamjbplchbolghkm";
|
|
4
|
+
export interface BrowserExtensionInstallOptions {
|
|
5
|
+
force?: boolean;
|
|
6
|
+
targetDirectory?: string;
|
|
7
|
+
chromeUserDataDirectory?: string;
|
|
8
|
+
waitForCdpMs?: number;
|
|
9
|
+
writeLine?: (line: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface BrowserExtensionInstallResult {
|
|
12
|
+
extensionId?: string;
|
|
13
|
+
loaded: boolean;
|
|
14
|
+
path: string;
|
|
15
|
+
version: string;
|
|
16
|
+
archiveUrl: string;
|
|
17
|
+
cdpError?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BrowserExtensionDependencies {
|
|
20
|
+
extractArchive?: (archivePath: string, destination: string) => Promise<void>;
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
inspectInstallation?: typeof inspectMearlExtensionViaCdp;
|
|
23
|
+
loadUnpacked?: typeof loadUnpackedViaCdp;
|
|
24
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface InstalledBrowserExtension {
|
|
27
|
+
id: string;
|
|
28
|
+
location: string;
|
|
29
|
+
name: string;
|
|
30
|
+
path: string;
|
|
31
|
+
version: string;
|
|
32
|
+
}
|
|
33
|
+
export declare class BrowserExtensionConflictError extends Error {
|
|
34
|
+
readonly installed: InstalledBrowserExtension;
|
|
35
|
+
constructor(installed: InstalledBrowserExtension);
|
|
36
|
+
}
|
|
37
|
+
export declare function resolveChromeUserDataDirectory(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string;
|
|
38
|
+
export declare function resolveMearlExtensionDirectory(home?: string): string;
|
|
39
|
+
export declare function findLatestExtensionVersion(html: string): string | null;
|
|
40
|
+
export declare function extensionArchiveUrl(version: string): string;
|
|
41
|
+
export declare function loadUnpackedViaCdp(extensionDirectory: string, chromeUserDataDirectory?: string): Promise<{
|
|
42
|
+
extensionId: string;
|
|
43
|
+
}>;
|
|
44
|
+
export declare function inspectMearlExtensionViaCdp(chromeUserDataDirectory?: string): Promise<InstalledBrowserExtension | null>;
|
|
45
|
+
export declare function installMearlBrowserExtension(options?: BrowserExtensionInstallOptions, dependencies?: BrowserExtensionDependencies): Promise<BrowserExtensionInstallResult>;
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import extractZip from 'extract-zip';
|
|
6
|
+
import WebSocket from 'ws';
|
|
7
|
+
import { isMearlReleaseVersion, parseMearlReleaseVersion } from './version.js';
|
|
8
|
+
export const MEARL_EXTENSION_DOWNLOAD_PAGE = 'https://mearl.alibaba-inc.com/';
|
|
9
|
+
export const MEARL_EXTENSION_DIRECTORY_NAME = 'chrome-extension';
|
|
10
|
+
export const MEARL_EXTENSION_ID = 'aoehhjnofngknnjefamjbplchbolghkm';
|
|
11
|
+
const ARCHIVE_PATTERN = /mearl-chrome-v(\d+\.\d+\.\d+(?:-beta\.\d+)?)\.zip/g;
|
|
12
|
+
const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
export class BrowserExtensionConflictError extends Error {
|
|
14
|
+
installed;
|
|
15
|
+
constructor(installed) {
|
|
16
|
+
super(`Mearl extension ${installed.id} is already installed from ${installed.location} at ${installed.path || '(managed location)'}. Re-run with --force to replace it.`);
|
|
17
|
+
this.installed = installed;
|
|
18
|
+
this.name = 'BrowserExtensionConflictError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
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
|
+
export function resolveChromeUserDataDirectory(platform = process.platform, home = os.homedir(), env = process.env) {
|
|
33
|
+
if (platform === 'darwin') {
|
|
34
|
+
return path.join(home, 'Library', 'Application Support', 'Google', 'Chrome');
|
|
35
|
+
}
|
|
36
|
+
if (platform === 'win32') {
|
|
37
|
+
return path.join(env.LOCALAPPDATA ?? home, 'Google', 'Chrome', 'User Data');
|
|
38
|
+
}
|
|
39
|
+
return path.join(home, '.config', 'google-chrome');
|
|
40
|
+
}
|
|
41
|
+
export function resolveMearlExtensionDirectory(home = os.homedir()) {
|
|
42
|
+
return path.join(home, '.mearl', MEARL_EXTENSION_DIRECTORY_NAME);
|
|
43
|
+
}
|
|
44
|
+
export function findLatestExtensionVersion(html) {
|
|
45
|
+
const versions = Array.from(html.matchAll(ARCHIVE_PATTERN), match => match[1]).filter(isMearlReleaseVersion);
|
|
46
|
+
return versions.sort(compareVersions).at(-1) ?? null;
|
|
47
|
+
}
|
|
48
|
+
export function extensionArchiveUrl(version) {
|
|
49
|
+
if (!isMearlReleaseVersion(version)) {
|
|
50
|
+
throw new Error(`Invalid Mearl browser extension version: ${version}`);
|
|
51
|
+
}
|
|
52
|
+
return new URL(`mearl-chrome-v${version}.zip`, MEARL_EXTENSION_DOWNLOAD_PAGE).toString();
|
|
53
|
+
}
|
|
54
|
+
async function resolveExtensionArchive(fetchImpl) {
|
|
55
|
+
const response = await fetchImpl(MEARL_EXTENSION_DOWNLOAD_PAGE, { redirect: 'follow' });
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Extension release page returned HTTP ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
const version = findLatestExtensionVersion(await response.text());
|
|
60
|
+
if (!version)
|
|
61
|
+
throw new Error('No Mearl browser extension archive was found on the release page');
|
|
62
|
+
return { version, archiveUrl: extensionArchiveUrl(version) };
|
|
63
|
+
}
|
|
64
|
+
function readDevToolsActivePort(userDataDirectory) {
|
|
65
|
+
try {
|
|
66
|
+
const content = fs
|
|
67
|
+
.readFileSync(path.join(userDataDirectory, 'DevToolsActivePort'), 'utf8')
|
|
68
|
+
.trim();
|
|
69
|
+
const [portValue, browserWsPath = '/devtools/browser'] = content
|
|
70
|
+
.split(/\r?\n/)
|
|
71
|
+
.map(value => value.trim());
|
|
72
|
+
const port = Number(portValue);
|
|
73
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535)
|
|
74
|
+
return null;
|
|
75
|
+
if (!browserWsPath.startsWith('/'))
|
|
76
|
+
return null;
|
|
77
|
+
return { port, browserWsPath };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function rawDataToString(data) {
|
|
84
|
+
if (Array.isArray(data))
|
|
85
|
+
return Buffer.concat(data).toString('utf8');
|
|
86
|
+
if (Buffer.isBuffer(data))
|
|
87
|
+
return data.toString('utf8');
|
|
88
|
+
return Buffer.from(data).toString('utf8');
|
|
89
|
+
}
|
|
90
|
+
class CdpConnection {
|
|
91
|
+
socket;
|
|
92
|
+
closed = false;
|
|
93
|
+
nextId = 1;
|
|
94
|
+
pending = new Map();
|
|
95
|
+
constructor(socket) {
|
|
96
|
+
this.socket = socket;
|
|
97
|
+
socket.on('message', data => this.handleMessage(data));
|
|
98
|
+
socket.on('close', () => this.close(new Error('CDP WebSocket closed before responding')));
|
|
99
|
+
socket.on('error', error => this.close(error));
|
|
100
|
+
}
|
|
101
|
+
static connect(endpoint) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const socket = new WebSocket(endpoint, {
|
|
104
|
+
handshakeTimeout: 5_000,
|
|
105
|
+
maxPayload: 8 * 1024 * 1024,
|
|
106
|
+
perMessageDeflate: false,
|
|
107
|
+
});
|
|
108
|
+
let settled = false;
|
|
109
|
+
const finish = (error) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
if (error) {
|
|
114
|
+
socket.terminate();
|
|
115
|
+
reject(error);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
resolve(new CdpConnection(socket));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
socket.once('open', () => finish());
|
|
122
|
+
socket.once('error', error => finish(error));
|
|
123
|
+
socket.once('unexpected-response', (_request, response) => {
|
|
124
|
+
response.resume();
|
|
125
|
+
finish(new Error(`CDP WebSocket returned HTTP ${response.statusCode ?? 0}`));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
send(method, params = {}, sessionId, timeoutMs = 10_000) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const id = this.nextId++;
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
this.pending.delete(id);
|
|
134
|
+
reject(new Error(`CDP command timed out: ${method}`));
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
this.pending.set(id, { reject, resolve, timer });
|
|
137
|
+
this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }), error => {
|
|
138
|
+
if (!error)
|
|
139
|
+
return;
|
|
140
|
+
const pending = this.pending.get(id);
|
|
141
|
+
if (!pending)
|
|
142
|
+
return;
|
|
143
|
+
clearTimeout(pending.timer);
|
|
144
|
+
this.pending.delete(id);
|
|
145
|
+
pending.reject(error);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
close(error = new Error('CDP connection closed')) {
|
|
150
|
+
if (this.closed)
|
|
151
|
+
return;
|
|
152
|
+
this.closed = true;
|
|
153
|
+
for (const pending of this.pending.values()) {
|
|
154
|
+
clearTimeout(pending.timer);
|
|
155
|
+
pending.reject(error);
|
|
156
|
+
}
|
|
157
|
+
this.pending.clear();
|
|
158
|
+
this.socket.terminate();
|
|
159
|
+
}
|
|
160
|
+
handleMessage(data) {
|
|
161
|
+
try {
|
|
162
|
+
const message = JSON.parse(rawDataToString(data));
|
|
163
|
+
if (message.id === undefined)
|
|
164
|
+
return;
|
|
165
|
+
const pending = this.pending.get(message.id);
|
|
166
|
+
if (!pending)
|
|
167
|
+
return;
|
|
168
|
+
clearTimeout(pending.timer);
|
|
169
|
+
this.pending.delete(message.id);
|
|
170
|
+
if (message.error) {
|
|
171
|
+
pending.reject(new Error(message.error.message ?? 'CDP command failed'));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
pending.resolve(message.result ?? {});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Ignore unrelated malformed protocol events and let command timeouts report failure.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function sameExtensionPath(left, right) {
|
|
183
|
+
const normalizedLeft = path.normalize(path.resolve(left));
|
|
184
|
+
const normalizedRight = path.normalize(path.resolve(right));
|
|
185
|
+
return process.platform === 'win32'
|
|
186
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
187
|
+
: normalizedLeft === normalizedRight;
|
|
188
|
+
}
|
|
189
|
+
function isSetupManagedInstallation(installed, targetDirectory) {
|
|
190
|
+
return installed.location === 'UNPACKED' && sameExtensionPath(installed.path, targetDirectory);
|
|
191
|
+
}
|
|
192
|
+
function browserCdpEndpoint(chromeUserDataDirectory) {
|
|
193
|
+
const activePort = readDevToolsActivePort(chromeUserDataDirectory);
|
|
194
|
+
if (!activePort)
|
|
195
|
+
throw new Error('Chrome remote debugging is not enabled');
|
|
196
|
+
return `ws://127.0.0.1:${activePort.port}${activePort.browserWsPath}`;
|
|
197
|
+
}
|
|
198
|
+
export async function loadUnpackedViaCdp(extensionDirectory, chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
|
|
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));
|
|
222
|
+
const target = await connection.send('Target.createTarget', {
|
|
223
|
+
background: true,
|
|
224
|
+
url: 'chrome://extensions-internals',
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
const attached = await connection.send('Target.attachToTarget', {
|
|
228
|
+
flatten: true,
|
|
229
|
+
targetId: target.targetId,
|
|
230
|
+
});
|
|
231
|
+
const evaluated = await connection.send('Runtime.evaluate', {
|
|
232
|
+
awaitPromise: true,
|
|
233
|
+
expression: "document.readyState === 'complete' ? document.body.innerText : new Promise(resolve => addEventListener('load', () => resolve(document.body.innerText), {once:true}))",
|
|
234
|
+
returnByValue: true,
|
|
235
|
+
}, attached.sessionId);
|
|
236
|
+
const text = evaluated.result?.value;
|
|
237
|
+
if (typeof text !== 'string') {
|
|
238
|
+
throw new Error('chrome://extensions-internals did not return extension inventory');
|
|
239
|
+
}
|
|
240
|
+
const inventory = JSON.parse(text);
|
|
241
|
+
const installed = inventory.find(extension => extension.id === MEARL_EXTENSION_ID);
|
|
242
|
+
if (!installed)
|
|
243
|
+
return null;
|
|
244
|
+
if (typeof installed.location !== 'string' ||
|
|
245
|
+
typeof installed.name !== 'string' ||
|
|
246
|
+
typeof installed.path !== 'string' ||
|
|
247
|
+
typeof installed.version !== 'string') {
|
|
248
|
+
throw new Error('Chrome returned incomplete Mearl extension metadata');
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
id: MEARL_EXTENSION_ID,
|
|
252
|
+
location: installed.location,
|
|
253
|
+
name: installed.name,
|
|
254
|
+
path: installed.path,
|
|
255
|
+
version: installed.version,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
if (typeof target.targetId === 'string') {
|
|
260
|
+
await connection.send('Target.closeTarget', { targetId: target.targetId }).catch(() => { });
|
|
261
|
+
}
|
|
262
|
+
connection.close();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function replaceDirectory(stagingDirectory, targetDirectory) {
|
|
266
|
+
const backupDirectory = `${targetDirectory}.backup-${randomUUID()}`;
|
|
267
|
+
const targetExists = fs.existsSync(targetDirectory);
|
|
268
|
+
try {
|
|
269
|
+
if (targetExists)
|
|
270
|
+
await fs.promises.rename(targetDirectory, backupDirectory);
|
|
271
|
+
await fs.promises.rename(stagingDirectory, targetDirectory);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (!fs.existsSync(targetDirectory) && fs.existsSync(backupDirectory)) {
|
|
275
|
+
await fs.promises.rename(backupDirectory, targetDirectory);
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (targetExists) {
|
|
280
|
+
await fs.promises.rm(backupDirectory, { force: true, recursive: true });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function readExtensionManifest(directory) {
|
|
284
|
+
const manifestPath = path.join(directory, 'manifest.json');
|
|
285
|
+
let parsed;
|
|
286
|
+
try {
|
|
287
|
+
parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
throw new Error(`Downloaded extension has no valid manifest.json: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
291
|
+
}
|
|
292
|
+
if (parsed.manifest_version !== 3 ||
|
|
293
|
+
typeof parsed.name !== 'string' ||
|
|
294
|
+
typeof parsed.key !== 'string' ||
|
|
295
|
+
parsed.key.length === 0) {
|
|
296
|
+
throw new Error('Downloaded archive is not a valid Manifest V3 extension');
|
|
297
|
+
}
|
|
298
|
+
if (!isMearlReleaseVersion(parsed.version)) {
|
|
299
|
+
throw new Error('Downloaded extension manifest has an invalid version');
|
|
300
|
+
}
|
|
301
|
+
return parsed;
|
|
302
|
+
}
|
|
303
|
+
export async function installMearlBrowserExtension(options = {}, dependencies = {}) {
|
|
304
|
+
const fetchImpl = dependencies.fetch ?? fetch;
|
|
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());
|
|
313
|
+
const parentDirectory = path.dirname(targetDirectory);
|
|
314
|
+
const stagingDirectory = `${targetDirectory}.staging-${randomUUID()}`;
|
|
315
|
+
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
|
+
await fs.promises.mkdir(parentDirectory, { recursive: true });
|
|
361
|
+
try {
|
|
362
|
+
const response = await fetchImpl(release.archiveUrl, { redirect: 'follow' });
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
throw new Error(`Extension archive returned HTTP ${response.status}: ${release.archiveUrl}`);
|
|
365
|
+
}
|
|
366
|
+
const contentLength = Number(response.headers.get('content-length') ?? 0);
|
|
367
|
+
if (contentLength > MAX_ARCHIVE_BYTES) {
|
|
368
|
+
throw new Error(`Extension archive exceeds ${MAX_ARCHIVE_BYTES} bytes`);
|
|
369
|
+
}
|
|
370
|
+
const archive = Buffer.from(await response.arrayBuffer());
|
|
371
|
+
if (archive.length === 0 || archive.length > MAX_ARCHIVE_BYTES) {
|
|
372
|
+
throw new Error('Extension archive is empty or too large');
|
|
373
|
+
}
|
|
374
|
+
await fs.promises.writeFile(archivePath, archive, { mode: 0o600 });
|
|
375
|
+
await fs.promises.mkdir(stagingDirectory, { recursive: true });
|
|
376
|
+
await extractArchive(archivePath, stagingDirectory);
|
|
377
|
+
const manifest = readExtensionManifest(stagingDirectory);
|
|
378
|
+
if (manifest.version !== release.version) {
|
|
379
|
+
throw new Error(`Extension archive version ${String(manifest.version)} does not match ${release.version}`);
|
|
380
|
+
}
|
|
381
|
+
await replaceDirectory(stagingDirectory, targetDirectory);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
await fs.promises.rm(archivePath, { force: true });
|
|
385
|
+
await fs.promises.rm(stagingDirectory, { force: true, recursive: true });
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const loaded = await loadUnpacked(targetDirectory, chromeUserDataDirectory);
|
|
389
|
+
return {
|
|
390
|
+
extensionId: loaded.extensionId,
|
|
391
|
+
loaded: true,
|
|
392
|
+
path: targetDirectory,
|
|
393
|
+
version: release.version,
|
|
394
|
+
archiveUrl: release.archiveUrl,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
return {
|
|
399
|
+
loaded: false,
|
|
400
|
+
path: targetDirectory,
|
|
401
|
+
version: release.version,
|
|
402
|
+
archiveUrl: release.archiveUrl,
|
|
403
|
+
cdpError: error instanceof Error ? error.message : String(error),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
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,34 @@ 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(`Browser extension v${result.version} installed or reloaded through CDP.`);
|
|
239
|
+
console.error(`Extension ID: ${result.extensionId}`);
|
|
240
|
+
console.error(`Extension directory: ${result.path}`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
console.error(`Browser extension v${result.version} was not installed.`);
|
|
244
|
+
if (result.cdpError)
|
|
245
|
+
console.error(`CDP installation pending: ${result.cdpError}`);
|
|
246
|
+
console.error('Open chrome://inspect/#remote-debugging, enable remote debugging, approve the connection, then rerun: npx @mearl/setup extension');
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
206
254
|
if (!interactiveTerminal && !parsed.yes && parsed.operation !== 'update') {
|
|
207
255
|
console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
|
|
208
256
|
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, 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
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';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/setup",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "One-shot environment setup and configuration for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -51,11 +51,19 @@
|
|
|
51
51
|
"uninstall"
|
|
52
52
|
],
|
|
53
53
|
"license": "ISC",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
54
57
|
"publishConfig": {
|
|
55
58
|
"registry": "https://registry.npmjs.org"
|
|
56
59
|
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"extract-zip": "^2.0.1",
|
|
62
|
+
"ws": "^8.18.3"
|
|
63
|
+
},
|
|
57
64
|
"devDependencies": {
|
|
58
65
|
"@types/node": "^24.9.1",
|
|
66
|
+
"@types/ws": "^8.18.1",
|
|
59
67
|
"typescript": "^5.8.3"
|
|
60
68
|
},
|
|
61
69
|
"scripts": {
|