@mearl/setup 2.11.0 → 2.13.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 CHANGED
@@ -6,6 +6,10 @@ 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>
12
+ npx @mearl/setup provider <install|update|uninstall> <package|id>
9
13
  ```
10
14
 
11
15
  配置 AgentBay API Key 时只需执行一次下面的命令,然后在隐藏输入提示中粘贴 Key。
@@ -23,6 +27,67 @@ npx @mearl/setup agentbay
23
27
  模式记录为 Qoder;`mearl-cloud-server` 的命令交互保持不变,`start` 和 `restart` 输出新的
24
28
  配对指令后退出,不监听端口或保留守护进程。配对 URL 不包含 Qoder PAT。
25
29
 
30
+ ## 安装与注册 Skill CLI 插件
31
+
32
+ 一个命令安装 skill 并注册它的 CLI 能力:
33
+
34
+ ```bash
35
+ npx @mearl/setup plugin install trip/mearl --skill yuque-doc-fetch --yes
36
+ mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
37
+ ```
38
+
39
+ 安装参数透传给 `npx -y ali-skills add`,末尾统一追加 `--global --install-mode symlink`;不接受会截断后置选项的 `--` 参数分隔符,
40
+ 确保 skill 存放在 `~/.agents/skills` 并共享给所选 agent。安装日志实时显示,保留交互;
41
+ setup 从 `Installed … skill(s)` 成功区块提取路径,仅注册本次安装且包含 `scripts/main.mjs` 的 skill。
42
+ 普通 skill 正常安装。输出格式无法识别时提示手动注册,不扫描其他 skill 或猜测安装结果。
43
+
44
+ 自动注册默认使用安装目录名;入口可用 `export const name = 'yuque'` 指定简短命令名。
45
+ 重装会刷新同名绑定。每个入口必须默认导出 `register(app)`;完整协议见
46
+ [CLI 插件规范](../skills/mearl-skill-creator/references/cli-plugin.md)。
47
+
48
+ 已通过其他渠道分发的 skill 或自定义入口,可以直接传入路径注册:
49
+
50
+ ```bash
51
+ npx @mearl/setup plugin yuque /absolute/path/to/yuque-doc-fetch
52
+ npx @mearl/setup plugin custom ./skills/custom/entry.mjs
53
+ ```
54
+
55
+ path 接受目录或具体文件;目录默认读取 `scripts/main.mjs`,相对路径基于调用工作目录解析。
56
+ 命令检查入口注册函数,然后保存绝对入口路径,同名注册更新绑定。
57
+ 两种注册流程都会检查本机 `@mearl/client` 和 `@mearl/native-host`:缺失或低于当前 setup 版本时,
58
+ 复用已有包管理器安装 setup 的对应版本,更新后再次验证;更高版本保持不动。
59
+ native-host 会幂等初始化 Skills 模式。基础环境补齐或初始化失败时保留已安装的 skill,并停止注册。
60
+ 注册后可在任意目录运行 `mearl <name> --help`,日常业务调用不执行安装或版本检查。
61
+
62
+ 不再需要插件时执行:
63
+
64
+ ```bash
65
+ npx @mearl/setup plugin uninstall yuque
66
+ ```
67
+
68
+ 若入口位于 `~/.agents/skills/<skill>/`,setup 会先执行
69
+ `ali-skills remove <skill> --global --yes`,成功后清除该 Skill 对应的全部插件注册;
70
+ 入口目录已被手动删除时会让 Ali Skills 仍能识别本次移除,从而一并清理其跟踪记录。
71
+ 移除失败时保留注册以便重试。手工注册的其他路径只解除 Mearl 注册,不删除源码。
72
+ 重复卸载未注册名称会成功返回,不产生改动。
73
+
74
+ ## 管理浏览器 Provider
75
+
76
+ Provider 由 npm 包分发,但包本身无需暴露 CLI 或安装脚本。setup 使用当前 npm 包管理器
77
+ 完成全局包生命周期,再读取包内的 `package.json#mearl.provider` 元数据登记协议入口、
78
+ 可创建的浏览器类型名称及其表单参数:
79
+
80
+ ```bash
81
+ npx @mearl/setup provider install @ali/mearl-provider-brow
82
+ npx @mearl/setup provider update brow
83
+ npx @mearl/setup provider uninstall brow
84
+ ```
85
+
86
+ 安装参数接受包名、版本或 dist-tag;更新和卸载可使用 `providerId` 或完整包名。阿里包在
87
+ 本机存在 `tnpm` 时优先使用 `tnpm`,其他情况沿用启动 setup 的包管理器。Provider 登记只保存
88
+ 经过校验的包名、安装目录和包内入口;不接受包自行注入启动命令、参数、工作目录或环境变量。
89
+ 卸载仅在包管理器成功移除包后清除登记,以便失败时可以重试。
90
+
26
91
  ## 行为
27
92
 
28
93
  - `install` 可以省略;直接执行 `npx @mearl/setup` 等同于 `npx @mearl/setup install`。
@@ -153,4 +218,4 @@ npx @mearl/setup agentbay
153
218
  npx @mearl/setup agentbay --key-file ./agentbay.key
154
219
  ```
155
220
 
156
- 原有的 npm/npx 安装方式和各包 CLI 保持可用;setup 只是统一的便利入口。
221
+ 原有 Mearl 环境包的 npm/npx 安装方式和 CLI 保持可用;setup 是统一的便利入口。
package/dist/cli.d.ts CHANGED
@@ -3,6 +3,8 @@ 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';
7
+ import { installProviderPackage, uninstallProviderPackage, updateProviderPackage } from './provider.js';
6
8
  export declare const SETUP_VERSION: string;
7
9
  interface ParsedArgs {
8
10
  agentBayKeyFile?: string;
@@ -11,7 +13,13 @@ interface ParsedArgs {
11
13
  force?: boolean;
12
14
  full?: boolean;
13
15
  help: boolean;
14
- operation?: MearlLifecycleOperation | 'agentbay' | 'extension';
16
+ operation?: MearlLifecycleOperation | 'agentbay' | 'extension' | 'plugin' | 'provider';
17
+ pluginName?: string;
18
+ pluginPath?: string;
19
+ pluginInstallArgs?: string[];
20
+ pluginUninstallName?: string;
21
+ providerCommand?: 'install' | 'uninstall' | 'update';
22
+ providerTarget?: string;
15
23
  profiles?: MearlProfile[];
16
24
  skipSkill: boolean;
17
25
  targetLine?: string;
@@ -19,12 +27,18 @@ interface ParsedArgs {
19
27
  yes: boolean;
20
28
  }
21
29
  export interface SetupCliDependencies {
30
+ registerPlugin?: typeof setupPlugin;
31
+ installPlugins?: typeof installPlugins;
32
+ uninstallPlugin?: typeof uninstallPlugin;
22
33
  configureAgentBay?: typeof configureAgentBayApiKey;
23
34
  promptAgentBayKey?: typeof promptAgentBayApiKey;
24
35
  readAgentBayKeyFile?: typeof readAgentBayApiKeyFile;
25
36
  readAgentBayKeyStdin?: typeof readAgentBayApiKeyStdin;
26
37
  installBrowserExtension?: typeof installMearlBrowserExtension;
38
+ installProvider?: typeof installProviderPackage;
27
39
  runLifecycle?: typeof runMearlLifecycle;
40
+ uninstallProvider?: typeof uninstallProviderPackage;
41
+ updateProvider?: typeof updateProviderPackage;
28
42
  }
29
43
  export declare function parseSetupArgs(argv: string[]): ParsedArgs;
30
44
  export declare function main(argv?: string[], interactiveTerminal?: boolean, dependencies?: SetupCliDependencies): Promise<number>;
package/dist/cli.js CHANGED
@@ -5,6 +5,8 @@ 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';
9
+ import { installProviderPackage, uninstallProviderPackage, updateProviderPackage, } from './provider.js';
8
10
  const require = createRequire(import.meta.url);
9
11
  export const SETUP_VERSION = require('../package.json').version;
10
12
  const baseParsedArgs = () => ({
@@ -24,6 +26,29 @@ function commanderError(error) {
24
26
  return new Error(message);
25
27
  }
26
28
  export function parseSetupArgs(argv) {
29
+ if (argv[0] === 'provider') {
30
+ const command = argv[1];
31
+ if ((command === 'install' || command === 'update' || command === 'uninstall') &&
32
+ argv.length === 3 &&
33
+ argv[2]) {
34
+ return {
35
+ ...baseParsedArgs(),
36
+ operation: 'provider',
37
+ providerCommand: command,
38
+ providerTarget: argv[2],
39
+ };
40
+ }
41
+ throw new Error('provider requires install <package>, update <id>, or uninstall <id>');
42
+ }
43
+ if (argv[0] === 'plugin' && argv[1] === 'install') {
44
+ return { ...baseParsedArgs(), operation: 'plugin', pluginInstallArgs: argv.slice(2) };
45
+ }
46
+ if (argv[0] === 'plugin' && argv[1] === 'uninstall') {
47
+ if (argv.length !== 3 || !argv[2]) {
48
+ throw new Error('plugin uninstall requires exactly one plugin name');
49
+ }
50
+ return { ...baseParsedArgs(), operation: 'plugin', pluginUninstallName: argv[2] };
51
+ }
27
52
  let parsed;
28
53
  const program = new Command()
29
54
  .name('mearl-setup')
@@ -77,6 +102,14 @@ export function parseSetupArgs(argv) {
77
102
  addLifecycleCommand('install');
78
103
  addLifecycleCommand('update');
79
104
  addLifecycleCommand('uninstall');
105
+ program
106
+ .command('plugin')
107
+ .argument('<name>')
108
+ .argument('<path>')
109
+ .allowExcessArguments(false)
110
+ .action((pluginName, pluginPath) => {
111
+ parsed = { ...baseParsedArgs(), operation: 'plugin', pluginName, pluginPath };
112
+ });
80
113
  const extension = new Command('extension')
81
114
  .exitOverride()
82
115
  .configureOutput({ writeErr: () => { }, writeOut: () => { } })
@@ -145,6 +178,10 @@ Usage:
145
178
  npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
146
179
  npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
147
180
  npx @mearl/setup extension [--full] [--force] [--yes]
181
+ npx @mearl/setup plugin <name> <path>
182
+ npx @mearl/setup plugin install <ali-skills add arguments...>
183
+ npx @mearl/setup plugin uninstall <name>
184
+ npx @mearl/setup provider <install|update|uninstall> <package|id>
148
185
  npx @mearl/setup agentbay [--stdin | --key-file <path>]
149
186
 
150
187
  Behavior:
@@ -153,6 +190,8 @@ Behavior:
153
190
  uninstall Auto-detects installed environments and asks for confirmation
154
191
  extension Install or update the browser extension; --full also installs the local environment
155
192
  agentbay Save or update AGENTBAY_KEY without changing other Mearl settings
193
+ plugin Register, install, or uninstall a CLI plugin
194
+ provider Install npm Provider packages and manage their Mearl registration
156
195
 
157
196
  Options:
158
197
  --local Manage @mearl/native-host and @mearl/client
@@ -183,6 +222,8 @@ Examples:
183
222
  npx @mearl/setup update
184
223
  npx @mearl/setup extension
185
224
  npx @mearl/setup extension --full
225
+ npx @mearl/setup plugin uninstall yuque
226
+ npx @mearl/setup provider install @ali/mearl-provider-brow
186
227
  npx @mearl/setup agentbay
187
228
  npx @mearl/setup --qoder-cloud --yes
188
229
  npx @mearl/setup update --local --target-line 2.3.x --skip-skill
@@ -210,6 +251,62 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
210
251
  console.log(SETUP_VERSION);
211
252
  return 0;
212
253
  }
254
+ if (parsed.operation === 'plugin') {
255
+ try {
256
+ if (parsed.pluginInstallArgs) {
257
+ return await (dependencies.installPlugins ?? installPlugins)(parsed.pluginInstallArgs);
258
+ }
259
+ if (parsed.pluginUninstallName) {
260
+ const result = await (dependencies.uninstallPlugin ?? uninstallPlugin)(parsed.pluginUninstallName);
261
+ if (!result.removed) {
262
+ console.error(`Plugin ${parsed.pluginUninstallName} is not registered.`);
263
+ }
264
+ else if (result.skillName) {
265
+ console.error(result.entryMissing
266
+ ? `Plugin ${parsed.pluginUninstallName} had a missing entry; ali-skills cleanup completed for ${result.skillName}.`
267
+ : `Plugin ${parsed.pluginUninstallName} uninstalled with ali-skills skill ${result.skillName}.`);
268
+ if (result.unregisteredNames.length > 1) {
269
+ console.error(`Removed registrations: ${result.unregisteredNames.join(', ')}`);
270
+ }
271
+ }
272
+ else {
273
+ console.error(result.entryMissing
274
+ ? `Plugin ${parsed.pluginUninstallName} stale registration removed; the external entry was already missing.`
275
+ : `Plugin ${parsed.pluginUninstallName} unregistered; external skill files were kept.`);
276
+ }
277
+ return 0;
278
+ }
279
+ const entry = await (dependencies.registerPlugin ?? setupPlugin)(parsed.pluginName, parsed.pluginPath);
280
+ console.error(`Plugin ${parsed.pluginName} registered: ${entry}`);
281
+ console.error(`Run: mearl ${parsed.pluginName} --help`);
282
+ return 0;
283
+ }
284
+ catch (error) {
285
+ console.error(error instanceof Error ? error.message : String(error));
286
+ return 1;
287
+ }
288
+ }
289
+ if (parsed.operation === 'provider') {
290
+ try {
291
+ const target = parsed.providerTarget;
292
+ const result = parsed.providerCommand === 'install'
293
+ ? await (dependencies.installProvider ?? installProviderPackage)(target)
294
+ : parsed.providerCommand === 'update'
295
+ ? await (dependencies.updateProvider ?? updateProviderPackage)(target)
296
+ : await (dependencies.uninstallProvider ?? uninstallProviderPackage)(target);
297
+ const verb = parsed.providerCommand === 'uninstall'
298
+ ? 'uninstalled'
299
+ : parsed.providerCommand === 'update'
300
+ ? 'updated'
301
+ : 'installed';
302
+ console.error(`Provider ${result.manifest.providerId} ${verb}: ${result.manifest.packageName}@${result.manifest.version} (${result.manager})`);
303
+ return 0;
304
+ }
305
+ catch (error) {
306
+ console.error(error instanceof Error ? error.message : String(error));
307
+ return 1;
308
+ }
309
+ }
213
310
  if (parsed.operation === 'agentbay') {
214
311
  try {
215
312
  const configureAgentBay = dependencies.configureAgentBay ?? configureAgentBayApiKey;
package/dist/index.d.ts CHANGED
@@ -11,4 +11,6 @@ export type { DetectedMearlProfiles, MearlProfile, MearlProfileConfig } from './
11
11
  export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
12
12
  export { compareMearlReleaseLines, compareMearlReleaseVersions, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
13
13
  export type { MearlReleaseLine, MearlReleaseVersion } from './version.js';
14
+ export { installProviderPackage, parseProviderPackageSpec, uninstallProviderPackage, updateProviderPackage, } from './provider.js';
15
+ export type { ProviderSetupDependencies, ProviderSetupResult } from './provider.js';
14
16
  export type { NativeHostCleanupDependencies, NativeHostCleanupOptions, NativeHostIntegrationMode, } from './nativeHost.js';
package/dist/index.js CHANGED
@@ -5,3 +5,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
7
  export { compareMearlReleaseLines, compareMearlReleaseVersions, isMearlReleaseLine, isMearlReleaseVersion, isMearlReleaseVersionInLine, mearlReleaseLineForVersion, parseMearlReleaseLine, parseMearlReleaseVersion, } from './version.js';
8
+ export { installProviderPackage, parseProviderPackageSpec, uninstallProviderPackage, updateProviderPackage, } from './provider.js';
@@ -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;
@@ -188,7 +188,7 @@ function packageRootFromWrapper(executable, packageName, readFileSync) {
188
188
  return null;
189
189
  }
190
190
  }
191
- export function detectGlobalPackage(packageName, binNames, options = {}) {
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
- const installation = resolvePackageManagerInstallation(packageRoot, packageName, {
219
- env,
220
- existsSync,
221
- platform,
222
- processExecPath,
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
- packageName,
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;
@@ -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
+ const plugin = await loadPlugin(entry, registrationContext);
21
+ await ensurePluginRuntime();
22
+ return registerPlugin(name, entry, plugin.description);
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, description: plugin.description });
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, description }] of plugins) {
181
+ registerPlugin(name, entry, description);
182
+ console.error(`Plugin ${name} registered: ${entry}\nRun: mearl ${name} --help`);
183
+ }
184
+ return 0;
185
+ }
@@ -0,0 +1,4 @@
1
+ import { type CommandRunner } from './process.js';
2
+ export declare const PLUGIN_RUNTIME_VERSION: string;
3
+ /** 插件使用当前 setup 同版本的运行时;已安装的更高版本保持不动。 */
4
+ export declare function ensurePluginRuntime(runner?: CommandRunner): Promise<void>;
@@ -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
@@ -10,6 +10,8 @@ export interface CommandResult {
10
10
  }
11
11
  export interface RunCommandOptions {
12
12
  capture?: boolean;
13
+ /** 显示并捕获输出,同时继承 stdin 以保留安装器交互。 */
14
+ tee?: boolean;
13
15
  cwd?: string;
14
16
  env?: NodeJS.ProcessEnv;
15
17
  timeoutMs?: number;
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) => current.length >= MAX_CAPTURE_LENGTH
104
- ? current
105
- : (current + chunk.toString('utf8')).slice(0, MAX_CAPTURE_LENGTH);
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 => (stdout = append(stdout, chunk)));
125
- child.stderr?.on('data', chunk => (stderr = append(stderr, chunk)));
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));
@@ -0,0 +1,28 @@
1
+ import { listInstalledProviders, registerProviderPackage, stopProvider, uninstallProvider, type InstalledProviderManifest } from '@mearl/provider/host';
2
+ import { resolvePackageManagerInstallation, type PackageManagerInstallation } from './packageManager.js';
3
+ import { findExecutable, type CommandRunner } from './process.js';
4
+ interface ProviderPackageSpec {
5
+ packageName: string;
6
+ target: string;
7
+ }
8
+ export interface ProviderSetupDependencies {
9
+ existsSync?: (filePath: string) => boolean;
10
+ findExecutable?: typeof findExecutable;
11
+ listProviders?: typeof listInstalledProviders;
12
+ registerPackage?: typeof registerProviderPackage;
13
+ resolveInvokingManager?: (env: NodeJS.ProcessEnv) => PackageManagerInstallation;
14
+ resolvePackageManager?: typeof resolvePackageManagerInstallation;
15
+ resolvePackageRoot?: (installation: PackageManagerInstallation, packageName: string, runner: CommandRunner, env: NodeJS.ProcessEnv) => Promise<string>;
16
+ runCommand?: CommandRunner;
17
+ stopProvider?: typeof stopProvider;
18
+ unregisterProvider?: typeof uninstallProvider;
19
+ }
20
+ export interface ProviderSetupResult {
21
+ manifest: InstalledProviderManifest;
22
+ manager: PackageManagerInstallation['manager'];
23
+ }
24
+ export declare function parseProviderPackageSpec(value: string): ProviderPackageSpec;
25
+ export declare function installProviderPackage(packageSpec: string, dependencies?: ProviderSetupDependencies): Promise<ProviderSetupResult>;
26
+ export declare function updateProviderPackage(selector: string, dependencies?: ProviderSetupDependencies): Promise<ProviderSetupResult>;
27
+ export declare function uninstallProviderPackage(selector: string, dependencies?: ProviderSetupDependencies): Promise<ProviderSetupResult>;
28
+ export {};
@@ -0,0 +1,114 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { listInstalledProviders, registerProviderPackage, stopProvider, uninstallProvider, } from '@mearl/provider/host';
4
+ import { buildGlobalPackageCommand, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
5
+ import { findExecutable, prependCommandPath, runCommand } from './process.js';
6
+ export function parseProviderPackageSpec(value) {
7
+ const input = value.trim();
8
+ const nameMatch = input.match(/^(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)(?:@(.+))?$/i);
9
+ if (!nameMatch || !nameMatch[1] || /\s/.test(nameMatch[2] ?? '')) {
10
+ throw new Error(`Provider install requires an npm package name, received: ${value}`);
11
+ }
12
+ return { packageName: nameMatch[1], target: nameMatch[2] || 'latest' };
13
+ }
14
+ function providerManager(packageName, env, dependencies) {
15
+ const resolved = (dependencies.resolveInvokingManager ??
16
+ (childEnv => resolveInvokingPackageManager({ env: childEnv })))(env);
17
+ if (!packageName.startsWith('@ali/') || resolved.command.includes('tnpm'))
18
+ return resolved;
19
+ const tnpm = (dependencies.findExecutable ?? findExecutable)('tnpm', {
20
+ env,
21
+ extraDirectories: [dirname(process.execPath)],
22
+ });
23
+ return tnpm ? { manager: 'npm', command: tnpm } : resolved;
24
+ }
25
+ function outputPath(output) {
26
+ const lines = output
27
+ .split(/\r?\n/)
28
+ .map(line => line.trim())
29
+ .filter(Boolean);
30
+ return lines.at(-1) ?? null;
31
+ }
32
+ async function resolveGlobalProviderPackageRoot(installation, packageName, runner, env) {
33
+ let args;
34
+ let toNodeModules;
35
+ switch (installation.manager) {
36
+ case 'npm':
37
+ case 'pnpm':
38
+ args = ['root', '-g'];
39
+ toNodeModules = value => value;
40
+ break;
41
+ case 'yarn':
42
+ args = ['global', 'dir'];
43
+ toNodeModules = value => join(value, 'node_modules');
44
+ break;
45
+ case 'bun':
46
+ args = ['pm', 'bin', '-g'];
47
+ toNodeModules = value => resolve(value, '..', 'install', 'global', 'node_modules');
48
+ break;
49
+ case 'volta':
50
+ throw new Error('Volta cannot manage a Provider package without a binary; use npm or pnpm');
51
+ }
52
+ const result = await runner(installation.command, args, {
53
+ capture: true,
54
+ env: prependCommandPath(installation.command, env),
55
+ });
56
+ const root = outputPath(result.stdout);
57
+ if (result.code !== 0 || !root) {
58
+ throw new Error(`${installation.manager} global package root lookup failed`);
59
+ }
60
+ return join(toNodeModules(root), ...packageName.split('/'));
61
+ }
62
+ function findRegisteredProvider(selector, listProviders) {
63
+ const providers = listProviders();
64
+ const matches = providers.filter(provider => provider.providerId === selector || provider.packageName === selector);
65
+ if (matches.length === 0)
66
+ throw new Error(`Provider is not registered: ${selector}`);
67
+ if (matches.length > 1)
68
+ throw new Error(`Provider selector is ambiguous: ${selector}`);
69
+ return matches[0];
70
+ }
71
+ async function runPackageCommand(installation, operation, packageName, target, runner, env) {
72
+ const command = buildGlobalPackageCommand(installation, operation, [packageName], target);
73
+ const result = await runner(command.command, command.args, {
74
+ env: prependCommandPath(command.command, env),
75
+ tee: true,
76
+ });
77
+ if (result.code !== 0) {
78
+ throw new Error(`${packageName} ${operation} failed (exit ${result.code})`);
79
+ }
80
+ }
81
+ export async function installProviderPackage(packageSpec, dependencies = {}) {
82
+ const { packageName, target } = parseProviderPackageSpec(packageSpec);
83
+ const env = process.env;
84
+ const runner = dependencies.runCommand ?? runCommand;
85
+ const installation = providerManager(packageName, env, dependencies);
86
+ await runPackageCommand(installation, 'install', packageName, target, runner, env);
87
+ const packageRoot = await (dependencies.resolvePackageRoot ?? resolveGlobalProviderPackageRoot)(installation, packageName, runner, env);
88
+ if (!(dependencies.existsSync ?? existsSync)(join(packageRoot, 'package.json'))) {
89
+ throw new Error(`${packageName} was installed but its package root was not found: ${packageRoot}`);
90
+ }
91
+ const manifest = (dependencies.registerPackage ?? registerProviderPackage)(packageRoot);
92
+ return { manifest, manager: installation.manager };
93
+ }
94
+ export async function updateProviderPackage(selector, dependencies = {}) {
95
+ const listProviders = dependencies.listProviders ?? listInstalledProviders;
96
+ const current = findRegisteredProvider(selector, listProviders);
97
+ const env = process.env;
98
+ const runner = dependencies.runCommand ?? runCommand;
99
+ const installation = (dependencies.resolvePackageManager ?? resolvePackageManagerInstallation)(current.packageRoot, current.packageName) ?? providerManager(current.packageName, env, dependencies);
100
+ (dependencies.stopProvider ?? stopProvider)(current.providerId);
101
+ await runPackageCommand(installation, 'update', current.packageName, 'latest', runner, env);
102
+ const manifest = (dependencies.registerPackage ?? registerProviderPackage)(current.packageRoot);
103
+ return { manifest, manager: installation.manager };
104
+ }
105
+ export async function uninstallProviderPackage(selector, dependencies = {}) {
106
+ const listProviders = dependencies.listProviders ?? listInstalledProviders;
107
+ const manifest = findRegisteredProvider(selector, listProviders);
108
+ const env = process.env;
109
+ const installation = (dependencies.resolvePackageManager ?? resolvePackageManagerInstallation)(manifest.packageRoot, manifest.packageName) ?? providerManager(manifest.packageName, env, dependencies);
110
+ (dependencies.stopProvider ?? stopProvider)(manifest.providerId);
111
+ await runPackageCommand(installation, 'uninstall', manifest.packageName, 'latest', dependencies.runCommand ?? runCommand, env);
112
+ (dependencies.unregisterProvider ?? uninstallProvider)(manifest.providerId);
113
+ return { manifest, manager: installation.manager };
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/setup",
3
- "version": "2.11.0",
3
+ "version": "2.13.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,8 @@
65
65
  "commander": "^15.0.0",
66
66
  "extract-zip": "^2.0.1",
67
67
  "ws": "^8.18.3",
68
- "@mearl/client": "2.11.0"
68
+ "@mearl/client": "2.13.0",
69
+ "@mearl/provider": "2.13.0"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@types/node": "^24.9.1",