@mearl/setup 2.10.0 → 2.12.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 +47 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +58 -0
- package/dist/packageManager.d.ts +1 -0
- package/dist/packageManager.js +13 -11
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.js +185 -0
- package/dist/pluginRuntime.d.ts +4 -0
- package/dist/pluginRuntime.js +57 -0
- package/dist/process.d.ts +2 -0
- package/dist/process.js +17 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@ Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全
|
|
|
6
6
|
npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp]
|
|
7
7
|
npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp]
|
|
8
8
|
npx @mearl/setup extension [--full] [--force]
|
|
9
|
+
npx @mearl/setup plugin <name> <path>
|
|
10
|
+
npx @mearl/setup plugin install <ali-skills add 参数...>
|
|
11
|
+
npx @mearl/setup plugin uninstall <name>
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
配置 AgentBay API Key 时只需执行一次下面的命令,然后在隐藏输入提示中粘贴 Key。
|
|
@@ -23,6 +26,50 @@ npx @mearl/setup agentbay
|
|
|
23
26
|
模式记录为 Qoder;`mearl-cloud-server` 的命令交互保持不变,`start` 和 `restart` 输出新的
|
|
24
27
|
配对指令后退出,不监听端口或保留守护进程。配对 URL 不包含 Qoder PAT。
|
|
25
28
|
|
|
29
|
+
## 安装与注册 Skill CLI 插件
|
|
30
|
+
|
|
31
|
+
一个命令安装 skill 并注册它的 CLI 能力:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @mearl/setup plugin install trip/mearl --skill yuque-doc-fetch --yes
|
|
35
|
+
mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
安装参数透传给 `npx -y ali-skills add`,末尾统一追加 `--global --install-mode symlink`;不接受会截断后置选项的 `--` 参数分隔符,
|
|
39
|
+
确保 skill 存放在 `~/.agents/skills` 并共享给所选 agent。安装日志实时显示,保留交互;
|
|
40
|
+
setup 从 `Installed … skill(s)` 成功区块提取路径,仅注册本次安装且包含 `scripts/main.mjs` 的 skill。
|
|
41
|
+
普通 skill 正常安装。输出格式无法识别时提示手动注册,不扫描其他 skill 或猜测安装结果。
|
|
42
|
+
|
|
43
|
+
自动注册默认使用安装目录名;入口可用 `export const name = 'yuque'` 指定简短命令名。
|
|
44
|
+
重装会刷新同名绑定。每个入口必须默认导出 `register(app)`;完整协议见
|
|
45
|
+
[CLI 插件规范](../skills/mearl-skill-creator/references/cli-plugin.md)。
|
|
46
|
+
|
|
47
|
+
已通过其他渠道分发的 skill 或自定义入口,可以直接传入路径注册:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @mearl/setup plugin yuque /absolute/path/to/yuque-doc-fetch
|
|
51
|
+
npx @mearl/setup plugin custom ./skills/custom/entry.mjs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
path 接受目录或具体文件;目录默认读取 `scripts/main.mjs`,相对路径基于调用工作目录解析。
|
|
55
|
+
命令检查入口注册函数,然后保存绝对入口路径,同名注册更新绑定。
|
|
56
|
+
两种注册流程都会检查本机 `@mearl/client` 和 `@mearl/native-host`:缺失或低于当前 setup 版本时,
|
|
57
|
+
复用已有包管理器安装 setup 的对应版本,更新后再次验证;更高版本保持不动。
|
|
58
|
+
native-host 会幂等初始化 Skills 模式。基础环境补齐或初始化失败时保留已安装的 skill,并停止注册。
|
|
59
|
+
注册后可在任意目录运行 `mearl <name> --help`,日常业务调用不执行安装或版本检查。
|
|
60
|
+
|
|
61
|
+
不再需要插件时执行:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @mearl/setup plugin uninstall yuque
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
若入口位于 `~/.agents/skills/<skill>/`,setup 会先执行
|
|
68
|
+
`ali-skills remove <skill> --global --yes`,成功后清除该 Skill 对应的全部插件注册;
|
|
69
|
+
入口目录已被手动删除时会让 Ali Skills 仍能识别本次移除,从而一并清理其跟踪记录。
|
|
70
|
+
移除失败时保留注册以便重试。手工注册的其他路径只解除 Mearl 注册,不删除源码。
|
|
71
|
+
重复卸载未注册名称会成功返回,不产生改动。
|
|
72
|
+
|
|
26
73
|
## 行为
|
|
27
74
|
|
|
28
75
|
- `install` 可以省略;直接执行 `npx @mearl/setup` 等同于 `npx @mearl/setup install`。
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile,
|
|
|
3
3
|
import { installMearlBrowserExtension } from './browserExtension.js';
|
|
4
4
|
import { runMearlLifecycle, type MearlLifecycleOperation } from './lifecycle.js';
|
|
5
5
|
import type { MearlProfile } from './profiles.js';
|
|
6
|
+
import { installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
|
|
6
7
|
export declare const SETUP_VERSION: string;
|
|
7
8
|
interface ParsedArgs {
|
|
8
9
|
agentBayKeyFile?: string;
|
|
@@ -11,7 +12,11 @@ interface ParsedArgs {
|
|
|
11
12
|
force?: boolean;
|
|
12
13
|
full?: boolean;
|
|
13
14
|
help: boolean;
|
|
14
|
-
operation?: MearlLifecycleOperation | 'agentbay' | 'extension';
|
|
15
|
+
operation?: MearlLifecycleOperation | 'agentbay' | 'extension' | 'plugin';
|
|
16
|
+
pluginName?: string;
|
|
17
|
+
pluginPath?: string;
|
|
18
|
+
pluginInstallArgs?: string[];
|
|
19
|
+
pluginUninstallName?: string;
|
|
15
20
|
profiles?: MearlProfile[];
|
|
16
21
|
skipSkill: boolean;
|
|
17
22
|
targetLine?: string;
|
|
@@ -19,6 +24,9 @@ interface ParsedArgs {
|
|
|
19
24
|
yes: boolean;
|
|
20
25
|
}
|
|
21
26
|
export interface SetupCliDependencies {
|
|
27
|
+
registerPlugin?: typeof setupPlugin;
|
|
28
|
+
installPlugins?: typeof installPlugins;
|
|
29
|
+
uninstallPlugin?: typeof uninstallPlugin;
|
|
22
30
|
configureAgentBay?: typeof configureAgentBayApiKey;
|
|
23
31
|
promptAgentBayKey?: typeof promptAgentBayApiKey;
|
|
24
32
|
readAgentBayKeyFile?: typeof readAgentBayApiKeyFile;
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile,
|
|
|
5
5
|
import { installMearlBrowserExtension } from './browserExtension.js';
|
|
6
6
|
import { runMearlLifecycle } from './lifecycle.js';
|
|
7
7
|
import { isMearlReleaseLine } from './version.js';
|
|
8
|
+
import { installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
export const SETUP_VERSION = require('../package.json').version;
|
|
10
11
|
const baseParsedArgs = () => ({
|
|
@@ -24,6 +25,15 @@ function commanderError(error) {
|
|
|
24
25
|
return new Error(message);
|
|
25
26
|
}
|
|
26
27
|
export function parseSetupArgs(argv) {
|
|
28
|
+
if (argv[0] === 'plugin' && argv[1] === 'install') {
|
|
29
|
+
return { ...baseParsedArgs(), operation: 'plugin', pluginInstallArgs: argv.slice(2) };
|
|
30
|
+
}
|
|
31
|
+
if (argv[0] === 'plugin' && argv[1] === 'uninstall') {
|
|
32
|
+
if (argv.length !== 3 || !argv[2]) {
|
|
33
|
+
throw new Error('plugin uninstall requires exactly one plugin name');
|
|
34
|
+
}
|
|
35
|
+
return { ...baseParsedArgs(), operation: 'plugin', pluginUninstallName: argv[2] };
|
|
36
|
+
}
|
|
27
37
|
let parsed;
|
|
28
38
|
const program = new Command()
|
|
29
39
|
.name('mearl-setup')
|
|
@@ -77,6 +87,14 @@ export function parseSetupArgs(argv) {
|
|
|
77
87
|
addLifecycleCommand('install');
|
|
78
88
|
addLifecycleCommand('update');
|
|
79
89
|
addLifecycleCommand('uninstall');
|
|
90
|
+
program
|
|
91
|
+
.command('plugin')
|
|
92
|
+
.argument('<name>')
|
|
93
|
+
.argument('<path>')
|
|
94
|
+
.allowExcessArguments(false)
|
|
95
|
+
.action((pluginName, pluginPath) => {
|
|
96
|
+
parsed = { ...baseParsedArgs(), operation: 'plugin', pluginName, pluginPath };
|
|
97
|
+
});
|
|
80
98
|
const extension = new Command('extension')
|
|
81
99
|
.exitOverride()
|
|
82
100
|
.configureOutput({ writeErr: () => { }, writeOut: () => { } })
|
|
@@ -145,6 +163,9 @@ Usage:
|
|
|
145
163
|
npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
|
|
146
164
|
npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
|
|
147
165
|
npx @mearl/setup extension [--full] [--force] [--yes]
|
|
166
|
+
npx @mearl/setup plugin <name> <path>
|
|
167
|
+
npx @mearl/setup plugin install <ali-skills add arguments...>
|
|
168
|
+
npx @mearl/setup plugin uninstall <name>
|
|
148
169
|
npx @mearl/setup agentbay [--stdin | --key-file <path>]
|
|
149
170
|
|
|
150
171
|
Behavior:
|
|
@@ -153,6 +174,7 @@ Behavior:
|
|
|
153
174
|
uninstall Auto-detects installed environments and asks for confirmation
|
|
154
175
|
extension Install or update the browser extension; --full also installs the local environment
|
|
155
176
|
agentbay Save or update AGENTBAY_KEY without changing other Mearl settings
|
|
177
|
+
plugin Register, install, or uninstall a CLI plugin
|
|
156
178
|
|
|
157
179
|
Options:
|
|
158
180
|
--local Manage @mearl/native-host and @mearl/client
|
|
@@ -183,6 +205,7 @@ Examples:
|
|
|
183
205
|
npx @mearl/setup update
|
|
184
206
|
npx @mearl/setup extension
|
|
185
207
|
npx @mearl/setup extension --full
|
|
208
|
+
npx @mearl/setup plugin uninstall yuque
|
|
186
209
|
npx @mearl/setup agentbay
|
|
187
210
|
npx @mearl/setup --qoder-cloud --yes
|
|
188
211
|
npx @mearl/setup update --local --target-line 2.3.x --skip-skill
|
|
@@ -210,6 +233,41 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
|
|
|
210
233
|
console.log(SETUP_VERSION);
|
|
211
234
|
return 0;
|
|
212
235
|
}
|
|
236
|
+
if (parsed.operation === 'plugin') {
|
|
237
|
+
try {
|
|
238
|
+
if (parsed.pluginInstallArgs) {
|
|
239
|
+
return await (dependencies.installPlugins ?? installPlugins)(parsed.pluginInstallArgs);
|
|
240
|
+
}
|
|
241
|
+
if (parsed.pluginUninstallName) {
|
|
242
|
+
const result = await (dependencies.uninstallPlugin ?? uninstallPlugin)(parsed.pluginUninstallName);
|
|
243
|
+
if (!result.removed) {
|
|
244
|
+
console.error(`Plugin ${parsed.pluginUninstallName} is not registered.`);
|
|
245
|
+
}
|
|
246
|
+
else if (result.skillName) {
|
|
247
|
+
console.error(result.entryMissing
|
|
248
|
+
? `Plugin ${parsed.pluginUninstallName} had a missing entry; ali-skills cleanup completed for ${result.skillName}.`
|
|
249
|
+
: `Plugin ${parsed.pluginUninstallName} uninstalled with ali-skills skill ${result.skillName}.`);
|
|
250
|
+
if (result.unregisteredNames.length > 1) {
|
|
251
|
+
console.error(`Removed registrations: ${result.unregisteredNames.join(', ')}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.error(result.entryMissing
|
|
256
|
+
? `Plugin ${parsed.pluginUninstallName} stale registration removed; the external entry was already missing.`
|
|
257
|
+
: `Plugin ${parsed.pluginUninstallName} unregistered; external skill files were kept.`);
|
|
258
|
+
}
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
const entry = await (dependencies.registerPlugin ?? setupPlugin)(parsed.pluginName, parsed.pluginPath);
|
|
262
|
+
console.error(`Plugin ${parsed.pluginName} registered: ${entry}`);
|
|
263
|
+
console.error(`Run: mearl ${parsed.pluginName} --help`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
213
271
|
if (parsed.operation === 'agentbay') {
|
|
214
272
|
try {
|
|
215
273
|
const configureAgentBay = dependencies.configureAgentBay ?? configureAgentBayApiKey;
|
package/dist/packageManager.d.ts
CHANGED
|
@@ -32,4 +32,5 @@ export declare function resolveInvokingPackageManager(options?: {
|
|
|
32
32
|
platform?: NodeJS.Platform;
|
|
33
33
|
processExecPath?: string;
|
|
34
34
|
}): PackageManagerInstallation;
|
|
35
|
+
export declare function detectCliPackage(packageName: string, binNames: string[], options?: PackageDetectionOptions): Pick<DetectedGlobalPackage, 'packageName' | 'packageRoot' | 'executable'> | null;
|
|
35
36
|
export declare function detectGlobalPackage(packageName: string, binNames: string[], options?: PackageDetectionOptions): DetectedGlobalPackage | null;
|
package/dist/packageManager.js
CHANGED
|
@@ -188,7 +188,7 @@ function packageRootFromWrapper(executable, packageName, readFileSync) {
|
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
export function
|
|
191
|
+
export function detectCliPackage(packageName, binNames, options = {}) {
|
|
192
192
|
const env = options.env ?? process.env;
|
|
193
193
|
const platform = options.platform ?? process.platform;
|
|
194
194
|
const existsSync = options.existsSync ?? (candidate => isExecutable(candidate, platform));
|
|
@@ -215,20 +215,22 @@ export function detectGlobalPackage(packageName, binNames, options = {}) {
|
|
|
215
215
|
packageRoot ??= packageRootFromWrapper(executable, packageName, readFileSync);
|
|
216
216
|
if (!packageRoot)
|
|
217
217
|
continue;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
return { packageName, packageRoot, executable };
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
export function detectGlobalPackage(packageName, binNames, options = {}) {
|
|
223
|
+
for (const binName of binNames) {
|
|
224
|
+
const cli = detectCliPackage(packageName, [binName], options);
|
|
225
|
+
if (!cli)
|
|
226
|
+
continue;
|
|
227
|
+
const installation = resolvePackageManagerInstallation(cli.packageRoot, packageName, options);
|
|
224
228
|
if (!installation)
|
|
225
229
|
continue;
|
|
226
230
|
return {
|
|
227
|
-
|
|
228
|
-
packageRoot,
|
|
229
|
-
executable,
|
|
231
|
+
...cli,
|
|
230
232
|
...installation,
|
|
231
|
-
binDirectory: path.dirname(executable),
|
|
233
|
+
binDirectory: path.dirname(cli.executable),
|
|
232
234
|
};
|
|
233
235
|
}
|
|
234
236
|
return null;
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CommandRunner } from './process.js';
|
|
2
|
+
export declare function setupPlugin(name: string, target: string): Promise<string>;
|
|
3
|
+
export interface PluginUninstallResult {
|
|
4
|
+
entryMissing?: boolean;
|
|
5
|
+
removed: boolean;
|
|
6
|
+
skillName?: string;
|
|
7
|
+
unregisteredNames: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function uninstallPlugin(name: string, runner?: CommandRunner): Promise<PluginUninstallResult>;
|
|
10
|
+
/** Ali Skills 的 Installed 区块列出成功路径;计数校验避免输出变化导致漏注册。 */
|
|
11
|
+
export declare function parseInstalledPluginPaths(output: string): string[];
|
|
12
|
+
export declare function installPlugins(args: string[], runner?: CommandRunner): Promise<number>;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { assertPluginName, findPluginEntry, listPluginNames, loadPlugin, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
5
|
+
import { ensurePluginRuntime } from './pluginRuntime.js';
|
|
6
|
+
import { resolveAliSkillsInvocation, stripAnsi } from './aliSkills.js';
|
|
7
|
+
import { findExecutable, prependNodePath, runCommand } from './process.js';
|
|
8
|
+
const registrationContext = {
|
|
9
|
+
options: {},
|
|
10
|
+
invoke: async () => {
|
|
11
|
+
throw new Error('注册时不能执行浏览器操作');
|
|
12
|
+
},
|
|
13
|
+
progress: () => {
|
|
14
|
+
throw new Error('注册时不能报告执行进度');
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export async function setupPlugin(name, target) {
|
|
18
|
+
assertPluginName(name);
|
|
19
|
+
const entry = resolvePluginEntry(target);
|
|
20
|
+
await loadPlugin(entry, registrationContext);
|
|
21
|
+
await ensurePluginRuntime();
|
|
22
|
+
return registerPlugin(name, entry);
|
|
23
|
+
}
|
|
24
|
+
function globalAliSkill(entry) {
|
|
25
|
+
const globalSkillsDirectory = join(homedir(), '.agents', 'skills');
|
|
26
|
+
const relativeEntry = relative(globalSkillsDirectory, resolve(entry));
|
|
27
|
+
if (isAbsolute(relativeEntry) || relativeEntry === '..' || relativeEntry.startsWith(`..${sep}`))
|
|
28
|
+
return undefined;
|
|
29
|
+
const [name, ...entrySegments] = relativeEntry.split(sep);
|
|
30
|
+
if (!name || entrySegments.length === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
return { directory: join(globalSkillsDirectory, name), name };
|
|
33
|
+
}
|
|
34
|
+
export async function uninstallPlugin(name, runner = runCommand) {
|
|
35
|
+
assertPluginName(name);
|
|
36
|
+
const entry = findPluginEntry(name);
|
|
37
|
+
if (!entry)
|
|
38
|
+
return { removed: false, unregisteredNames: [] };
|
|
39
|
+
const entryMissing = !existsSync(entry);
|
|
40
|
+
const managedSkill = globalAliSkill(entry);
|
|
41
|
+
if (!managedSkill) {
|
|
42
|
+
unregisterPlugin(name);
|
|
43
|
+
return {
|
|
44
|
+
...(entryMissing ? { entryMissing: true } : {}),
|
|
45
|
+
removed: true,
|
|
46
|
+
unregisteredNames: [name],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const invocation = resolveAliSkillsInvocation();
|
|
50
|
+
if (!invocation)
|
|
51
|
+
throw new Error('未找到 ali-skills 或 npx,插件注册已保留。');
|
|
52
|
+
const { directory: skillDirectory, name: skillName } = managedSkill;
|
|
53
|
+
let placeholderCreated = false;
|
|
54
|
+
if (!existsSync(skillDirectory)) {
|
|
55
|
+
mkdirSync(dirname(skillDirectory), { recursive: true, mode: 0o700 });
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(skillDirectory, { mode: 0o700 });
|
|
58
|
+
placeholderCreated = true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== 'EEXIST')
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let removed;
|
|
66
|
+
try {
|
|
67
|
+
removed = await runner(invocation.command, [...invocation.argsPrefix, 'remove', skillName, '--global', '--yes'], { env: invocation.env, tee: true });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`ali-skills remove ${skillName} 执行失败:${error instanceof Error ? error.message : String(error)};插件注册已保留。`, { cause: error });
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (placeholderCreated) {
|
|
74
|
+
try {
|
|
75
|
+
rmdirSync(skillDirectory);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* ali-skills 已移除,或目录中出现了新内容。 */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (removed.code !== 0) {
|
|
83
|
+
throw new Error(`ali-skills remove ${skillName} 失败(exit ${removed.code});插件注册已保留。`);
|
|
84
|
+
}
|
|
85
|
+
const unregisteredNames = listPluginNames().filter(pluginName => {
|
|
86
|
+
try {
|
|
87
|
+
const candidate = findPluginEntry(pluginName);
|
|
88
|
+
return candidate !== undefined && globalAliSkill(candidate)?.directory === skillDirectory;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
for (const pluginName of unregisteredNames)
|
|
95
|
+
unregisterPlugin(pluginName);
|
|
96
|
+
return {
|
|
97
|
+
...(entryMissing ? { entryMissing: true } : {}),
|
|
98
|
+
removed: true,
|
|
99
|
+
skillName,
|
|
100
|
+
unregisteredNames,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Ali Skills 的 Installed 区块列出成功路径;计数校验避免输出变化导致漏注册。 */
|
|
104
|
+
export function parseInstalledPluginPaths(output) {
|
|
105
|
+
const paths = [];
|
|
106
|
+
let expected = 0;
|
|
107
|
+
let inResult = false;
|
|
108
|
+
for (const line of stripAnsi(output).split(/\r?\n/)) {
|
|
109
|
+
const heading = line.match(/\bInstalled (\d+) skills?\b/);
|
|
110
|
+
if (heading) {
|
|
111
|
+
expected += Number(heading[1]);
|
|
112
|
+
inResult = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!inResult)
|
|
116
|
+
continue;
|
|
117
|
+
if (/[╰└├]/.test(line)) {
|
|
118
|
+
inResult = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const item = line.trim().match(/^[│|]?\s*✓\s+(.+?)\s*[│|]?$/);
|
|
122
|
+
if (!item)
|
|
123
|
+
continue;
|
|
124
|
+
const value = item[1].trim().replaceAll('\\', '/');
|
|
125
|
+
const skillPath = resolve(value.startsWith('~/') ? join(homedir(), value.slice(2)) : value);
|
|
126
|
+
if (dirname(skillPath) !== join(homedir(), '.agents', 'skills')) {
|
|
127
|
+
throw new Error(`无法识别全局 skill 安装路径: ${item[1]}。请使用 setup plugin <name> <path> 注册。`);
|
|
128
|
+
}
|
|
129
|
+
paths.push(skillPath);
|
|
130
|
+
}
|
|
131
|
+
if (paths.length !== expected)
|
|
132
|
+
throw new Error('Ali Skills 安装结果格式无法识别,请使用 setup plugin <name> <path> 注册。');
|
|
133
|
+
return [...new Set(paths)];
|
|
134
|
+
}
|
|
135
|
+
function permitsEmptyInstallResult(args, output) {
|
|
136
|
+
return (args.length === 0 ||
|
|
137
|
+
args.some(argument => ['--help', '-h', '--list'].includes(argument)) ||
|
|
138
|
+
/\b(?:installation\s+)?cancel(?:led|ed)\b/i.test(output));
|
|
139
|
+
}
|
|
140
|
+
export async function installPlugins(args, runner = runCommand) {
|
|
141
|
+
if (args.includes('--')) {
|
|
142
|
+
throw new Error('plugin install 不支持 -- 参数分隔符,以确保全局 symlink 安装选项生效。');
|
|
143
|
+
}
|
|
144
|
+
const npx = findExecutable('npx', { extraDirectories: [dirname(process.execPath)] });
|
|
145
|
+
if (!npx)
|
|
146
|
+
throw new Error('未找到 npx,请检查 Node.js 安装。');
|
|
147
|
+
// 固定共享安装模式,确保成功结果指向 ~/.agents/skills 中的入口。
|
|
148
|
+
const installed = await runner(npx, ['-y', 'ali-skills', 'add', ...args, '--global', '--install-mode', 'symlink'], {
|
|
149
|
+
env: prependNodePath(),
|
|
150
|
+
tee: true,
|
|
151
|
+
});
|
|
152
|
+
if (installed.code !== 0)
|
|
153
|
+
return installed.code;
|
|
154
|
+
const output = `${installed.stdout ?? ''}\n${installed.stderr ?? ''}`;
|
|
155
|
+
const paths = parseInstalledPluginPaths(output);
|
|
156
|
+
if (paths.length === 0) {
|
|
157
|
+
if (permitsEmptyInstallResult(args, output))
|
|
158
|
+
return 0;
|
|
159
|
+
throw new Error('Ali Skills 安装成功但无法识别安装结果,请使用 setup plugin <name> <path> 注册。');
|
|
160
|
+
}
|
|
161
|
+
const plugins = new Map();
|
|
162
|
+
for (const skillPath of paths) {
|
|
163
|
+
if (!statSync(join(skillPath, 'SKILL.md')).isFile())
|
|
164
|
+
throw new Error(`安装结果缺少 SKILL.md: ${skillPath}`);
|
|
165
|
+
const entry = join(skillPath, 'scripts', 'main.mjs');
|
|
166
|
+
if (!existsSync(entry))
|
|
167
|
+
continue;
|
|
168
|
+
const plugin = await loadPlugin(entry, registrationContext);
|
|
169
|
+
const name = plugin.name ?? basename(skillPath);
|
|
170
|
+
assertPluginName(name);
|
|
171
|
+
if (plugins.has(name))
|
|
172
|
+
throw new Error(`多个 skill 使用插件名称 ${name},请通过 setup plugin <name> <path> 分别注册。`);
|
|
173
|
+
plugins.set(name, entry);
|
|
174
|
+
}
|
|
175
|
+
if (plugins.size === 0) {
|
|
176
|
+
console.error('未找到 scripts/main.mjs 插件入口;自定义入口可用 setup plugin <name> <path> 注册。');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
await ensurePluginRuntime(runner);
|
|
180
|
+
for (const [name, entry] of plugins) {
|
|
181
|
+
registerPlugin(name, entry);
|
|
182
|
+
console.error(`Plugin ${name} registered: ${entry}\nRun: mearl ${name} --help`);
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { buildGlobalPackageCommand, detectCliPackage, detectGlobalPackage, resolveInvokingPackageManager, } from './packageManager.js';
|
|
5
|
+
import { prependCommandPath, runCommand } from './process.js';
|
|
6
|
+
import { compareMearlReleaseVersions, isMearlReleaseVersion } from './version.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
export const PLUGIN_RUNTIME_VERSION = require('../package.json').version;
|
|
9
|
+
/** 插件使用当前 setup 同版本的运行时;已安装的更高版本保持不动。 */
|
|
10
|
+
export async function ensurePluginRuntime(runner = runCommand) {
|
|
11
|
+
for (const [packageName, bin] of [
|
|
12
|
+
['@mearl/client', 'mearl'],
|
|
13
|
+
['@mearl/native-host', 'mearl-native-host'],
|
|
14
|
+
]) {
|
|
15
|
+
const inspect = () => {
|
|
16
|
+
const installed = detectCliPackage(packageName, [bin]);
|
|
17
|
+
const manifest = installed && JSON.parse(readFileSync(join(installed.packageRoot, 'package.json'), 'utf8'));
|
|
18
|
+
const version = manifest?.version;
|
|
19
|
+
const pluginExport = manifest?.exports?.['./plugin']?.import;
|
|
20
|
+
const supported = packageName !== '@mearl/client' ||
|
|
21
|
+
(installed &&
|
|
22
|
+
typeof pluginExport === 'string' &&
|
|
23
|
+
existsSync(join(installed.packageRoot, pluginExport)));
|
|
24
|
+
const current = isMearlReleaseVersion(version) &&
|
|
25
|
+
compareMearlReleaseVersions(version, PLUGIN_RUNTIME_VERSION) >= 0;
|
|
26
|
+
return { installed, version, current, ready: current && supported };
|
|
27
|
+
};
|
|
28
|
+
const before = inspect();
|
|
29
|
+
let ready = before;
|
|
30
|
+
if (!before.ready) {
|
|
31
|
+
if (before.current &&
|
|
32
|
+
compareMearlReleaseVersions(before.version, PLUGIN_RUNTIME_VERSION) > 0) {
|
|
33
|
+
throw new Error(`${packageName}@${before.version} 缺少插件入口,请修复该版本的安装后重试。`);
|
|
34
|
+
}
|
|
35
|
+
const manager = detectGlobalPackage(packageName, [bin]) ?? resolveInvokingPackageManager();
|
|
36
|
+
const command = buildGlobalPackageCommand(manager, 'install', [packageName], PLUGIN_RUNTIME_VERSION);
|
|
37
|
+
console.error(`${packageName}: ${before.version ?? '未安装'} → ${PLUGIN_RUNTIME_VERSION}`);
|
|
38
|
+
const result = await runner(command.command, command.args, {
|
|
39
|
+
env: prependCommandPath(command.command),
|
|
40
|
+
});
|
|
41
|
+
if (result.code !== 0)
|
|
42
|
+
throw new Error(`${packageName} 安装失败 (exit ${result.code}),插件尚未注册。`);
|
|
43
|
+
ready = inspect();
|
|
44
|
+
if (!ready.ready || !ready.installed) {
|
|
45
|
+
throw new Error(`${packageName} 更新后仍不可用,请检查 PATH 中的 ${bin} 是否指向 >= ${PLUGIN_RUNTIME_VERSION} 的有效安装。`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (packageName === '@mearl/native-host') {
|
|
49
|
+
const executable = ready.installed.executable;
|
|
50
|
+
const initialized = await runner(executable, ['--init', '--mode', 'skills'], {
|
|
51
|
+
env: prependCommandPath(executable),
|
|
52
|
+
});
|
|
53
|
+
if (initialized.code !== 0)
|
|
54
|
+
throw new Error(`native-host 初始化失败 (exit ${initialized.code}),插件尚未注册。`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/process.d.ts
CHANGED
package/dist/process.js
CHANGED
|
@@ -89,20 +89,22 @@ export function findExecutable(name, options = {}) {
|
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
91
|
export const runCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
|
|
92
|
-
const capture = options.capture === true;
|
|
92
|
+
const capture = options.capture === true || options.tee === true;
|
|
93
93
|
const launch = resolveSpawnCommand(command, args);
|
|
94
94
|
const child = spawn(launch.command, launch.args, {
|
|
95
95
|
cwd: options.cwd,
|
|
96
96
|
env: options.env,
|
|
97
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
97
|
+
stdio: capture ? [options.tee ? 'inherit' : 'ignore', 'pipe', 'pipe'] : 'inherit',
|
|
98
98
|
windowsHide: true,
|
|
99
99
|
});
|
|
100
100
|
let stdout = '';
|
|
101
101
|
let stderr = '';
|
|
102
102
|
let settled = false;
|
|
103
|
-
const append = (current, chunk) =>
|
|
104
|
-
? current
|
|
105
|
-
:
|
|
103
|
+
const append = (current, chunk) => options.tee
|
|
104
|
+
? (current + chunk).slice(-MAX_CAPTURE_LENGTH)
|
|
105
|
+
: current.length >= MAX_CAPTURE_LENGTH
|
|
106
|
+
? current
|
|
107
|
+
: (current + chunk).slice(0, MAX_CAPTURE_LENGTH);
|
|
106
108
|
const finish = (error, code = 1) => {
|
|
107
109
|
if (settled)
|
|
108
110
|
return;
|
|
@@ -121,8 +123,16 @@ export const runCommand = (command, args, options = {}) => new Promise((resolve,
|
|
|
121
123
|
finish(new Error(`Command timed out after ${options.timeoutMs}ms`));
|
|
122
124
|
}, options.timeoutMs);
|
|
123
125
|
if (capture) {
|
|
124
|
-
child.stdout?.on('data', chunk =>
|
|
125
|
-
|
|
126
|
+
child.stdout?.setEncoding('utf8').on('data', (chunk) => {
|
|
127
|
+
stdout = append(stdout, chunk);
|
|
128
|
+
if (options.tee)
|
|
129
|
+
process.stdout.write(chunk);
|
|
130
|
+
});
|
|
131
|
+
child.stderr?.setEncoding('utf8').on('data', (chunk) => {
|
|
132
|
+
stderr = append(stderr, chunk);
|
|
133
|
+
if (options.tee)
|
|
134
|
+
process.stderr.write(chunk);
|
|
135
|
+
});
|
|
126
136
|
}
|
|
127
137
|
child.once('error', error => finish(error));
|
|
128
138
|
child.once('close', code => finish(undefined, code ?? 1));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/setup",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "One-shot environment setup and configuration for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"commander": "^15.0.0",
|
|
66
66
|
"extract-zip": "^2.0.1",
|
|
67
67
|
"ws": "^8.18.3",
|
|
68
|
-
"@mearl/client": "2.
|
|
68
|
+
"@mearl/client": "2.12.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^24.9.1",
|