@matelink/cli 2026.4.11 → 2026.4.12

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 (2) hide show
  1. package/bin/matecli.mjs +280 -18
  2. package/package.json +1 -1
package/bin/matecli.mjs CHANGED
@@ -440,6 +440,10 @@ function resolveBridgeLogsDir() {
440
440
  return path.join(resolveMatecliRuntimeDir(), "logs");
441
441
  }
442
442
 
443
+ function resolveBridgeLaunchScriptPath() {
444
+ return path.join(resolveMatecliRuntimeDir(), "bridge-launch.sh");
445
+ }
446
+
443
447
  function resolveMacLaunchAgentPath() {
444
448
  return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
445
449
  }
@@ -1247,6 +1251,127 @@ function bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }) {
1247
1251
  return [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl];
1248
1252
  }
1249
1253
 
1254
+ function escapePosixShellSingleQuoted(value) {
1255
+ return String(value ?? "").replaceAll("'", "'\"'\"'");
1256
+ }
1257
+
1258
+ function buildPosixShellExec(command, args) {
1259
+ return [command, ...args]
1260
+ .map((part) => `'${escapePosixShellSingleQuoted(part)}'`)
1261
+ .join(" ");
1262
+ }
1263
+
1264
+ function resolveNodeCandidates() {
1265
+ const homeDir = os.homedir();
1266
+ const candidates = new Set();
1267
+ const addCandidate = (candidate) => {
1268
+ const normalized = String(candidate ?? "").trim();
1269
+ if (!normalized) {
1270
+ return;
1271
+ }
1272
+ try {
1273
+ if (fs.existsSync(normalized)) {
1274
+ candidates.add(fs.realpathSync(normalized));
1275
+ }
1276
+ } catch {
1277
+ if (fs.existsSync(normalized)) {
1278
+ candidates.add(normalized);
1279
+ }
1280
+ }
1281
+ };
1282
+
1283
+ addCandidate(process.execPath);
1284
+ addCandidate(path.join(homeDir, ".nvm", "current", "bin", "node"));
1285
+ addCandidate(path.join(homeDir, ".volta", "bin", "node"));
1286
+ addCandidate("/opt/homebrew/bin/node");
1287
+ addCandidate("/usr/local/bin/node");
1288
+ addCandidate("/usr/bin/node");
1289
+
1290
+ const nvmVersionsDir = path.join(homeDir, ".nvm", "versions", "node");
1291
+ try {
1292
+ const nvmEntries = fs
1293
+ .readdirSync(nvmVersionsDir, { withFileTypes: true })
1294
+ .filter((entry) => entry.isDirectory())
1295
+ .map((entry) => entry.name)
1296
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
1297
+ for (const version of nvmEntries) {
1298
+ addCandidate(path.join(nvmVersionsDir, version, "bin", "node"));
1299
+ }
1300
+ } catch {}
1301
+
1302
+ const localNodeRoots = [
1303
+ path.join(homeDir, ".local"),
1304
+ path.join(homeDir, ".local", "node"),
1305
+ ];
1306
+ for (const root of localNodeRoots) {
1307
+ try {
1308
+ const entries = fs
1309
+ .readdirSync(root, { withFileTypes: true })
1310
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("node-"))
1311
+ .map((entry) => entry.name)
1312
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
1313
+ for (const name of entries) {
1314
+ addCandidate(path.join(root, name, "bin", "node"));
1315
+ }
1316
+ } catch {}
1317
+ }
1318
+
1319
+ return [...candidates];
1320
+ }
1321
+
1322
+ function resolveBridgeServiceNodePath() {
1323
+ for (const candidate of resolveNodeCandidates()) {
1324
+ if (candidate && fs.existsSync(candidate)) {
1325
+ return candidate;
1326
+ }
1327
+ }
1328
+ const whichNode = runCommand("which", ["node"]);
1329
+ if (whichNode.status === 0) {
1330
+ const resolved = String(whichNode.stdout ?? "").trim();
1331
+ if (resolved && fs.existsSync(resolved)) {
1332
+ return resolved;
1333
+ }
1334
+ }
1335
+ fail("node runtime is required for bridge service install");
1336
+ }
1337
+
1338
+ function writeBridgeLaunchScript(installConfig) {
1339
+ const scriptPath = resolveBridgeLaunchScriptPath();
1340
+ fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
1341
+ const defaultPath = [
1342
+ "/opt/homebrew/bin",
1343
+ "/usr/local/bin",
1344
+ "/usr/bin",
1345
+ "/bin",
1346
+ "/usr/sbin",
1347
+ "/sbin",
1348
+ path.join(os.homedir(), ".volta", "bin"),
1349
+ path.join(os.homedir(), ".nvm", "current", "bin"),
1350
+ ].join(":");
1351
+ const knownNodeCandidates = resolveNodeCandidates();
1352
+ const candidateChecks = knownNodeCandidates
1353
+ .map((candidate) => `if [ -x '${escapePosixShellSingleQuoted(candidate)}' ]; then NODE_BIN='${escapePosixShellSingleQuoted(candidate)}'; fi`)
1354
+ .join("\n");
1355
+ const fallbackLine = buildPosixShellExec("node", installConfig.args);
1356
+ const script = [
1357
+ "#!/bin/sh",
1358
+ "set -eu",
1359
+ "export PATH='" + escapePosixShellSingleQuoted(defaultPath) + ":'\"${PATH:-}\"",
1360
+ "NODE_BIN=''",
1361
+ candidateChecks,
1362
+ "if [ -z \"$NODE_BIN\" ] && command -v node >/dev/null 2>&1; then",
1363
+ " NODE_BIN=\"$(command -v node)\"",
1364
+ "fi",
1365
+ "if [ -n \"$NODE_BIN\" ]; then",
1366
+ ` exec "$NODE_BIN" ${installConfig.args.map((arg) => `'${escapePosixShellSingleQuoted(arg)}'`).join(" ")}`,
1367
+ "fi",
1368
+ `exec ${fallbackLine}`,
1369
+ "",
1370
+ ].join("\n");
1371
+ fs.writeFileSync(scriptPath, script, { encoding: "utf8", mode: 0o755 });
1372
+ return scriptPath;
1373
+ }
1374
+
1250
1375
  function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1251
1376
  if (!relayUrl) {
1252
1377
  fail("relay URL is required for bridge service");
@@ -1262,17 +1387,72 @@ function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1262
1387
  logDir,
1263
1388
  stdoutLog: path.join(logDir, "bridge.stdout.log"),
1264
1389
  stderrLog: path.join(logDir, "bridge.stderr.log"),
1265
- nodePath: process.execPath,
1390
+ nodePath: resolveBridgeServiceNodePath(),
1266
1391
  scriptPath: CLI_ENTRY,
1267
1392
  args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
1268
1393
  };
1269
1394
  }
1270
1395
 
1396
+ function escapeXmlText(value) {
1397
+ return String(value)
1398
+ .replaceAll("&", "&")
1399
+ .replaceAll("<", "&lt;")
1400
+ .replaceAll(">", "&gt;");
1401
+ }
1402
+
1403
+ function readMacLaunchdServiceStatus(uid, plistPath) {
1404
+ const refs = uid == null ? [] : [`gui/${uid}/${SERVICE_LABEL}`, `user/${uid}/${SERVICE_LABEL}`];
1405
+ for (const ref of refs) {
1406
+ const result = runCommand("launchctl", ["print", ref]);
1407
+ if (result.status !== 0) {
1408
+ continue;
1409
+ }
1410
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
1411
+ const stateMatch = output.match(/state = ([^\n]+)/u);
1412
+ const activeCountMatch = output.match(/active count = (\d+)/u);
1413
+ const pidMatch = output.match(/\bpid = (\d+)/u);
1414
+ const exitCodeMatch = output.match(/last exit code = (\d+)/u);
1415
+ const state = stateMatch?.[1]?.trim() ?? "";
1416
+ const activeCount = Number.parseInt(activeCountMatch?.[1] ?? "0", 10) || 0;
1417
+ const pid = Number.parseInt(pidMatch?.[1] ?? "0", 10) || 0;
1418
+ const lastExitCode = Number.parseInt(exitCodeMatch?.[1] ?? "0", 10) || 0;
1419
+ const running =
1420
+ state.toLowerCase() === "running" ||
1421
+ activeCount > 0 ||
1422
+ (pid > 0 && isPidAlive(pid));
1423
+ return {
1424
+ manager: "launchd",
1425
+ installed: true,
1426
+ running,
1427
+ target: plistPath,
1428
+ logDir: resolveBridgeLogsDir(),
1429
+ ref,
1430
+ state,
1431
+ activeCount,
1432
+ pid: pid || null,
1433
+ lastExitCode,
1434
+ };
1435
+ }
1436
+ return {
1437
+ manager: "launchd",
1438
+ installed: fs.existsSync(plistPath),
1439
+ running: false,
1440
+ target: plistPath,
1441
+ logDir: resolveBridgeLogsDir(),
1442
+ ref: SERVICE_LABEL,
1443
+ state: "not-loaded",
1444
+ activeCount: 0,
1445
+ pid: null,
1446
+ lastExitCode: 0,
1447
+ };
1448
+ }
1449
+
1271
1450
  function installBridgeServiceMac(installConfig) {
1272
1451
  const plistPath = resolveMacLaunchAgentPath();
1452
+ const launchScriptPath = writeBridgeLaunchScript(installConfig);
1273
1453
  fs.mkdirSync(path.dirname(plistPath), { recursive: true });
1274
- const argsXml = installConfig.args
1275
- .map((arg) => ` <string>${String(arg).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</string>`)
1454
+ const argsXml = [launchScriptPath]
1455
+ .map((arg) => ` <string>${escapeXmlText(arg)}</string>`)
1276
1456
  .join("\n");
1277
1457
  const plist = [
1278
1458
  "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
@@ -1315,6 +1495,13 @@ function installBridgeServiceMac(installConfig) {
1315
1495
  }
1316
1496
  selectedDomain = "launchd";
1317
1497
  }
1498
+ const status = readMacLaunchdServiceStatus(uid, plistPath);
1499
+ if (!status.running) {
1500
+ const details = status.lastExitCode
1501
+ ? `launchd last exit code: ${status.lastExitCode}`
1502
+ : "launchd service failed to stay running";
1503
+ throw new Error(details);
1504
+ }
1318
1505
  return {
1319
1506
  manager: "launchd",
1320
1507
  installed: true,
@@ -1409,6 +1596,7 @@ function installBridgeService({ relayUrl, gatewayBaseUrl }) {
1409
1596
  function uninstallBridgeService() {
1410
1597
  if (process.platform === "darwin") {
1411
1598
  const plistPath = resolveMacLaunchAgentPath();
1599
+ const launchScriptPath = resolveBridgeLaunchScriptPath();
1412
1600
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
1413
1601
  if (uid != null) {
1414
1602
  runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
@@ -1417,6 +1605,9 @@ function uninstallBridgeService() {
1417
1605
  try {
1418
1606
  fs.unlinkSync(plistPath);
1419
1607
  } catch {}
1608
+ try {
1609
+ fs.unlinkSync(launchScriptPath);
1610
+ } catch {}
1420
1611
  return { manager: "launchd", installed: false, running: false, target: plistPath, logDir: resolveBridgeLogsDir() };
1421
1612
  }
1422
1613
  if (process.platform === "linux") {
@@ -1435,23 +1626,56 @@ function uninstallBridgeService() {
1435
1626
  throw new Error(`unsupported platform for service uninstall: ${process.platform}`);
1436
1627
  }
1437
1628
 
1629
+ function stopBridgeService() {
1630
+ if (process.platform === "darwin") {
1631
+ const plistPath = resolveMacLaunchAgentPath();
1632
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1633
+ if (uid != null) {
1634
+ runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
1635
+ runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
1636
+ }
1637
+ return {
1638
+ manager: "launchd",
1639
+ installed: fs.existsSync(plistPath),
1640
+ running: false,
1641
+ target: plistPath,
1642
+ logDir: resolveBridgeLogsDir(),
1643
+ };
1644
+ }
1645
+ if (process.platform === "linux") {
1646
+ const unitPath = resolveLinuxUserUnitPath();
1647
+ runCommand("systemctl", ["--user", "stop", SERVICE_UNIT_NAME]);
1648
+ return {
1649
+ manager: "systemd-user",
1650
+ installed: fs.existsSync(unitPath),
1651
+ running: false,
1652
+ target: unitPath,
1653
+ logDir: resolveBridgeLogsDir(),
1654
+ };
1655
+ }
1656
+ if (process.platform === "win32") {
1657
+ runCommand("powershell.exe", ["-NoProfile", "-Command", `Stop-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -ErrorAction SilentlyContinue`]);
1658
+ return {
1659
+ manager: "task-scheduler",
1660
+ installed: true,
1661
+ running: false,
1662
+ target: SERVICE_TASK_NAME,
1663
+ logDir: resolveBridgeLogsDir(),
1664
+ };
1665
+ }
1666
+ return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir() };
1667
+ }
1668
+
1438
1669
  function restartBridgeService({ relayUrl, gatewayBaseUrl }) {
1439
- const installed = installBridgeService({ relayUrl, gatewayBaseUrl });
1440
- return installed;
1670
+ stopBridgeService();
1671
+ return installBridgeService({ relayUrl, gatewayBaseUrl });
1441
1672
  }
1442
1673
 
1443
1674
  function getBridgeServiceStatus() {
1444
1675
  if (process.platform === "darwin") {
1445
1676
  const plistPath = resolveMacLaunchAgentPath();
1446
1677
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
1447
- const refs = uid == null ? [] : [`gui/${uid}/${SERVICE_LABEL}`, `user/${uid}/${SERVICE_LABEL}`];
1448
- for (const ref of refs) {
1449
- const result = runCommand("launchctl", ["print", ref]);
1450
- if (result.status === 0) {
1451
- return { manager: "launchd", installed: true, running: true, target: plistPath, logDir: resolveBridgeLogsDir(), ref };
1452
- }
1453
- }
1454
- return { manager: "launchd", installed: fs.existsSync(plistPath), running: false, target: plistPath, logDir: resolveBridgeLogsDir(), ref: SERVICE_LABEL };
1678
+ return readMacLaunchdServiceStatus(uid, plistPath);
1455
1679
  }
1456
1680
  if (process.platform === "linux") {
1457
1681
  const enabled = runCommand("systemctl", ["--user", "is-enabled", SERVICE_UNIT_NAME]);
@@ -1541,6 +1765,29 @@ function stopBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1541
1765
  };
1542
1766
  }
1543
1767
 
1768
+ function clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl }) {
1769
+ const serviceStatus = getBridgeServiceStatus();
1770
+ let serviceStopped = null;
1771
+ if (serviceStatus.installed) {
1772
+ try {
1773
+ serviceStopped = stopBridgeService();
1774
+ } catch (error) {
1775
+ serviceStopped = {
1776
+ error: String(error instanceof Error ? error.message : error),
1777
+ };
1778
+ }
1779
+ }
1780
+ const stoppedProcesses =
1781
+ relayUrl && gatewayBaseUrl
1782
+ ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
1783
+ : { count: 0, pids: [] };
1784
+ return {
1785
+ serviceStatus,
1786
+ serviceStopped,
1787
+ stoppedProcesses,
1788
+ };
1789
+ }
1790
+
1544
1791
  function resolvePublishedGatewayBaseUrl({
1545
1792
  relayUrl,
1546
1793
  bindPublicBaseUrl,
@@ -3162,11 +3409,24 @@ async function runPair({
3162
3409
  publishedGatewayBaseUrl,
3163
3410
  gatewayRestarted: Boolean(restartResult?.ok),
3164
3411
  relayGatewayId: relayCredentials?.gatewayId ?? null,
3412
+ bridgeCleanup: null,
3165
3413
  };
3166
3414
 
3167
3415
  if (serveRelay && !json) {
3416
+ const bridgeCleanup = clearExistingBridgeRuntime({
3417
+ relayUrl,
3418
+ gatewayBaseUrl: localGatewayBaseUrl,
3419
+ });
3420
+ output.bridgeCleanup = {
3421
+ serviceInstalled: bridgeCleanup.serviceStatus?.installed === true,
3422
+ serviceRunning: bridgeCleanup.serviceStatus?.running === true,
3423
+ serviceStopError: bridgeCleanup.serviceStopped?.error ?? null,
3424
+ stoppedProcessCount: bridgeCleanup.stoppedProcesses?.count ?? 0,
3425
+ stoppedPids: bridgeCleanup.stoppedProcesses?.pids ?? [],
3426
+ };
3168
3427
  try {
3169
- const serviceResult = installBridgeService({
3428
+ const serviceStatus = getBridgeServiceStatus();
3429
+ const serviceResult = (serviceStatus.installed ? restartBridgeService : installBridgeService)({
3170
3430
  relayUrl,
3171
3431
  gatewayBaseUrl: localGatewayBaseUrl,
3172
3432
  });
@@ -3240,10 +3500,6 @@ async function runReset({ json, relay, noRelay, gateway }) {
3240
3500
  });
3241
3501
  }
3242
3502
 
3243
- const stoppedBridges =
3244
- relayUrl && gatewayBaseUrl
3245
- ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
3246
- : { count: 0, pids: [] };
3247
3503
  let serviceReset = null;
3248
3504
  if (relayUrl && gatewayBaseUrl) {
3249
3505
  try {
@@ -3252,6 +3508,10 @@ async function runReset({ json, relay, noRelay, gateway }) {
3252
3508
  serviceReset = { error: String(error instanceof Error ? error.message : error) };
3253
3509
  }
3254
3510
  }
3511
+ const stoppedBridges =
3512
+ relayUrl && gatewayBaseUrl
3513
+ ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
3514
+ : { count: 0, pids: [] };
3255
3515
  const nextState = resetLocalBindingState({ accountId });
3256
3516
 
3257
3517
  const output = {
@@ -3324,9 +3584,11 @@ async function runService({ json, relay, noRelay, gateway, serviceAction }) {
3324
3584
  result = getBridgeServiceStatus();
3325
3585
  break;
3326
3586
  case "install":
3587
+ clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
3327
3588
  result = installBridgeService({ relayUrl, gatewayBaseUrl });
3328
3589
  break;
3329
3590
  case "restart":
3591
+ clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
3330
3592
  result = restartBridgeService({ relayUrl, gatewayBaseUrl });
3331
3593
  break;
3332
3594
  case "uninstall":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.11",
3
+ "version": "2026.4.12",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",