@mearl/setup 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @mearl/setup
2
+
3
+ Mearl 环境的一次性安装、更新与卸载工具。无需先全局安装 setup:
4
+
5
+ ```bash
6
+ npx @mearl/setup <install|update|uninstall> [--local|--cloud|--mcp] [--yes]
7
+ ```
8
+
9
+ ## 行为
10
+
11
+ - `install` 未指定环境时默认 `--local`。
12
+ - `update` 和 `uninstall` 未指定环境时,从当前 `PATH` 中的
13
+ `mearl-native-host`、`mearl-cloud`、`mearl-mcp-server` 自动检测,可同时处理
14
+ 多个已安装环境。
15
+ - 显式传入 `--local`、`--cloud` 或 `--mcp` 时只处理指定环境。
16
+ - 已有环境沿用其原包管理器和全局安装前缀;首次安装沿用启动 setup 的包管理器。
17
+ - local/cloud 共用的全局 mearl skill 通过 `ali-skills` 的 JSON 清单定位,只处理一次。
18
+ - 卸载一个环境但另一个仍依赖全局 mearl skill 时会保留 skill。
19
+ - `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
20
+
21
+ ## 人工与 Agent 使用
22
+
23
+ - 人工在终端执行时不加 `--yes`,需要选择 Skill 安装目标或 MCP 客户端时会显示交互提示。
24
+ - Agent、CI 或其他无 TTY 环境应加 `--yes`(也可写
25
+ `--non-interactive`)。首次安装会选择已检测到的 Agent / MCP 客户端,卸载会清理
26
+ 所有已检测到的相关配置。
27
+ - 无 TTY 环境执行 `install` 或 `uninstall` 却未加上述参数时,会在任何改动前退出并提示正确命令。
28
+ - 退出码 `0` 表示完整成功,`1` 表示执行未完成,`2` 表示参数或交互模式不适用。
29
+
30
+ ## 示例
31
+
32
+ ```bash
33
+ # 默认安装本地浏览器环境
34
+ npx @mearl/setup install
35
+
36
+ # Agent / CI 无交互安装
37
+ npx @mearl/setup install --yes
38
+
39
+ # 安装云端 Agent 环境
40
+ npx @mearl/setup install --cloud
41
+
42
+ # 自动更新当前 PATH 中检测到的全部 Mearl 环境
43
+ npx @mearl/setup update
44
+
45
+ # 只卸载本地环境
46
+ npx @mearl/setup uninstall --local
47
+
48
+ # 自动检测并无交互卸载
49
+ npx @mearl/setup uninstall --yes
50
+ ```
51
+
52
+ 原有的 npm/npx 安装方式和各包 CLI 保持可用;setup 只是统一的便利入口。
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import { type CommandRunner } from './process.js';
3
+ export interface AliSkillsCliOutput {
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+ export type AliSkillsCliRunner = (executable: string, args: string[], cwd?: string) => Promise<AliSkillsCliOutput>;
8
+ export interface ResolveAliSkillsOptions {
9
+ env?: NodeJS.ProcessEnv;
10
+ existsSync?: (filePath: string) => boolean;
11
+ homedir?: () => string;
12
+ platform?: NodeJS.Platform;
13
+ processExecPath?: string;
14
+ readdirSync?: typeof fs.readdirSync;
15
+ }
16
+ export declare function stripAnsi(value: string): string;
17
+ export declare function resolveAliSkillsExecutable(options?: ResolveAliSkillsOptions): string | null;
18
+ export declare const runAliSkillsCli: AliSkillsCliRunner;
19
+ interface AliSkillsInvocation {
20
+ command: string;
21
+ argsPrefix: string[];
22
+ env: NodeJS.ProcessEnv;
23
+ }
24
+ export declare function resolveAliSkillsInvocation(options?: ResolveAliSkillsOptions): AliSkillsInvocation | null;
25
+ export declare function parseGlobalMearlSkillPaths(output: string): string[];
26
+ export type MearlSkillOperation = 'install' | 'update' | 'uninstall';
27
+ export declare function runGlobalMearlSkillOperation(operation: MearlSkillOperation, options?: ResolveAliSkillsOptions & {
28
+ runCommand?: CommandRunner;
29
+ yes?: boolean;
30
+ writeLine?: (line: string) => void;
31
+ }): Promise<boolean>;
32
+ export {};
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { executableNames, findExecutable, isExecutable, prependCommandPath, prependNodePath, resolveSpawnCommand, runCommand, } from './process.js';
6
+ const COMMAND_TIMEOUT_MS = 120_000;
7
+ const MAX_OUTPUT_LENGTH = 1_000_000;
8
+ const ANSI_ESCAPE_PATTERN = new RegExp(String.raw `\u001B\[[0-?]*[ -/]*[@-~]`, 'g');
9
+ function existingExecutable(candidates, existsSync) {
10
+ return candidates.find(candidate => candidate && existsSync(candidate)) ?? null;
11
+ }
12
+ function versionManagerCandidates(home, executableNames, readdirSync) {
13
+ const roots = [
14
+ path.join(home, '.nvm', 'versions', 'node'),
15
+ path.join(home, '.fnm', 'node-versions'),
16
+ ];
17
+ return roots.flatMap(root => {
18
+ try {
19
+ return readdirSync(root, { withFileTypes: true })
20
+ .filter(entry => entry.isDirectory())
21
+ .sort((left, right) => right.name.localeCompare(left.name, undefined, { numeric: true }))
22
+ .flatMap(entry => executableNames.flatMap(name => [
23
+ path.join(root, entry.name, 'bin', name),
24
+ path.join(root, entry.name, 'installation', 'bin', name),
25
+ ]));
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ });
31
+ }
32
+ export function stripAnsi(value) {
33
+ return value.replace(ANSI_ESCAPE_PATTERN, '');
34
+ }
35
+ export function resolveAliSkillsExecutable(options = {}) {
36
+ const env = options.env ?? process.env;
37
+ const home = (options.homedir ?? os.homedir)();
38
+ const platform = options.platform ?? process.platform;
39
+ const existsSync = options.existsSync ?? (candidate => isExecutable(candidate, platform));
40
+ const processExecPath = options.processExecPath ?? process.execPath;
41
+ const readdirSync = options.readdirSync ?? fs.readdirSync;
42
+ const names = executableNames('ali-skills', platform);
43
+ const pathKey = Object.keys(env).find(key => key.toLowerCase() === 'path') || 'PATH';
44
+ const pathCandidates = (env[pathKey] || '')
45
+ .split(path.delimiter)
46
+ .filter(Boolean)
47
+ .flatMap(directory => names.map(name => path.join(directory, name)));
48
+ const candidates = [
49
+ env.MEARL_ALI_SKILLS_PATH,
50
+ ...names.map(name => path.join(path.dirname(processExecPath), name)),
51
+ ...names.map(name => path.join(home, '.local', 'bin', name)),
52
+ ...names.map(name => path.join(home, '.npm-global', 'bin', name)),
53
+ ...names.map(name => path.join(home, '.volta', 'bin', name)),
54
+ ...names.map(name => path.join(home, '.local', 'share', 'pnpm', name)),
55
+ ...names.map(name => path.join(home, 'Library', 'pnpm', name)),
56
+ ...versionManagerCandidates(home, names, readdirSync),
57
+ ...(platform === 'win32'
58
+ ? []
59
+ : ['/opt/homebrew/bin/ali-skills', '/usr/local/bin/ali-skills', '/usr/bin/ali-skills']),
60
+ ...pathCandidates,
61
+ ];
62
+ return existingExecutable(candidates, existsSync);
63
+ }
64
+ export const runAliSkillsCli = (executable, args, cwd) => new Promise((resolve, reject) => {
65
+ const launch = resolveSpawnCommand(executable, args);
66
+ const child = spawn(launch.command, launch.args, {
67
+ cwd,
68
+ env: prependNodePath(prependCommandPath(executable)),
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ windowsHide: true,
71
+ });
72
+ let stdout = '';
73
+ let stderr = '';
74
+ let settled = false;
75
+ const append = (current, chunk) => current.length >= MAX_OUTPUT_LENGTH
76
+ ? current
77
+ : (current + chunk.toString('utf8')).slice(0, MAX_OUTPUT_LENGTH);
78
+ const finish = (error) => {
79
+ if (settled)
80
+ return;
81
+ settled = true;
82
+ clearTimeout(timer);
83
+ if (error)
84
+ reject(error);
85
+ else
86
+ resolve({ stdout, stderr });
87
+ };
88
+ const timer = setTimeout(() => {
89
+ child.kill('SIGTERM');
90
+ finish(new Error(`Ali Skills command timed out after ${COMMAND_TIMEOUT_MS / 1000}s`));
91
+ }, COMMAND_TIMEOUT_MS);
92
+ child.stdout.on('data', chunk => (stdout = append(stdout, chunk)));
93
+ child.stderr.on('data', chunk => (stderr = append(stderr, chunk)));
94
+ child.once('error', error => finish(error));
95
+ child.once('exit', code => {
96
+ if (code === 0) {
97
+ finish();
98
+ return;
99
+ }
100
+ const detail = stripAnsi(stderr || stdout).trim();
101
+ finish(new Error(detail || `Ali Skills command exited with code ${String(code)}`));
102
+ });
103
+ });
104
+ export function resolveAliSkillsInvocation(options = {}) {
105
+ const env = options.env ?? process.env;
106
+ const executable = resolveAliSkillsExecutable(options);
107
+ if (executable) {
108
+ return {
109
+ command: executable,
110
+ argsPrefix: [],
111
+ env: prependNodePath(prependCommandPath(executable, env)),
112
+ };
113
+ }
114
+ const processExecPath = options.processExecPath ?? process.execPath;
115
+ const npx = findExecutable('npx', {
116
+ env,
117
+ existsSync: options.existsSync,
118
+ platform: options.platform,
119
+ extraDirectories: [path.dirname(processExecPath)],
120
+ });
121
+ if (!npx)
122
+ return null;
123
+ return {
124
+ command: npx,
125
+ argsPrefix: ['-y', 'ali-skills'],
126
+ env: prependNodePath(prependCommandPath(npx, env), processExecPath),
127
+ };
128
+ }
129
+ export function parseGlobalMearlSkillPaths(output) {
130
+ const parsed = JSON.parse(stripAnsi(output).trim());
131
+ if (!Array.isArray(parsed))
132
+ throw new Error('Ali Skills returned an invalid skill list');
133
+ return [
134
+ ...new Set(parsed.flatMap((skill) => skill.name === 'mearl' &&
135
+ skill.scope === 'global' &&
136
+ typeof skill.path === 'string' &&
137
+ skill.path.trim()
138
+ ? [skill.path.trim()]
139
+ : [])),
140
+ ];
141
+ }
142
+ export async function runGlobalMearlSkillOperation(operation, options = {}) {
143
+ const runner = options.runCommand ?? runCommand;
144
+ const writeLine = options.writeLine ?? (line => console.error(line));
145
+ const invocation = resolveAliSkillsInvocation(options);
146
+ if (!invocation) {
147
+ writeLine('Skill: ali-skills and npx are both unavailable.');
148
+ return false;
149
+ }
150
+ const run = async (args, capture) => {
151
+ try {
152
+ return await runner(invocation.command, [...invocation.argsPrefix, ...args], {
153
+ capture,
154
+ env: invocation.env,
155
+ timeoutMs: COMMAND_TIMEOUT_MS,
156
+ });
157
+ }
158
+ catch (error) {
159
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}`);
160
+ return null;
161
+ }
162
+ };
163
+ const listMearlPaths = async () => {
164
+ const listed = await run(['list', '--global', '--json'], true);
165
+ if (!listed)
166
+ return null;
167
+ if (listed.code !== 0) {
168
+ writeLine(`Skill: unable to list global skills (exit ${listed.code}).`);
169
+ const detail = stripAnsi(listed.stderr || listed.stdout).trim();
170
+ if (detail)
171
+ writeLine(detail);
172
+ return null;
173
+ }
174
+ try {
175
+ return parseGlobalMearlSkillPaths(listed.stdout);
176
+ }
177
+ catch (error) {
178
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}.`);
179
+ return null;
180
+ }
181
+ };
182
+ const paths = await listMearlPaths();
183
+ if (!paths)
184
+ return false;
185
+ if (operation === 'install') {
186
+ if (paths.length > 0) {
187
+ writeLine('Skill: already installed:');
188
+ paths.forEach(skillPath => writeLine(` ${skillPath}`));
189
+ return true;
190
+ }
191
+ const installed = await run(['add', 'trip/mearl', '--skill', 'mearl', '--global', ...(options.yes ? ['--yes'] : [])], false);
192
+ if (!installed)
193
+ return false;
194
+ if (installed.code !== 0) {
195
+ writeLine(`Skill: install failed (exit ${installed.code}).`);
196
+ return false;
197
+ }
198
+ const installedPaths = await listMearlPaths();
199
+ if (!installedPaths || installedPaths.length === 0) {
200
+ writeLine('Skill: installation finished without a tracked global mearl skill.');
201
+ return false;
202
+ }
203
+ writeLine('Skill: installed.');
204
+ return true;
205
+ }
206
+ if (paths.length === 0) {
207
+ writeLine('Skill: no tracked global mearl skill found; skipped.');
208
+ return true;
209
+ }
210
+ writeLine('Skill installation:');
211
+ paths.forEach(skillPath => writeLine(` ${skillPath}`));
212
+ const args = operation === 'update'
213
+ ? ['update', 'mearl', '--global', '--yes']
214
+ : ['remove', 'mearl', '--global', '--yes'];
215
+ const result = await run(args, false);
216
+ if (!result)
217
+ return false;
218
+ if (result.code !== 0) {
219
+ writeLine(`Skill: ${operation} failed (exit ${result.code}).`);
220
+ return false;
221
+ }
222
+ writeLine(`Skill: ${operation === 'update' ? 'updated' : 'uninstalled'}.`);
223
+ return true;
224
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { type MearlLifecycleOperation } from './lifecycle.js';
3
+ import type { MearlProfile } from './profiles.js';
4
+ interface ParsedArgs {
5
+ help: boolean;
6
+ operation?: MearlLifecycleOperation;
7
+ profile?: MearlProfile;
8
+ yes: boolean;
9
+ }
10
+ export declare function parseSetupArgs(argv: string[]): ParsedArgs;
11
+ export declare function main(argv?: string[], interactiveTerminal?: boolean): Promise<number>;
12
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { runMearlLifecycle } from './lifecycle.js';
5
+ export function parseSetupArgs(argv) {
6
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
7
+ return { help: true, yes: false };
8
+ }
9
+ const operation = argv[0];
10
+ if (operation !== 'install' && operation !== 'update' && operation !== 'uninstall') {
11
+ throw new Error(`Unknown operation: ${operation}`);
12
+ }
13
+ let profile;
14
+ let yes = false;
15
+ for (const arg of argv.slice(1)) {
16
+ if (arg === '--yes' || arg === '-y' || arg === '--non-interactive') {
17
+ yes = true;
18
+ continue;
19
+ }
20
+ const candidate = arg === '--local'
21
+ ? 'local'
22
+ : arg === '--cloud'
23
+ ? 'cloud'
24
+ : arg === '--mcp'
25
+ ? 'mcp'
26
+ : undefined;
27
+ if (!candidate)
28
+ throw new Error(`Unknown option: ${arg}`);
29
+ if (profile && profile !== candidate) {
30
+ throw new Error('Choose only one of --local, --cloud, or --mcp');
31
+ }
32
+ profile = candidate;
33
+ }
34
+ return { help: false, operation, profile, yes };
35
+ }
36
+ function printHelp() {
37
+ console.error(`Mearl Setup
38
+
39
+ Usage:
40
+ npx @mearl/setup <install|update|uninstall> [--local|--cloud|--mcp] [--yes]
41
+
42
+ Behavior:
43
+ install Defaults to --local when no environment is specified
44
+ update Auto-detects installed environments from the current PATH
45
+ uninstall Auto-detects installed environments and asks for confirmation
46
+
47
+ Options:
48
+ --local Manage @mearl/native-host and @mearl/client
49
+ --cloud Manage @mearl/cloud-client
50
+ --mcp Manage @mearl/mcp-server and its configuration
51
+ --yes, -y Run without prompts and accept detected defaults
52
+ --non-interactive
53
+ Alias for --yes, intended for Agents and CI
54
+ --help, -h Show this help
55
+
56
+ Examples:
57
+ npx @mearl/setup install
58
+ npx @mearl/setup install --yes
59
+ npx @mearl/setup install --cloud
60
+ npx @mearl/setup update
61
+ npx @mearl/setup uninstall --local`);
62
+ }
63
+ export async function main(argv = process.argv.slice(2), interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true) {
64
+ let parsed;
65
+ try {
66
+ parsed = parseSetupArgs(argv);
67
+ }
68
+ catch (error) {
69
+ console.error(error instanceof Error ? error.message : String(error));
70
+ printHelp();
71
+ return 2;
72
+ }
73
+ if (parsed.help) {
74
+ printHelp();
75
+ return 0;
76
+ }
77
+ if (!interactiveTerminal && !parsed.yes && parsed.operation !== 'update') {
78
+ console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
79
+ return 2;
80
+ }
81
+ try {
82
+ const ok = await runMearlLifecycle({
83
+ operation: parsed.operation,
84
+ profile: parsed.profile,
85
+ yes: parsed.yes,
86
+ });
87
+ return ok ? 0 : 1;
88
+ }
89
+ catch (error) {
90
+ console.error(error instanceof Error ? error.message : String(error));
91
+ return 1;
92
+ }
93
+ }
94
+ const currentFile = fileURLToPath(import.meta.url);
95
+ if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
96
+ process.exitCode = await main();
97
+ }
@@ -0,0 +1,8 @@
1
+ export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
+ export type { DetectedGlobalPackage, GlobalPackageCommand, GlobalPackageManager, PackageManagerInstallation, PackageOperation, PackageDetectionOptions, } from './packageManager.js';
3
+ export { runMearlLifecycle } from './lifecycle.js';
4
+ export type { MearlLifecycleDependencies, MearlLifecycleOperation, MearlLifecycleOptions, } from './lifecycle.js';
5
+ export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
6
+ export type { DetectedMearlProfiles, MearlProfile, MearlProfileConfig } from './profiles.js';
7
+ export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
8
+ export type { NativeHostCleanupDependencies, NativeHostCleanupOptions, NativeHostIntegrationMode, } from './nativeHost.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
+ export { runMearlLifecycle } from './lifecycle.js';
3
+ export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
4
+ export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
@@ -0,0 +1,24 @@
1
+ import { runGlobalMearlSkillOperation } from './aliSkills.js';
2
+ import { detectGlobalPackage, type PackageManagerInstallation } from './packageManager.js';
3
+ import { type DetectedMearlProfiles, type MearlProfile } from './profiles.js';
4
+ import { uninstallNativeHostArtifacts } from './nativeHost.js';
5
+ import { type CommandRunner } from './process.js';
6
+ export type MearlLifecycleOperation = 'install' | 'update' | 'uninstall';
7
+ export interface MearlLifecycleOptions {
8
+ operation: MearlLifecycleOperation;
9
+ profile?: MearlProfile;
10
+ yes?: boolean;
11
+ env?: NodeJS.ProcessEnv;
12
+ }
13
+ export interface MearlLifecycleDependencies {
14
+ cleanupNativeHost?: typeof uninstallNativeHostArtifacts;
15
+ confirm?: (message: string) => Promise<boolean>;
16
+ detectPackage?: typeof detectGlobalPackage;
17
+ detectProfiles?: (env: NodeJS.ProcessEnv) => DetectedMearlProfiles;
18
+ existsSync?: (filePath: string) => boolean;
19
+ resolveInvokingManager?: (env: NodeJS.ProcessEnv) => PackageManagerInstallation;
20
+ runCommand?: CommandRunner;
21
+ runSkillOperation?: typeof runGlobalMearlSkillOperation;
22
+ writeLine?: (line: string) => void;
23
+ }
24
+ export declare function runMearlLifecycle(options: MearlLifecycleOptions, dependencies?: MearlLifecycleDependencies): Promise<boolean>;
@@ -0,0 +1,221 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+ import { runGlobalMearlSkillOperation } from './aliSkills.js';
6
+ import { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, } from './packageManager.js';
7
+ import { MEARL_PROFILES, detectMearlProfiles, } from './profiles.js';
8
+ import { uninstallNativeHostArtifacts } from './nativeHost.js';
9
+ import { executableNames, prependCommandPath, runCommand, } from './process.js';
10
+ async function confirmInTerminal(message) {
11
+ if (!input.isTTY || !output.isTTY)
12
+ return false;
13
+ const readline = createInterface({ input, output });
14
+ try {
15
+ const answer = (await readline.question(`${message} [y/N] `)).trim().toLowerCase();
16
+ return answer === 'y' || answer === 'yes';
17
+ }
18
+ finally {
19
+ readline.close();
20
+ }
21
+ }
22
+ function siblingExecutable(anchor, name, existsSync) {
23
+ for (const executableName of executableNames(name)) {
24
+ const candidate = path.join(path.dirname(anchor), executableName);
25
+ if (existsSync(candidate))
26
+ return candidate;
27
+ }
28
+ return null;
29
+ }
30
+ async function runLifecycleCommand(runner, command, args, env, label, writeLine) {
31
+ try {
32
+ return await runner(command, args, {
33
+ capture: false,
34
+ env: prependCommandPath(command, env),
35
+ });
36
+ }
37
+ catch (error) {
38
+ writeLine(`${label}: ${error instanceof Error ? error.message : String(error)}`);
39
+ return null;
40
+ }
41
+ }
42
+ async function runHook(profile, operation, detected, runner, existsSync, env, writeLine, yes) {
43
+ let executable;
44
+ let args;
45
+ if (profile === 'local' && operation !== 'uninstall') {
46
+ executable = detected.executable;
47
+ args = ['--init', '--mode', 'skills'];
48
+ }
49
+ else if (profile === 'mcp' && operation === 'uninstall') {
50
+ executable = siblingExecutable(detected.executable, 'mearl-mcp-uninstall', existsSync);
51
+ args = ['--managed-by-setup', ...(yes ? ['--yes'] : [])];
52
+ }
53
+ else if (profile === 'mcp' && operation === 'install') {
54
+ executable = siblingExecutable(detected.executable, 'mearl-mcp-configure', existsSync);
55
+ args = ['--global-install', ...(yes ? ['--yes'] : [])];
56
+ }
57
+ else if (profile === 'mcp' && operation === 'update') {
58
+ executable = siblingExecutable(detected.executable, 'mearl-mcp-configure', existsSync);
59
+ args = ['--skip-mcp', '--global-install'];
60
+ }
61
+ else {
62
+ return true;
63
+ }
64
+ if (!executable) {
65
+ writeLine(`Hook: ${profile} ${operation} command was not found.`);
66
+ return false;
67
+ }
68
+ const result = await runLifecycleCommand(runner, executable, args, env, `Hook: ${profile} ${operation}`, writeLine);
69
+ if (!result)
70
+ return false;
71
+ if (result.code !== 0) {
72
+ writeLine(`Hook: ${profile} ${operation} failed (exit ${result.code}).`);
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ function selectedProfiles(operation, requested, detected) {
78
+ if (requested)
79
+ return [requested];
80
+ if (operation === 'install')
81
+ return ['local'];
82
+ return [...detected.keys()];
83
+ }
84
+ function operationVerb(operation) {
85
+ return operation === 'uninstall' ? 'uninstall' : operation === 'update' ? 'update' : 'install';
86
+ }
87
+ export async function runMearlLifecycle(options, dependencies = {}) {
88
+ const env = options.env ?? process.env;
89
+ const writeLine = dependencies.writeLine ?? (line => console.error(line));
90
+ const existsSync = dependencies.existsSync ?? fs.existsSync;
91
+ const runner = dependencies.runCommand ?? runCommand;
92
+ const cleanupNativeHost = dependencies.cleanupNativeHost ?? uninstallNativeHostArtifacts;
93
+ const detectPackage = dependencies.detectPackage ?? detectGlobalPackage;
94
+ const detectProfiles = dependencies.detectProfiles ?? (childEnv => detectMearlProfiles({ env: childEnv }));
95
+ const resolveInvokingManager = dependencies.resolveInvokingManager ??
96
+ (childEnv => resolveInvokingPackageManager({ env: childEnv }));
97
+ const runSkill = dependencies.runSkillOperation ?? runGlobalMearlSkillOperation;
98
+ const confirm = dependencies.confirm ?? confirmInTerminal;
99
+ const detected = detectProfiles(env);
100
+ const selected = selectedProfiles(options.operation, options.profile, detected);
101
+ if (selected.length === 0) {
102
+ writeLine('No installed Mearl environment was detected on the current PATH.');
103
+ if (options.operation === 'uninstall')
104
+ return true;
105
+ writeLine('Run: npx @mearl/setup install');
106
+ return false;
107
+ }
108
+ if (options.operation === 'update') {
109
+ const missing = selected.filter(profile => !detected.has(profile));
110
+ if (missing.length > 0) {
111
+ writeLine(`Not installed: ${missing.join(', ')}. Run the install command first.`);
112
+ return false;
113
+ }
114
+ }
115
+ if (options.operation === 'uninstall') {
116
+ const installedSelected = selected.filter(profile => detected.has(profile));
117
+ if (installedSelected.length === 0) {
118
+ writeLine(`Nothing to uninstall for ${selected.join(', ')}.`);
119
+ return true;
120
+ }
121
+ if (!options.yes &&
122
+ !(await confirm(`Uninstall Mearl environments: ${installedSelected.join(', ')}?`))) {
123
+ writeLine('Uninstall cancelled.');
124
+ return false;
125
+ }
126
+ }
127
+ writeLine(`${operationVerb(options.operation)} targets: ${selected
128
+ .map(profile => MEARL_PROFILES[profile].label)
129
+ .join(', ')}`);
130
+ let packagesOk = true;
131
+ for (const profile of selected) {
132
+ const config = MEARL_PROFILES[profile];
133
+ const current = detected.get(profile);
134
+ if (options.operation === 'uninstall' && !current)
135
+ continue;
136
+ const installation = current ?? resolveInvokingManager(env);
137
+ if (options.operation === 'uninstall' && profile === 'local') {
138
+ const keepsMcp = detected.has('mcp') && !selected.includes('mcp');
139
+ let cleanupOk = false;
140
+ try {
141
+ cleanupOk = cleanupNativeHost({
142
+ mode: keepsMcp ? 'skills' : 'both',
143
+ silent: true,
144
+ });
145
+ }
146
+ catch (error) {
147
+ writeLine(`Native host: ${error instanceof Error ? error.message : String(error)}`);
148
+ }
149
+ writeLine(`Native host: ${cleanupOk ? 'cleaned.' : 'cleanup failed.'}`);
150
+ if (!cleanupOk) {
151
+ packagesOk = false;
152
+ continue;
153
+ }
154
+ }
155
+ if (options.operation === 'uninstall' &&
156
+ !(await runHook(profile, options.operation, current, runner, existsSync, env, writeLine, options.yes === true))) {
157
+ packagesOk = false;
158
+ continue;
159
+ }
160
+ const packageOperation = options.operation === 'uninstall' ? 'uninstall' : options.operation;
161
+ const command = buildGlobalPackageCommand(installation, packageOperation, config.packages);
162
+ writeLine(`Packages (${profile}): ${command.manager}`);
163
+ const result = await runLifecycleCommand(runner, command.command, command.args, env, `Packages (${profile})`, writeLine);
164
+ if (!result) {
165
+ packagesOk = false;
166
+ continue;
167
+ }
168
+ if (result.code !== 0) {
169
+ writeLine(`Packages (${profile}): failed (exit ${result.code}).`);
170
+ packagesOk = false;
171
+ continue;
172
+ }
173
+ if (options.operation !== 'uninstall' && (profile === 'local' || profile === 'mcp')) {
174
+ const refreshed = detectPackage(config.anchorPackage, config.anchorBins, {
175
+ env: prependCommandPath(command.command, env),
176
+ }) ?? current;
177
+ if (!refreshed ||
178
+ !(await runHook(profile, options.operation, refreshed, runner, existsSync, env, writeLine, options.yes === true))) {
179
+ packagesOk = false;
180
+ }
181
+ }
182
+ }
183
+ let skillOk = true;
184
+ const selectedUsesSkill = selected.some(profile => MEARL_PROFILES[profile].usesSkill);
185
+ if (packagesOk && selectedUsesSkill) {
186
+ try {
187
+ if (options.operation === 'uninstall') {
188
+ const remainingSkillProfiles = ['local', 'cloud'].filter(profile => detected.has(profile) && !selected.includes(profile));
189
+ if (remainingSkillProfiles.length > 0) {
190
+ writeLine(`Skill: kept for ${remainingSkillProfiles.join(', ')}.`);
191
+ }
192
+ else {
193
+ skillOk = await runSkill('uninstall', {
194
+ env,
195
+ yes: options.yes,
196
+ writeLine,
197
+ runCommand: runner,
198
+ });
199
+ }
200
+ }
201
+ else {
202
+ skillOk = await runSkill(options.operation, {
203
+ env,
204
+ yes: options.yes,
205
+ writeLine,
206
+ runCommand: runner,
207
+ });
208
+ }
209
+ }
210
+ catch (error) {
211
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}`);
212
+ skillOk = false;
213
+ }
214
+ }
215
+ if (packagesOk && skillOk) {
216
+ writeLine('Mearl setup complete.');
217
+ return true;
218
+ }
219
+ writeLine('Mearl setup incomplete. Review the messages above.');
220
+ return false;
221
+ }
@@ -0,0 +1,19 @@
1
+ import fs from 'node:fs';
2
+ export type NativeHostIntegrationMode = 'skills' | 'mcp';
3
+ export declare const MEARL_NATIVE_HOST_NAMES: Record<NativeHostIntegrationMode, string>;
4
+ export interface NativeHostCleanupOptions {
5
+ mode?: NativeHostIntegrationMode | 'both';
6
+ silent?: boolean;
7
+ }
8
+ export interface NativeHostCleanupDependencies {
9
+ env?: NodeJS.ProcessEnv;
10
+ existsSync?: typeof fs.existsSync;
11
+ homedir?: () => string;
12
+ hostDir?: string;
13
+ platform?: NodeJS.Platform;
14
+ unlinkSync?: typeof fs.unlinkSync;
15
+ unregisterWindowsNativeHost?: (hostName: string) => void;
16
+ writeLine?: (line: string) => void;
17
+ }
18
+ export declare function resolveNativeHostDirectory(options?: Pick<NativeHostCleanupDependencies, 'env' | 'homedir' | 'platform'>): string;
19
+ export declare function uninstallNativeHostArtifacts(options?: NativeHostCleanupOptions, dependencies?: NativeHostCleanupDependencies): boolean;
@@ -0,0 +1,75 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ export const MEARL_NATIVE_HOST_NAMES = {
6
+ skills: 'com.alibaba.mearl.skills',
7
+ mcp: 'com.alibaba.mearl.mcp',
8
+ };
9
+ export function resolveNativeHostDirectory(options = {}) {
10
+ const env = options.env ?? process.env;
11
+ const home = (options.homedir ?? os.homedir)();
12
+ switch (options.platform ?? process.platform) {
13
+ case 'darwin':
14
+ return path.join(home, 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
15
+ case 'linux':
16
+ return path.join(home, '.config/google-chrome/NativeMessagingHosts');
17
+ case 'win32':
18
+ return path.join(env.PROGRAMDATA || 'C:\\ProgramData', 'Google\\Chrome\\NativeMessagingHosts');
19
+ default:
20
+ throw new Error(`Unsupported platform: ${String(options.platform ?? process.platform)}`);
21
+ }
22
+ }
23
+ function unregisterWindowsNativeHost(hostName) {
24
+ const registryKey = `HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\${hostName}`;
25
+ try {
26
+ execFileSync('reg', ['delete', registryKey, '/f'], { stdio: 'pipe' });
27
+ }
28
+ catch {
29
+ // A missing registry key is already the desired state.
30
+ }
31
+ }
32
+ export function uninstallNativeHostArtifacts(options = {}, dependencies = {}) {
33
+ const { mode = 'both', silent = false } = options;
34
+ if (mode === 'both') {
35
+ return ['skills', 'mcp']
36
+ .map(integrationMode => uninstallNativeHostArtifacts({ ...options, mode: integrationMode }, dependencies))
37
+ .every(Boolean);
38
+ }
39
+ const writeLine = dependencies.writeLine ?? (line => console.error(line));
40
+ const log = (line) => {
41
+ if (!silent)
42
+ writeLine(line);
43
+ };
44
+ try {
45
+ const existsSync = dependencies.existsSync ?? fs.existsSync;
46
+ const platform = dependencies.platform ?? process.platform;
47
+ const hostDir = dependencies.hostDir ??
48
+ resolveNativeHostDirectory({
49
+ env: dependencies.env,
50
+ homedir: dependencies.homedir,
51
+ platform,
52
+ });
53
+ const unlinkSync = dependencies.unlinkSync ?? fs.unlinkSync;
54
+ const unregister = dependencies.unregisterWindowsNativeHost ?? unregisterWindowsNativeHost;
55
+ const hostName = MEARL_NATIVE_HOST_NAMES[mode];
56
+ const launcherExtension = platform === 'win32' ? 'cmd' : 'sh';
57
+ const artifacts = [
58
+ path.join(hostDir, `${hostName}.json`),
59
+ path.join(hostDir, `${hostName}-launcher.${launcherExtension}`),
60
+ ];
61
+ for (const artifact of artifacts) {
62
+ if (!existsSync(artifact))
63
+ continue;
64
+ unlinkSync(artifact);
65
+ log(`✅ Removed: ${artifact}`);
66
+ }
67
+ if (platform === 'win32')
68
+ unregister(hostName);
69
+ return true;
70
+ }
71
+ catch (error) {
72
+ log(`❌ Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
73
+ return false;
74
+ }
75
+ }
@@ -0,0 +1,31 @@
1
+ export type GlobalPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'volta';
2
+ export type PackageOperation = 'install' | 'update' | 'uninstall';
3
+ export interface PackageManagerInstallation {
4
+ manager: GlobalPackageManager;
5
+ command: string;
6
+ }
7
+ export interface GlobalPackageCommand extends PackageManagerInstallation {
8
+ args: string[];
9
+ }
10
+ export interface DetectedGlobalPackage extends PackageManagerInstallation {
11
+ packageName: string;
12
+ packageRoot: string;
13
+ executable: string;
14
+ }
15
+ export interface PackageDetectionOptions {
16
+ env?: NodeJS.ProcessEnv;
17
+ existsSync?: (filePath: string) => boolean;
18
+ platform?: NodeJS.Platform;
19
+ processExecPath?: string;
20
+ readFileSync?: (filePath: string, encoding: BufferEncoding) => string;
21
+ realpathSync?: (filePath: string) => string;
22
+ }
23
+ export declare function resolvePackageManagerInstallation(packageRoot: string, packageName: string, options?: PackageDetectionOptions): PackageManagerInstallation | null;
24
+ export declare function buildGlobalPackageCommand(installation: PackageManagerInstallation, operation: PackageOperation, packages: string[]): GlobalPackageCommand;
25
+ export declare function resolveInvokingPackageManager(options?: {
26
+ env?: NodeJS.ProcessEnv;
27
+ existsSync?: (filePath: string) => boolean;
28
+ platform?: NodeJS.Platform;
29
+ processExecPath?: string;
30
+ }): PackageManagerInstallation;
31
+ export declare function detectGlobalPackage(packageName: string, binNames: string[], options?: PackageDetectionOptions): DetectedGlobalPackage | null;
@@ -0,0 +1,200 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { executableNames, findExecutable, isExecutable } from './process.js';
4
+ function normalizePath(filePath) {
5
+ return filePath.replaceAll('\\', '/').replace(/\/+$/, '');
6
+ }
7
+ function firstExisting(candidates, existsSync) {
8
+ return candidates.find(candidate => existsSync(candidate));
9
+ }
10
+ function fallbackCommand(name, platform) {
11
+ if (platform !== 'win32')
12
+ return name;
13
+ if (name === 'bun' || name === 'volta')
14
+ return `${name}.exe`;
15
+ return `${name}.cmd`;
16
+ }
17
+ export function resolvePackageManagerInstallation(packageRoot, packageName, options = {}) {
18
+ const env = options.env ?? process.env;
19
+ const platform = options.platform ?? process.platform;
20
+ const existsSync = options.existsSync ?? (candidate => isExecutable(candidate, platform));
21
+ const processExecPath = options.processExecPath ?? process.execPath;
22
+ const normalizedRoot = normalizePath(path.resolve(packageRoot));
23
+ const lowerRoot = normalizedRoot.toLowerCase();
24
+ const lowerPackageName = packageName.toLowerCase();
25
+ const pnpmGlobalRoot = /\/global\/\d+\/(?:\.pnpm\/.*\/)?node_modules\//.exec(lowerRoot);
26
+ if (pnpmGlobalRoot && lowerRoot.endsWith(`/node_modules/${lowerPackageName}`)) {
27
+ const pnpmHome = normalizedRoot.slice(0, pnpmGlobalRoot.index);
28
+ const command = firstExisting([
29
+ ...executableNames('pnpm', platform).map(name => path.join(pnpmHome, name)),
30
+ ...executableNames('pnpm', platform).map(name => path.join(path.dirname(processExecPath), name)),
31
+ ], existsSync) ?? fallbackCommand('pnpm', platform);
32
+ return { manager: 'pnpm', command };
33
+ }
34
+ const bunMarker = `/.bun/install/global/node_modules/${lowerPackageName}`;
35
+ if (lowerRoot.endsWith(bunMarker)) {
36
+ const bunHome = normalizedRoot.slice(0, lowerRoot.indexOf('/.bun/'));
37
+ const command = firstExisting(executableNames('bun', platform).map(name => path.join(bunHome, '.bun', 'bin', name)), existsSync) ?? fallbackCommand('bun', platform);
38
+ return { manager: 'bun', command };
39
+ }
40
+ if ((lowerRoot.includes('/.config/yarn/global/node_modules/') ||
41
+ lowerRoot.includes('/.yarn/global/node_modules/') ||
42
+ lowerRoot.includes('/yarn/global/node_modules/') ||
43
+ lowerRoot.includes('/yarn/data/global/node_modules/')) &&
44
+ lowerRoot.endsWith(`/node_modules/${lowerPackageName}`)) {
45
+ return { manager: 'yarn', command: fallbackCommand('yarn', platform) };
46
+ }
47
+ if (lowerRoot.includes('/.volta/tools/image/packages/') &&
48
+ lowerRoot.endsWith(`/lib/node_modules/${lowerPackageName}`)) {
49
+ const voltaHome = normalizedRoot.slice(0, lowerRoot.indexOf('/.volta/'));
50
+ const command = firstExisting(executableNames('volta', platform).map(name => path.join(voltaHome, '.volta', 'bin', name)), existsSync) ?? fallbackCommand('volta', platform);
51
+ return { manager: 'volta', command };
52
+ }
53
+ const npmMarker = `/lib/node_modules/${lowerPackageName}`;
54
+ if (lowerRoot.endsWith(npmMarker)) {
55
+ const prefix = normalizedRoot.slice(0, lowerRoot.length - npmMarker.length);
56
+ const command = firstExisting([
57
+ ...executableNames('npm', platform).map(name => path.join(prefix, 'bin', name)),
58
+ ...executableNames('npm', platform).map(name => path.join(path.dirname(processExecPath), name)),
59
+ ], existsSync) ?? fallbackCommand('npm', platform);
60
+ return { manager: 'npm', command };
61
+ }
62
+ if (platform === 'win32' && env.APPDATA) {
63
+ const npmRoot = normalizePath(path.join(env.APPDATA, 'npm', 'node_modules', packageName));
64
+ if (lowerRoot === npmRoot.toLowerCase()) {
65
+ return { manager: 'npm', command: fallbackCommand('npm', platform) };
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ export function buildGlobalPackageCommand(installation, operation, packages) {
71
+ const targets = operation === 'uninstall' ? packages : packages.map(packageName => `${packageName}@latest`);
72
+ let args;
73
+ switch (installation.manager) {
74
+ case 'npm':
75
+ args =
76
+ operation === 'uninstall' ? ['uninstall', '-g', ...targets] : ['install', '-g', ...targets];
77
+ break;
78
+ case 'pnpm':
79
+ args = operation === 'uninstall' ? ['remove', '-g', ...targets] : ['add', '-g', ...targets];
80
+ break;
81
+ case 'yarn':
82
+ args =
83
+ operation === 'uninstall'
84
+ ? ['global', 'remove', ...targets]
85
+ : ['global', 'add', ...targets];
86
+ break;
87
+ case 'bun':
88
+ args = operation === 'uninstall' ? ['remove', '-g', ...targets] : ['add', '-g', ...targets];
89
+ break;
90
+ case 'volta':
91
+ args = operation === 'uninstall' ? ['uninstall', ...targets] : ['install', ...targets];
92
+ break;
93
+ }
94
+ return { ...installation, args };
95
+ }
96
+ export function resolveInvokingPackageManager(options = {}) {
97
+ const env = options.env ?? process.env;
98
+ const existsSync = options.existsSync ?? fs.existsSync;
99
+ const platform = options.platform ?? process.platform;
100
+ const processExecPath = options.processExecPath ?? process.execPath;
101
+ const userAgent = (env.npm_config_user_agent ?? '').toLowerCase();
102
+ const manager = userAgent.startsWith('pnpm/')
103
+ ? 'pnpm'
104
+ : userAgent.startsWith('yarn/')
105
+ ? 'yarn'
106
+ : userAgent.startsWith('bun/')
107
+ ? 'bun'
108
+ : 'npm';
109
+ const sibling = firstExisting(executableNames(manager, platform).map(name => path.join(path.dirname(processExecPath), name)), existsSync);
110
+ return { manager, command: sibling ?? fallbackCommand(manager, platform) };
111
+ }
112
+ function readPackageName(packageRoot, readFileSync) {
113
+ try {
114
+ const parsed = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
115
+ return typeof parsed.name === 'string' ? parsed.name : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ function findPackageRoot(entryPath, packageName, readFileSync) {
122
+ let current = path.dirname(entryPath);
123
+ for (let depth = 0; depth < 12; depth += 1) {
124
+ if (readPackageName(current, readFileSync) === packageName)
125
+ return current;
126
+ const parent = path.dirname(current);
127
+ if (parent === current)
128
+ break;
129
+ current = parent;
130
+ }
131
+ return null;
132
+ }
133
+ function packageRootFromWrapper(executable, packageName, readFileSync) {
134
+ try {
135
+ const content = readFileSync(executable, 'utf8').slice(0, 32_000).replaceAll('\\', '/');
136
+ const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
137
+ const packageSuffix = `node_modules/${escapedName}`;
138
+ const wrapperDirectory = path.dirname(executable);
139
+ const candidates = [];
140
+ // npm and pnpm wrappers resolve their entry point relative to the bin
141
+ // directory. Preserve the complete relative prefix because pnpm places
142
+ // packages below global/<version>/.pnpm rather than bin/node_modules.
143
+ const relativePattern = new RegExp(`(?:\\$basedir|%~?dp0%?)/([^"'\\r\\n]*?${packageSuffix})(?=/|["'\\s])`, 'gi');
144
+ for (const match of content.matchAll(relativePattern)) {
145
+ candidates.push(path.resolve(wrapperDirectory, match[1].replaceAll('/', path.sep)));
146
+ }
147
+ // Some package managers emit an absolute package entry in the wrapper.
148
+ const absolutePattern = new RegExp(`((?:[A-Za-z]:)?/[^"'\\r\\n:]*?${packageSuffix})(?=/|["'\\s:])`, 'gi');
149
+ for (const match of content.matchAll(absolutePattern)) {
150
+ candidates.push(path.resolve(match[1].replaceAll('/', path.sep)));
151
+ }
152
+ for (const candidate of new Set(candidates)) {
153
+ if (readPackageName(candidate, readFileSync) === packageName)
154
+ return candidate;
155
+ }
156
+ return null;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ export function detectGlobalPackage(packageName, binNames, options = {}) {
163
+ const env = options.env ?? process.env;
164
+ const platform = options.platform ?? process.platform;
165
+ const existsSync = options.existsSync ?? (candidate => isExecutable(candidate, platform));
166
+ const readFileSync = options.readFileSync ?? fs.readFileSync;
167
+ const realpathSync = options.realpathSync ?? fs.realpathSync;
168
+ const processExecPath = options.processExecPath ?? process.execPath;
169
+ const extraDirectories = [path.dirname(processExecPath)];
170
+ for (const binName of binNames) {
171
+ const executable = findExecutable(binName, {
172
+ env,
173
+ existsSync,
174
+ platform,
175
+ extraDirectories,
176
+ });
177
+ if (!executable)
178
+ continue;
179
+ let packageRoot = null;
180
+ try {
181
+ packageRoot = findPackageRoot(realpathSync(executable), packageName, readFileSync);
182
+ }
183
+ catch {
184
+ // Windows package-manager wrappers are regular files rather than symlinks.
185
+ }
186
+ packageRoot ??= packageRootFromWrapper(executable, packageName, readFileSync);
187
+ if (!packageRoot)
188
+ continue;
189
+ const installation = resolvePackageManagerInstallation(packageRoot, packageName, {
190
+ env,
191
+ existsSync,
192
+ platform,
193
+ processExecPath,
194
+ });
195
+ if (!installation)
196
+ continue;
197
+ return { packageName, packageRoot, executable, ...installation };
198
+ }
199
+ return null;
200
+ }
@@ -0,0 +1,28 @@
1
+ export interface SpawnCommand {
2
+ command: string;
3
+ args: string[];
4
+ }
5
+ export interface CommandResult {
6
+ code: number;
7
+ stdout: string;
8
+ stderr: string;
9
+ }
10
+ export interface RunCommandOptions {
11
+ capture?: boolean;
12
+ cwd?: string;
13
+ env?: NodeJS.ProcessEnv;
14
+ timeoutMs?: number;
15
+ }
16
+ export type CommandRunner = (command: string, args: string[], options?: RunCommandOptions) => Promise<CommandResult>;
17
+ export declare function isExecutable(filePath: string, platform?: NodeJS.Platform): boolean;
18
+ export declare function executableNames(name: string, platform?: NodeJS.Platform): string[];
19
+ export declare function resolveSpawnCommand(executable: string, args: string[], platform?: NodeJS.Platform, commandInterpreter?: string | undefined): SpawnCommand;
20
+ export declare function prependCommandPath(command: string, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
21
+ export declare function prependNodePath(env?: NodeJS.ProcessEnv, nodeExecutable?: string): NodeJS.ProcessEnv;
22
+ export declare function findExecutable(name: string, options?: {
23
+ env?: NodeJS.ProcessEnv;
24
+ existsSync?: (filePath: string) => boolean;
25
+ platform?: NodeJS.Platform;
26
+ extraDirectories?: string[];
27
+ }): string | null;
28
+ export declare const runCommand: CommandRunner;
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ const MAX_CAPTURE_LENGTH = 1_000_000;
5
+ export function isExecutable(filePath, platform = process.platform) {
6
+ try {
7
+ fs.accessSync(filePath, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export function executableNames(name, platform = process.platform) {
15
+ return platform === 'win32' ? [`${name}.cmd`, `${name}.exe`, name] : [name];
16
+ }
17
+ export function resolveSpawnCommand(executable, args, platform = process.platform, commandInterpreter = process.env.ComSpec) {
18
+ if (platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable)) {
19
+ return {
20
+ command: commandInterpreter || 'cmd.exe',
21
+ args: ['/d', '/s', '/c', executable, ...args],
22
+ };
23
+ }
24
+ return { command: executable, args };
25
+ }
26
+ export function prependCommandPath(command, env = process.env) {
27
+ if (!path.isAbsolute(command))
28
+ return env;
29
+ return prependPath(path.dirname(command), env);
30
+ }
31
+ export function prependNodePath(env = process.env, nodeExecutable = process.execPath) {
32
+ return prependPath(path.dirname(nodeExecutable), env);
33
+ }
34
+ function prependPath(directory, env) {
35
+ const pathKey = Object.keys(env).find(key => key.toLowerCase() === 'path') || 'PATH';
36
+ const inherited = env[pathKey] || '';
37
+ const entries = inherited.split(path.delimiter).filter(Boolean);
38
+ if (entries.includes(directory))
39
+ return env;
40
+ return { ...env, [pathKey]: [directory, ...entries].join(path.delimiter) };
41
+ }
42
+ export function findExecutable(name, options = {}) {
43
+ const env = options.env ?? process.env;
44
+ const platform = options.platform ?? process.platform;
45
+ const existsSync = options.existsSync ?? (candidate => isExecutable(candidate, platform));
46
+ const pathKey = Object.keys(env).find(key => key.toLowerCase() === 'path') || 'PATH';
47
+ const directories = [
48
+ ...(options.extraDirectories ?? []),
49
+ ...(env[pathKey] ?? '').split(path.delimiter).filter(Boolean),
50
+ ];
51
+ for (const directory of directories) {
52
+ for (const executableName of executableNames(name, platform)) {
53
+ const candidate = path.join(directory, executableName);
54
+ if (existsSync(candidate))
55
+ return candidate;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ export const runCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
61
+ const capture = options.capture === true;
62
+ const launch = resolveSpawnCommand(command, args);
63
+ const child = spawn(launch.command, launch.args, {
64
+ cwd: options.cwd,
65
+ env: options.env,
66
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
67
+ windowsHide: true,
68
+ });
69
+ let stdout = '';
70
+ let stderr = '';
71
+ let settled = false;
72
+ const append = (current, chunk) => current.length >= MAX_CAPTURE_LENGTH
73
+ ? current
74
+ : (current + chunk.toString('utf8')).slice(0, MAX_CAPTURE_LENGTH);
75
+ const finish = (error, code = 1) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ if (timer)
80
+ clearTimeout(timer);
81
+ if (error)
82
+ reject(error);
83
+ else
84
+ resolve({ code, stdout, stderr });
85
+ };
86
+ const timer = options.timeoutMs === undefined
87
+ ? undefined
88
+ : setTimeout(() => {
89
+ child.kill('SIGTERM');
90
+ finish(new Error(`Command timed out after ${options.timeoutMs}ms`));
91
+ }, options.timeoutMs);
92
+ if (capture) {
93
+ child.stdout?.on('data', chunk => (stdout = append(stdout, chunk)));
94
+ child.stderr?.on('data', chunk => (stderr = append(stderr, chunk)));
95
+ }
96
+ child.once('error', error => finish(error));
97
+ child.once('close', code => finish(undefined, code ?? 1));
98
+ });
@@ -0,0 +1,13 @@
1
+ import { detectGlobalPackage, type DetectedGlobalPackage } from './packageManager.js';
2
+ export type MearlProfile = 'local' | 'cloud' | 'mcp';
3
+ export interface MearlProfileConfig {
4
+ id: MearlProfile;
5
+ label: string;
6
+ anchorPackage: string;
7
+ anchorBins: string[];
8
+ packages: string[];
9
+ usesSkill: boolean;
10
+ }
11
+ export declare const MEARL_PROFILES: Record<MearlProfile, MearlProfileConfig>;
12
+ export type DetectedMearlProfiles = Map<MearlProfile, DetectedGlobalPackage>;
13
+ export declare function detectMearlProfiles(options?: Parameters<typeof detectGlobalPackage>[2]): DetectedMearlProfiles;
@@ -0,0 +1,36 @@
1
+ import { detectGlobalPackage } from './packageManager.js';
2
+ export const MEARL_PROFILES = {
3
+ local: {
4
+ id: 'local',
5
+ label: 'local browser',
6
+ anchorPackage: '@mearl/native-host',
7
+ anchorBins: ['mearl-native-host'],
8
+ packages: ['@mearl/native-host', '@mearl/client'],
9
+ usesSkill: true,
10
+ },
11
+ cloud: {
12
+ id: 'cloud',
13
+ label: 'cloud agent',
14
+ anchorPackage: '@mearl/cloud-client',
15
+ anchorBins: ['mearl-cloud'],
16
+ packages: ['@mearl/cloud-client'],
17
+ usesSkill: true,
18
+ },
19
+ mcp: {
20
+ id: 'mcp',
21
+ label: 'MCP',
22
+ anchorPackage: '@mearl/mcp-server',
23
+ anchorBins: ['mearl-mcp-server'],
24
+ packages: ['@mearl/mcp-server'],
25
+ usesSkill: false,
26
+ },
27
+ };
28
+ export function detectMearlProfiles(options = {}) {
29
+ const detected = new Map();
30
+ for (const profile of Object.values(MEARL_PROFILES)) {
31
+ const installation = detectGlobalPackage(profile.anchorPackage, profile.anchorBins, options);
32
+ if (installation)
33
+ detected.set(profile.id, installation);
34
+ }
35
+ return detected;
36
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@mearl/setup",
3
+ "version": "2.2.2",
4
+ "description": "One-shot installer, updater, and uninstaller for Mearl environments",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./ali-skills": {
14
+ "types": "./dist/aliSkills.d.ts",
15
+ "import": "./dist/aliSkills.js"
16
+ },
17
+ "./process": {
18
+ "types": "./dist/process.d.ts",
19
+ "import": "./dist/process.js"
20
+ },
21
+ "./native-host": {
22
+ "types": "./dist/nativeHost.d.ts",
23
+ "import": "./dist/nativeHost.js"
24
+ }
25
+ },
26
+ "bin": {
27
+ "mearl-setup": "./dist/cli.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "keywords": [
34
+ "mearl",
35
+ "setup",
36
+ "installer",
37
+ "update",
38
+ "uninstall"
39
+ ],
40
+ "license": "ISC",
41
+ "publishConfig": {
42
+ "registry": "https://registry.npmjs.org"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^24.9.1",
46
+ "typescript": "^5.8.3"
47
+ },
48
+ "scripts": {
49
+ "build": "rm -rf dist/ && tsc && chmod +x dist/cli.js",
50
+ "dev": "tsc --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run"
53
+ }
54
+ }