@mearl/setup 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # @mearl/setup
2
2
 
3
- Mearl 环境的一次性安装、更新与卸载工具。无需先全局安装 setup:
3
+ Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全局安装 setup:
4
4
 
5
5
  ```bash
6
6
  npx @mearl/setup <install|update|uninstall> [--local|--cloud|--mcp] [--yes]
7
7
  ```
8
8
 
9
+ 配置 AgentBay API Key 时只需执行一次下面的命令,然后在隐藏输入提示中粘贴 Key。
10
+ 重复执行会更新现有 Key,并保留 `~/.mearl/.env` 中的其他配置:
11
+
12
+ ```bash
13
+ npx @mearl/setup agentbay
14
+ ```
15
+
16
+ 无交互环境可使用 `--stdin` 或 `--key-file <path>`。为避免 Key 进入 shell history 和
17
+ 进程参数,不接受把 Key 直接写在命令行中。
18
+
9
19
  ## 行为
10
20
 
11
21
  - `install` 未指定环境时默认 `--local`。
@@ -14,6 +24,8 @@ npx @mearl/setup <install|update|uninstall> [--local|--cloud|--mcp] [--yes]
14
24
  多个已安装环境。
15
25
  - 显式传入 `--local`、`--cloud` 或 `--mcp` 时只处理指定环境。
16
26
  - 已有环境沿用其原包管理器和全局安装前缀;首次安装沿用启动 setup 的包管理器。
27
+ - cloud 环境统一管理 `@mearl/cloud-server` 与 `@mearl/cloud-client`:首次安装后启动
28
+ Server,更新后仅在 Server 原本运行时重启,卸载前停止。
17
29
  - local/cloud 共用的全局 mearl skill 通过 `ali-skills` 的 JSON 清单定位,只处理一次。
18
30
  - 卸载一个环境但另一个仍依赖全局 mearl skill 时会保留 skill。
19
31
  - `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
@@ -59,6 +71,12 @@ npx @mearl/setup uninstall --local
59
71
 
60
72
  # 自动检测并无交互卸载
61
73
  npx @mearl/setup uninstall --yes
74
+
75
+ # 配置或更新 AgentBay API Key
76
+ npx @mearl/setup agentbay
77
+
78
+ # 无交互配置(从权限受控的文件读取)
79
+ npx @mearl/setup agentbay --key-file ./agentbay.key
62
80
  ```
63
81
 
64
82
  原有的 npm/npx 安装方式和各包 CLI 保持可用;setup 只是统一的便利入口。
@@ -0,0 +1,23 @@
1
+ export interface AgentBayConfigurationOptions {
2
+ envFile?: string;
3
+ }
4
+ export interface AgentBayConfigurationResult {
5
+ envFile: string;
6
+ updated: boolean;
7
+ }
8
+ export interface AgentBaySecretInput {
9
+ isRaw?: boolean;
10
+ isTTY?: boolean;
11
+ on(event: 'data', listener: (chunk: Buffer | string) => void): unknown;
12
+ pause(): unknown;
13
+ removeListener(event: 'data', listener: (chunk: Buffer | string) => void): unknown;
14
+ resume(): unknown;
15
+ setRawMode?(mode: boolean): unknown;
16
+ }
17
+ export interface AgentBaySecretOutput {
18
+ write(value: string): unknown;
19
+ }
20
+ export declare function readAgentBayApiKeyFile(file: string): string;
21
+ export declare function readAgentBayApiKeyStdin(input?: NodeJS.ReadableStream): Promise<string>;
22
+ export declare function promptAgentBayApiKey(input?: AgentBaySecretInput, output?: AgentBaySecretOutput): Promise<string>;
23
+ export declare function configureAgentBayApiKey(apiKey: string, options?: AgentBayConfigurationOptions): AgentBayConfigurationResult;
@@ -0,0 +1,114 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ const AGENTBAY_ENV_KEY = 'AGENTBAY_KEY';
5
+ const AGENTBAY_ENV_LINE = /^\s*(?:export\s+)?AGENTBAY_KEY\s*=/;
6
+ function normalizeApiKey(apiKey) {
7
+ const value = apiKey.trim();
8
+ if (!value)
9
+ throw new Error('AgentBay API key is required.');
10
+ if (/[\r\n\0]/.test(value)) {
11
+ throw new Error('AgentBay API key must be a single-line value.');
12
+ }
13
+ return value;
14
+ }
15
+ export function readAgentBayApiKeyFile(file) {
16
+ const resolved = path.resolve(file);
17
+ return normalizeApiKey(fs.readFileSync(resolved, 'utf8'));
18
+ }
19
+ export async function readAgentBayApiKeyStdin(input = process.stdin) {
20
+ let value = '';
21
+ for await (const chunk of input)
22
+ value += chunk.toString();
23
+ return normalizeApiKey(value);
24
+ }
25
+ export function promptAgentBayApiKey(input = process.stdin, output = process.stderr) {
26
+ if (!input.isTTY || !input.setRawMode) {
27
+ return Promise.reject(new Error('A TTY is required to enter the AgentBay API key securely.'));
28
+ }
29
+ const setRawMode = input.setRawMode.bind(input);
30
+ return new Promise((resolvePrompt, rejectPrompt) => {
31
+ const wasRaw = input.isRaw === true;
32
+ let value = '';
33
+ let settled = false;
34
+ const finish = (error) => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ input.removeListener('data', onData);
39
+ setRawMode(wasRaw);
40
+ input.pause();
41
+ output.write('\n');
42
+ if (error)
43
+ rejectPrompt(error);
44
+ else {
45
+ try {
46
+ resolvePrompt(normalizeApiKey(value));
47
+ }
48
+ catch (validationError) {
49
+ rejectPrompt(validationError instanceof Error ? validationError : new Error(String(validationError)));
50
+ }
51
+ }
52
+ };
53
+ const onData = (chunk) => {
54
+ for (const character of chunk.toString()) {
55
+ if (character === '\u0003') {
56
+ finish(new Error('AgentBay API key input cancelled.'));
57
+ return;
58
+ }
59
+ if (character === '\r' || character === '\n') {
60
+ finish();
61
+ return;
62
+ }
63
+ if (character === '\u007f' || character === '\b') {
64
+ value = Array.from(value).slice(0, -1).join('');
65
+ continue;
66
+ }
67
+ if (character >= ' ')
68
+ value += character;
69
+ }
70
+ };
71
+ output.write('AgentBay API key: ');
72
+ setRawMode(true);
73
+ input.resume();
74
+ input.on('data', onData);
75
+ });
76
+ }
77
+ function updateEnvContent(content, apiKey) {
78
+ const newline = content.includes('\r\n') ? '\r\n' : '\n';
79
+ const lines = content ? content.split(/\r?\n/) : [];
80
+ if (lines.at(-1) === '')
81
+ lines.pop();
82
+ const entry = `${AGENTBAY_ENV_KEY}=${apiKey}`;
83
+ const output = [];
84
+ let updated = false;
85
+ for (const line of lines) {
86
+ if (!AGENTBAY_ENV_LINE.test(line)) {
87
+ output.push(line);
88
+ continue;
89
+ }
90
+ if (!updated)
91
+ output.push(entry);
92
+ updated = true;
93
+ }
94
+ if (!updated)
95
+ output.push(entry);
96
+ return { content: `${output.join(newline)}${newline}`, updated };
97
+ }
98
+ export function configureAgentBayApiKey(apiKey, options = {}) {
99
+ const value = normalizeApiKey(apiKey);
100
+ const envFile = options.envFile ?? path.join(os.homedir(), '.mearl', '.env');
101
+ let current = '';
102
+ try {
103
+ current = fs.readFileSync(envFile, 'utf8');
104
+ }
105
+ catch (error) {
106
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
107
+ throw error;
108
+ }
109
+ const next = updateEnvContent(current, value);
110
+ fs.mkdirSync(path.dirname(envFile), { recursive: true, mode: 0o700 });
111
+ fs.writeFileSync(envFile, next.content, { encoding: 'utf8', mode: 0o600 });
112
+ fs.chmodSync(envFile, 0o600);
113
+ return { envFile, updated: next.updated };
114
+ }
package/dist/cli.d.ts CHANGED
@@ -1,14 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin } from './agentBay.js';
2
3
  import { type MearlLifecycleOperation } from './lifecycle.js';
3
4
  import type { MearlProfile } from './profiles.js';
4
5
  interface ParsedArgs {
6
+ agentBayKeyFile?: string;
7
+ agentBayReadStdin: boolean;
5
8
  help: boolean;
6
- operation?: MearlLifecycleOperation;
9
+ operation?: MearlLifecycleOperation | 'agentbay';
7
10
  profile?: MearlProfile;
8
11
  skipSkill: boolean;
9
12
  targetLine?: string;
10
13
  yes: boolean;
11
14
  }
15
+ export interface SetupCliDependencies {
16
+ configureAgentBay?: typeof configureAgentBayApiKey;
17
+ promptAgentBayKey?: typeof promptAgentBayApiKey;
18
+ readAgentBayKeyFile?: typeof readAgentBayApiKeyFile;
19
+ readAgentBayKeyStdin?: typeof readAgentBayApiKeyStdin;
20
+ }
12
21
  export declare function parseSetupArgs(argv: string[]): ParsedArgs;
13
- export declare function main(argv?: string[], interactiveTerminal?: boolean): Promise<number>;
22
+ export declare function main(argv?: string[], interactiveTerminal?: boolean, dependencies?: SetupCliDependencies): Promise<number>;
14
23
  export {};
package/dist/cli.js CHANGED
@@ -1,13 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
4
5
  import { runMearlLifecycle } from './lifecycle.js';
5
6
  import { isMearlReleaseLine } from './version.js';
6
7
  export function parseSetupArgs(argv) {
7
8
  if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
8
- return { help: true, skipSkill: false, yes: false };
9
+ return { agentBayReadStdin: false, help: true, skipSkill: false, yes: false };
9
10
  }
10
11
  const operation = argv[0];
12
+ if (operation === 'agentbay') {
13
+ if (argv.length === 1) {
14
+ return {
15
+ agentBayReadStdin: false,
16
+ help: false,
17
+ operation,
18
+ skipSkill: false,
19
+ yes: false,
20
+ };
21
+ }
22
+ if (argv.length === 2 && argv[1] === '--stdin') {
23
+ return {
24
+ agentBayReadStdin: true,
25
+ help: false,
26
+ operation,
27
+ skipSkill: false,
28
+ yes: false,
29
+ };
30
+ }
31
+ if (argv.length === 3 && argv[1] === '--key-file' && argv[2]?.trim()) {
32
+ return {
33
+ agentBayKeyFile: argv[2],
34
+ agentBayReadStdin: false,
35
+ help: false,
36
+ operation,
37
+ skipSkill: false,
38
+ yes: false,
39
+ };
40
+ }
41
+ throw new Error('Usage: npx @mearl/setup agentbay [--stdin | --key-file <path>]. API keys are not accepted as command-line arguments.');
42
+ }
11
43
  if (operation !== 'install' && operation !== 'update' && operation !== 'uninstall') {
12
44
  throw new Error(`Unknown operation: ${operation}`);
13
45
  }
@@ -51,23 +83,35 @@ export function parseSetupArgs(argv) {
51
83
  if (operation === 'uninstall' && targetLine) {
52
84
  throw new Error('--target-line is not available for uninstall');
53
85
  }
54
- return { help: false, operation, profile, skipSkill, targetLine, yes };
86
+ return {
87
+ agentBayReadStdin: false,
88
+ help: false,
89
+ operation,
90
+ profile,
91
+ skipSkill,
92
+ targetLine,
93
+ yes,
94
+ };
55
95
  }
56
96
  function printHelp() {
57
97
  console.error(`Mearl Setup
58
98
 
59
99
  Usage:
60
100
  npx @mearl/setup <install|update|uninstall> [--local|--cloud|--mcp] [--yes]
101
+ npx @mearl/setup agentbay [--stdin | --key-file <path>]
61
102
 
62
103
  Behavior:
63
104
  install Defaults to --local when no environment is specified
64
105
  update Auto-detects installed environments from the current PATH
65
106
  uninstall Auto-detects installed environments and asks for confirmation
107
+ agentbay Save or update AGENTBAY_KEY without changing other Mearl settings
66
108
 
67
109
  Options:
68
110
  --local Manage @mearl/native-host and @mearl/client
69
- --cloud Manage @mearl/cloud-client
111
+ --cloud Manage @mearl/cloud-server and @mearl/cloud-client
70
112
  --mcp Manage @mearl/mcp-server and its configuration
113
+ --stdin Read the AgentBay API key from standard input
114
+ --key-file Read the AgentBay API key from a local file
71
115
  --target-line <major.minor.x>
72
116
  Install the latest packages from one release line, such as 2.3.x
73
117
  --skip-skill Keep the independently managed Mearl Skill unchanged
@@ -81,10 +125,11 @@ Examples:
81
125
  npx @mearl/setup install --yes
82
126
  npx @mearl/setup install --cloud
83
127
  npx @mearl/setup update
128
+ npx @mearl/setup agentbay
84
129
  npx @mearl/setup update --local --target-line 2.3.x --skip-skill
85
130
  npx @mearl/setup uninstall --local`);
86
131
  }
87
- export async function main(argv = process.argv.slice(2), interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true) {
132
+ export async function main(argv = process.argv.slice(2), interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true, dependencies = {}) {
88
133
  let parsed;
89
134
  try {
90
135
  parsed = parseSetupArgs(argv);
@@ -98,6 +143,31 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
98
143
  printHelp();
99
144
  return 0;
100
145
  }
146
+ if (parsed.operation === 'agentbay') {
147
+ try {
148
+ const configureAgentBay = dependencies.configureAgentBay ?? configureAgentBayApiKey;
149
+ let apiKey;
150
+ if (parsed.agentBayKeyFile) {
151
+ apiKey = (dependencies.readAgentBayKeyFile ?? readAgentBayApiKeyFile)(parsed.agentBayKeyFile);
152
+ }
153
+ else if (parsed.agentBayReadStdin) {
154
+ apiKey = await (dependencies.readAgentBayKeyStdin ?? readAgentBayApiKeyStdin)();
155
+ }
156
+ else {
157
+ if (!interactiveTerminal) {
158
+ throw new Error('Interactive input is unavailable. Re-run with --stdin or --key-file <path>.');
159
+ }
160
+ apiKey = await (dependencies.promptAgentBayKey ?? promptAgentBayApiKey)();
161
+ }
162
+ const result = configureAgentBay(apiKey);
163
+ console.error(`AgentBay API key saved to ${result.envFile}.`);
164
+ return 0;
165
+ }
166
+ catch (error) {
167
+ console.error(error instanceof Error ? error.message : String(error));
168
+ return 1;
169
+ }
170
+ }
101
171
  if (!interactiveTerminal && !parsed.yes && parsed.operation !== 'update') {
102
172
  console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
103
173
  return 2;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
+ export { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
3
+ export type { AgentBayConfigurationOptions, AgentBayConfigurationResult, AgentBaySecretInput, AgentBaySecretOutput, } from './agentBay.js';
2
4
  export type { DetectedGlobalPackage, GlobalPackageCommand, GlobalPackageManager, PackageManagerInstallation, PackageOperation, PackageDetectionOptions, } from './packageManager.js';
3
5
  export { runMearlLifecycle } from './lifecycle.js';
4
6
  export type { MearlLifecycleDependencies, MearlLifecycleOperation, MearlLifecycleOptions, } from './lifecycle.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
+ export { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
2
3
  export { runMearlLifecycle } from './lifecycle.js';
3
4
  export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
4
5
  export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
package/dist/lifecycle.js CHANGED
@@ -28,10 +28,10 @@ function siblingExecutable(anchor, name, existsSync) {
28
28
  }
29
29
  return null;
30
30
  }
31
- async function runLifecycleCommand(runner, command, args, env, label, writeLine) {
31
+ async function runLifecycleCommand(runner, command, args, env, label, writeLine, capture = false) {
32
32
  try {
33
33
  return await runner(command, args, {
34
- capture: false,
34
+ capture,
35
35
  env: prependCommandPath(command, env),
36
36
  });
37
37
  }
@@ -59,6 +59,31 @@ async function runHook(profile, operation, detected, runner, existsSync, env, wr
59
59
  executable = siblingExecutable(detected.executable, 'mearl-mcp-configure', existsSync);
60
60
  args = ['--skip-mcp', '--global-install'];
61
61
  }
62
+ else if (profile === 'cloud') {
63
+ executable = siblingExecutable(detected.executable, 'mearl-cloud-server', existsSync);
64
+ if (!executable) {
65
+ writeLine(`Hook: ${profile} ${operation} command was not found.`);
66
+ return false;
67
+ }
68
+ if (operation === 'uninstall') {
69
+ args = ['stop'];
70
+ }
71
+ else {
72
+ const status = await runLifecycleCommand(runner, executable, ['status'], env, 'Hook: cloud status', writeLine, true);
73
+ if (!status)
74
+ return false;
75
+ if (status.code !== 0) {
76
+ writeLine(`Hook: cloud status failed (exit ${status.code}).`);
77
+ return false;
78
+ }
79
+ const running = /\[CloudServer\]\s+Status:\s+running\b/.test(status.stdout);
80
+ if (operation === 'update' && !running) {
81
+ writeLine('Hook: cloud update skipped because cloud-server is not running.');
82
+ return true;
83
+ }
84
+ args = [running ? 'restart' : 'start'];
85
+ }
86
+ }
62
87
  else {
63
88
  return true;
64
89
  }
@@ -174,7 +199,8 @@ export async function runMearlLifecycle(options, dependencies = {}) {
174
199
  packagesOk = false;
175
200
  continue;
176
201
  }
177
- if (options.operation !== 'uninstall' && (profile === 'local' || profile === 'mcp')) {
202
+ if (options.operation !== 'uninstall' &&
203
+ (profile === 'local' || profile === 'cloud' || profile === 'mcp')) {
178
204
  const refreshed = detectPackage(config.anchorPackage, config.anchorBins, {
179
205
  env: prependCommandPath(command.command, env),
180
206
  }) ?? current;
package/dist/profiles.js CHANGED
@@ -13,7 +13,7 @@ export const MEARL_PROFILES = {
13
13
  label: 'cloud agent',
14
14
  anchorPackage: '@mearl/cloud-client',
15
15
  anchorBins: ['mearl-cloud'],
16
- packages: ['@mearl/cloud-client'],
16
+ packages: ['@mearl/cloud-server', '@mearl/cloud-client'],
17
17
  usesSkill: true,
18
18
  },
19
19
  mcp: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mearl/setup",
3
- "version": "2.4.0",
4
- "description": "One-shot installer, updater, and uninstaller for Mearl environments",
3
+ "version": "2.5.1",
4
+ "description": "One-shot environment setup and configuration for Mearl",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -26,6 +26,10 @@
26
26
  "types": "./dist/nativeHost.d.ts",
27
27
  "import": "./dist/nativeHost.js"
28
28
  },
29
+ "./agentbay": {
30
+ "types": "./dist/agentBay.d.ts",
31
+ "import": "./dist/agentBay.js"
32
+ },
29
33
  "./version": {
30
34
  "types": "./dist/version.d.ts",
31
35
  "import": "./dist/version.js"