@mearl/cloud-server 2.8.1 → 2.9.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.
Files changed (3) hide show
  1. package/dist/cli.d.ts +12 -1
  2. package/dist/cli.js +146 -79
  3. package/package.json +6 -5
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,13 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import type { CloudServerOptions } from './index.js';
3
+ import { type CloudServerCommand } from './qoder.js';
4
+ export interface ParsedCloudServerArgs {
5
+ command: CloudServerCommand;
6
+ daemonArgs: string[];
7
+ foreground: boolean;
8
+ help: boolean;
9
+ options: CloudServerOptions;
10
+ version: boolean;
11
+ }
12
+ export declare function parseCloudServerArgs(argv: string[]): ParsedCloudServerArgs;
13
+ export declare function main(argv?: string[]): Promise<number>;
package/dist/cli.js CHANGED
@@ -6,6 +6,11 @@ import {
6
6
  MODE_FILE
7
7
  } from "./chunks/chunk-DCTZIDJ7.js";
8
8
 
9
+ // src/cli.ts
10
+ import path from "node:path";
11
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
12
+ import { Command, CommanderError, InvalidArgumentError } from "commander";
13
+
9
14
  // src/daemon.ts
10
15
  import { fileURLToPath } from "node:url";
11
16
  import { createDaemon, readRecord, removeRecord } from "@mearl/daemon-core";
@@ -14,11 +19,11 @@ function formatConnectInfo(record) {
14
19
  const config = record;
15
20
  const lines = [];
16
21
  if (config.port) lines.push(` port: ${config.port}`);
17
- const command2 = config.publicUrl ? `npx @mearl/cloud-connector start "${config.publicUrl}"` : config.connectorCommand;
18
- if (command2) {
22
+ const command = config.publicUrl ? `npx @mearl/cloud-connector start "${config.publicUrl}"` : config.connectorCommand;
23
+ if (command) {
19
24
  lines.push("");
20
25
  lines.push(" Connect from your local machine:");
21
- lines.push(` ${command2}`);
26
+ lines.push(` ${command}`);
22
27
  }
23
28
  return lines.length ? lines.join("\n") : null;
24
29
  }
@@ -147,9 +152,9 @@ function formatQoderPairingOutput(info) {
147
152
 
148
153
  The connector resolves QODER_PERSONAL_ACCESS_TOKEN from the local environment, ~/.mearl/.env, or supported local credential storage. Never add the PAT to the pairing URL.`;
149
154
  }
150
- async function runQoderCloudServerCommand(command2, dependencies) {
155
+ async function runQoderCloudServerCommand(command, dependencies) {
151
156
  const writeLine = dependencies.writeLine ?? console.log;
152
- switch (command2) {
157
+ switch (command) {
153
158
  case "start":
154
159
  case "restart":
155
160
  if (dependencies.isServerRunning()) await dependencies.stopServer();
@@ -173,24 +178,18 @@ async function runQoderCloudServerCommand(command2, dependencies) {
173
178
  }
174
179
 
175
180
  // src/cli.ts
176
- var CLOUD_SERVER_VERSION = true ? "2.8.1" : "unknown";
177
- var argv = process.argv.slice(2);
178
- if (argv.includes("--version") || argv.includes("-v")) {
179
- console.log(CLOUD_SERVER_VERSION);
180
- process.exit(0);
181
- }
182
- if (argv.includes("--help") || argv.includes("-h")) {
183
- showHelp();
184
- process.exit(0);
185
- }
181
+ var CLOUD_SERVER_VERSION = true ? "2.9.0" : "unknown";
186
182
  var COMMANDS = ["start", "stop", "restart", "status", "logs"];
187
- var command = "start";
188
- var rest = argv;
189
- if (argv[0] && COMMANDS.includes(argv[0])) {
190
- command = argv[0];
191
- rest = argv.slice(1);
192
- }
193
- var foreground = rest.includes("--foreground") || rest.includes("-f");
183
+ var START_OPTION_NAMES = /* @__PURE__ */ new Set([
184
+ "foreground",
185
+ "port",
186
+ "agentPort",
187
+ "path",
188
+ "token",
189
+ "heartbeat",
190
+ "maxConnections",
191
+ "allowRemoteAgent"
192
+ ]);
194
193
  function showHelp() {
195
194
  console.log(`mearl-cloud-server v${CLOUD_SERVER_VERSION}
196
195
 
@@ -205,89 +204,147 @@ Commands:
205
204
  logs Print the tail of the server log
206
205
 
207
206
  Options:
208
- --foreground, -f Run in the foreground instead of daemonizing
207
+ --foreground, -f Run start in the foreground instead of daemonizing
209
208
  --port <port> Connector (network) port (default: 8080)
210
209
  --agent-port <port> Agent port, bound to 127.0.0.1 only (default: port + 1)
211
210
  --path <path> WebSocket path (default: /ws)
212
211
  --token <token> Authentication token (auto-generated if not specified)
213
212
  --heartbeat <seconds> Heartbeat timeout in seconds (default: 90)
214
213
  --max-connections <n> Max concurrent connections (default: 100)
215
- --allow-remote-agent Allow command-issuing agents to connect from the
216
- network (default: off; agents must be local)
214
+ --allow-remote-agent Allow command-issuing agents to connect from the network
217
215
  --version, -v Show version
218
216
  --help, -h Show this help message
219
-
220
- Environment Variables:
221
- MEARL_TOKEN Authentication token (overrides --token if set)
222
- CLOUD_SERVER_HOST Host name for WebSocket URL (default: localhost)
223
-
224
- Examples:
225
- # Start in the background (default) and print the connector command
226
- mearl-cloud-server
227
-
228
- # Check status / connect info, then stop
229
- mearl-cloud-server status
230
- mearl-cloud-server stop
231
-
232
- # Start with a custom port and token
233
- mearl-cloud-server start --port 9000 --token your-secret-token
234
-
235
- # Run in the foreground (e.g. under systemd / Docker / PM2)
236
- mearl-cloud-server --foreground
237
217
  `);
238
218
  }
239
- function parseOptions(args) {
240
- const options = {};
241
- for (let i = 0; i < args.length; i++) {
242
- const arg = args[i];
243
- if (arg === "--port" && i + 1 < args.length) {
244
- options.port = parseInt(args[++i], 10);
245
- } else if (arg === "--agent-port" && i + 1 < args.length) {
246
- options.agentPort = parseInt(args[++i], 10);
247
- } else if (arg === "--path" && i + 1 < args.length) {
248
- options.path = args[++i];
249
- } else if (arg === "--token" && i + 1 < args.length) {
250
- options.token = args[++i];
251
- } else if (arg === "--heartbeat" && i + 1 < args.length) {
252
- options.heartbeatTimeout = parseInt(args[++i], 10);
253
- } else if (arg === "--max-connections" && i + 1 < args.length) {
254
- options.maxConnections = parseInt(args[++i], 10);
255
- } else if (arg === "--allow-remote-agent") {
256
- options.allowRemoteAgent = true;
219
+ function integerOption(label, minimum, maximum = Number.MAX_SAFE_INTEGER) {
220
+ return (value) => {
221
+ const parsed = Number(value);
222
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
223
+ throw new InvalidArgumentError(`${label} must be an integer from ${minimum} to ${maximum}`);
224
+ }
225
+ return parsed;
226
+ };
227
+ }
228
+ function optionUsed(program, name) {
229
+ return program.getOptionValueSource(name) === "cli";
230
+ }
231
+ function assertAllowedOptions(program, command) {
232
+ const allowed = command === "start" || command === "restart" ? START_OPTION_NAMES : /* @__PURE__ */ new Set();
233
+ for (const option of program.options) {
234
+ const name = option.attributeName();
235
+ if (optionUsed(program, name) && !allowed.has(name) && name !== "help" && name !== "version") {
236
+ throw new Error(`${option.long ?? option.flags} is not available for ${command}`);
257
237
  }
258
238
  }
259
- if (!options.token) {
260
- const envToken = process.env.MEARL_TOKEN;
261
- if (envToken) {
262
- options.token = envToken;
239
+ if (command === "restart" && optionUsed(program, "foreground")) {
240
+ throw new Error("--foreground is only available for start");
241
+ }
242
+ }
243
+ function daemonArgs(program, options) {
244
+ const args = [];
245
+ const add = (name, flag) => {
246
+ const value = options[name];
247
+ if (optionUsed(program, String(name)) && value !== void 0) args.push(flag, String(value));
248
+ };
249
+ add("port", "--port");
250
+ add("agentPort", "--agent-port");
251
+ add("path", "--path");
252
+ add("token", "--token");
253
+ add("heartbeat", "--heartbeat");
254
+ add("maxConnections", "--max-connections");
255
+ if (options.allowRemoteAgent) args.push("--allow-remote-agent");
256
+ return args;
257
+ }
258
+ function parseCloudServerArgs(argv) {
259
+ const program = new Command().name("mearl-cloud-server").helpOption(false).allowExcessArguments(false).exitOverride().configureOutput({ writeErr: () => {
260
+ }, writeOut: () => {
261
+ } }).argument("[command]").option("-f, --foreground").option("--port <port>", "connector port", integerOption("port", 1, 65535)).option("--agent-port <port>", "agent port", integerOption("agent-port", 1, 65535)).option("--path <path>").option("--token <token>").option("--heartbeat <seconds>", "heartbeat timeout", integerOption("heartbeat", 1)).option("--max-connections <n>", "connection limit", integerOption("max-connections", 1)).option("--allow-remote-agent").option("-v, --version").option("-h, --help");
262
+ try {
263
+ program.parse(["node", "mearl-cloud-server", ...argv]);
264
+ } catch (error) {
265
+ if (error instanceof CommanderError) {
266
+ throw new Error(error.message.replace(/^error:\s*/i, ""), { cause: error });
263
267
  }
268
+ throw error;
269
+ }
270
+ const cliOptions = program.opts();
271
+ if (cliOptions.help || cliOptions.version) {
272
+ return {
273
+ command: "start",
274
+ daemonArgs: [],
275
+ foreground: false,
276
+ help: cliOptions.help === true,
277
+ options: {},
278
+ version: cliOptions.version === true
279
+ };
264
280
  }
265
- return options;
281
+ const commandArg = program.processedArgs[0];
282
+ if (commandArg && !COMMANDS.includes(commandArg)) {
283
+ throw new Error(`Unknown command: ${commandArg}`);
284
+ }
285
+ const command = commandArg ?? "start";
286
+ assertAllowedOptions(program, command);
287
+ if (cliOptions.token !== void 0 && cliOptions.token.length === 0) {
288
+ throw new Error("--token requires a non-empty value");
289
+ }
290
+ const options = {
291
+ ...cliOptions.port !== void 0 ? { port: cliOptions.port } : {},
292
+ ...cliOptions.agentPort !== void 0 ? { agentPort: cliOptions.agentPort } : {},
293
+ ...cliOptions.path !== void 0 ? { path: cliOptions.path } : {},
294
+ ...cliOptions.token !== void 0 ? { token: cliOptions.token } : process.env.MEARL_TOKEN ? { token: process.env.MEARL_TOKEN } : {},
295
+ ...cliOptions.heartbeat !== void 0 ? { heartbeatTimeout: cliOptions.heartbeat } : {},
296
+ ...cliOptions.maxConnections !== void 0 ? { maxConnections: cliOptions.maxConnections } : {},
297
+ ...cliOptions.allowRemoteAgent ? { allowRemoteAgent: true } : {}
298
+ };
299
+ return {
300
+ command,
301
+ daemonArgs: daemonArgs(program, cliOptions),
302
+ foreground: cliOptions.foreground === true,
303
+ help: false,
304
+ options,
305
+ version: false
306
+ };
266
307
  }
267
- async function main() {
308
+ async function main(argv = process.argv.slice(2)) {
309
+ let parsed;
310
+ try {
311
+ parsed = parseCloudServerArgs(argv);
312
+ } catch (error) {
313
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
314
+ showHelp();
315
+ return 2;
316
+ }
317
+ if (parsed.help) {
318
+ showHelp();
319
+ return 0;
320
+ }
321
+ if (parsed.version) {
322
+ console.log(CLOUD_SERVER_VERSION);
323
+ return 0;
324
+ }
268
325
  const mode = resolveCloudServerMode();
269
326
  if (mode === "qoder") {
270
- await runQoderCloudServerCommand(command, {
327
+ await runQoderCloudServerCommand(parsed.command, {
271
328
  isServerRunning: isDaemonRunning,
272
329
  stopServer: stopDaemon,
273
330
  printStatus: statusDaemon,
274
331
  printLogs
275
332
  });
276
- return;
333
+ return 0;
277
334
  }
278
- if (foreground) {
279
- await runForeground(parseOptions(rest));
280
- return;
335
+ if (parsed.foreground) {
336
+ await runForeground(parsed.options);
337
+ return 0;
281
338
  }
282
- switch (command) {
339
+ switch (parsed.command) {
283
340
  case "start":
284
- await startDaemon(rest);
341
+ await startDaemon(parsed.daemonArgs);
285
342
  break;
286
343
  case "stop":
287
344
  await stopDaemon();
288
345
  break;
289
346
  case "restart":
290
- await restartDaemon(rest);
347
+ await restartDaemon(parsed.daemonArgs);
291
348
  break;
292
349
  case "status":
293
350
  statusDaemon();
@@ -296,8 +353,18 @@ async function main() {
296
353
  printLogs();
297
354
  break;
298
355
  }
356
+ return 0;
357
+ }
358
+ var currentFile = fileURLToPath2(import.meta.url);
359
+ if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
360
+ main().then((code) => {
361
+ process.exitCode = code;
362
+ }).catch((error) => {
363
+ console.error("[CloudServer] Fatal error:", error);
364
+ process.exitCode = 1;
365
+ });
299
366
  }
300
- main().catch((error) => {
301
- console.error("[CloudServer] Fatal error:", error);
302
- process.exit(1);
303
- });
367
+ export {
368
+ main,
369
+ parseCloudServerArgs
370
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@mearl/cloud-server",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "Cloud WebSocket server for Mearl — bridges cloud agents to local connectors",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=18"
7
+ "node": ">=22.12.0"
8
8
  },
9
9
  "main": "dist/index.js",
10
10
  "types": "dist/index.d.ts",
@@ -33,10 +33,11 @@
33
33
  "registry": "https://registry.npmjs.org"
34
34
  },
35
35
  "dependencies": {
36
+ "commander": "^15.0.0",
36
37
  "ws": "^8.18.0",
37
- "@mearl/client": "2.8.1",
38
- "@mearl/cloud-types": "2.8.1",
39
- "@mearl/daemon-core": "2.8.1"
38
+ "@mearl/client": "2.9.0",
39
+ "@mearl/daemon-core": "2.9.0",
40
+ "@mearl/cloud-types": "2.9.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "^24.9.1",