@remcp/remcp 0.2.53 → 0.2.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.53",
3
+ "version": "0.2.55",
4
4
  "description": "ReMCP device client: pair a computer with ReMCP and run the outbound-only agent that hosts the local MCP runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,9 +29,92 @@ export function npmGlobalInstall(...specs) {
29
29
  run(npm.command, [...npm.args, 'install', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
30
30
  }
31
31
 
32
- export function npmGlobalInstallForNode(nodePath, ...specs) {
32
+ function pathEntryExists(file) {
33
+ try {
34
+ fs.lstatSync(file);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ function globalPackagePath(prefix, packageName) {
42
+ const name = String(packageName || '').trim();
43
+ const parts = name.split('/');
44
+ const validUnscoped = parts.length === 1 && parts[0] && !parts[0].startsWith('@') && parts[0] !== '.' && parts[0] !== '..';
45
+ const validScoped = parts.length === 2 && /^@[^/]+$/.test(parts[0]) && parts[1] && parts[1] !== '.' && parts[1] !== '..';
46
+ if (!validUnscoped && !validScoped) throw new Error(`Invalid npm package name for update: ${name || '(empty)'}`);
47
+ return path.join(prefix, 'lib', 'node_modules', ...parts);
48
+ }
49
+
50
+ function transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs }) {
51
+ prefix = String(prefix || '').trim();
52
+ if (!path.isAbsolute(prefix)) throw new Error(`npm global prefix must be absolute for a macOS update: ${prefix || '(empty)'}`);
53
+ const backupRoot = path.join(prefix, `.remcp-update-backup-${process.pid}-${Date.now()}`);
54
+ const moved = [];
55
+ const moveAside = (source, label) => {
56
+ if (!pathEntryExists(source)) return;
57
+ const backup = path.join(backupRoot, label);
58
+ fs.renameSync(source, backup);
59
+ moved.push({ source, backup });
60
+ };
61
+ const restore = originalError => {
62
+ let rollbackError = null;
63
+ for (const item of [...moved].reverse()) {
64
+ try {
65
+ if (pathEntryExists(item.source)) fs.rmSync(item.source, { recursive:true, force:true });
66
+ fs.mkdirSync(path.dirname(item.source), { recursive:true });
67
+ fs.renameSync(item.backup, item.source);
68
+ } catch (error) {
69
+ rollbackError ??= error;
70
+ }
71
+ }
72
+ try { fs.rmSync(backupRoot, { recursive:true, force:true }); } catch (error) { rollbackError ??= error; }
73
+ if (rollbackError) {
74
+ throw new Error(
75
+ `ReMCP update failed and the previous macOS installation could not be fully restored: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
76
+ { cause:originalError },
77
+ );
78
+ }
79
+ };
80
+
81
+ fs.mkdirSync(prefix, { recursive:true });
82
+ fs.mkdirSync(backupRoot, { recursive:false, mode:0o700 });
83
+ try {
84
+ const uniqueNames = [...new Set(packageNames.map(name => String(name || '').trim()).filter(Boolean))];
85
+ uniqueNames.forEach((name, index) => moveAside(globalPackagePath(prefix, name), `package-${index}`));
86
+ if (uniqueNames.includes(PACKAGE_NAME)) moveAside(path.join(prefix, 'bin', 'remcp'), 'bin-remcp');
87
+
88
+ run(resolved.command, [...resolved.args, 'install', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
89
+ } catch (error) {
90
+ restore(error);
91
+ throw error;
92
+ }
93
+
94
+ try {
95
+ fs.rmSync(backupRoot, { recursive:true, force:true });
96
+ } catch (error) {
97
+ console.error(`ReMCP update: could not remove macOS rollback backup ${backupRoot}: ${error instanceof Error ? error.message : String(error)}`);
98
+ }
99
+ }
100
+
101
+ export function npmGlobalUpdate(packageNames, ...specs) {
102
+ if (servicePlatform() !== 'darwin') {
103
+ npmGlobalInstall(...specs);
104
+ return;
105
+ }
106
+ const prefix = globalPrefix();
107
+ transactionalMacGlobalInstall({ resolved:npm, prefix, packageNames, specs });
108
+ }
109
+
110
+ export function npmGlobalUpdateForNode(nodePath, packageNames, ...specs) {
33
111
  const resolved = resolveNpm({ nodePath, home, platform:servicePlatform() });
34
- run(resolved.command, [...resolved.args, 'install', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
112
+ if (servicePlatform() !== 'darwin') {
113
+ run(resolved.command, [...resolved.args, 'install', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
114
+ return;
115
+ }
116
+ const prefix = output(resolved.command, [...resolved.args, 'prefix', '--global']);
117
+ transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs });
35
118
  }
36
119
 
37
120
  export function quoteSystemd(value) {
@@ -337,7 +420,7 @@ export function syncKnownInstallations(config, ...specs) {
337
420
  seen.add(resolvedNode);
338
421
  const required = Boolean(candidate?.service) || resolvedNode === serviceInfo.nodePath;
339
422
  try {
340
- npmGlobalInstallForNode(resolvedNode, ...specs);
423
+ npmGlobalUpdateForNode(resolvedNode, [PACKAGE_NAME, config.runtime.packageName], ...specs);
341
424
  results.push({ nodePath:resolvedNode, ok:true, required });
342
425
  } catch (error) {
343
426
  const message = error instanceof Error ? error.message : String(error);
@@ -414,11 +497,12 @@ export function installPersistentAgent(config) {
414
497
  // (a failed install, a cleaned LaunchAgents directory, a re-imaged user), the next update recreates
415
498
  // it instead of leaving a hand-over to a process nobody supervises.
416
499
  export function ensureServiceIfRecorded(config) {
417
- if (!persistentServiceExpected(config)) return false;
500
+ if (!persistentServiceExpected(config)) return null;
418
501
  try {
419
502
  const cliPath = canonicalServiceCliPath(config);
420
503
  const nodePath = canonicalServiceNodePath(config);
421
504
  const platform = servicePlatform();
505
+ let restartScheduled = null;
422
506
  if (platform === 'linux') {
423
507
  if (!fs.existsSync(linuxServiceFile)) {
424
508
  installLinuxService(cliPath, nodePath);
@@ -445,7 +529,8 @@ export function ensureServiceIfRecorded(config) {
445
529
  // boot out a loaded agent inline: this updater may itself be a descendant of that LaunchAgent.
446
530
  // installMacService either leaves an unchanged loaded job alone, bootstraps an unloaded job, or
447
531
  // hands a changed launcher to an independent transient launchd helper.
448
- installMacService(cliPath, { restart: false, nodePath });
532
+ const state = installMacService(cliPath, { restart: false, nodePath });
533
+ if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
449
534
  } else if (platform === 'win32') installWindowsService(cliPath);
450
535
  // Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
451
536
  // must not turn a stale artifact into a permanent "managed service" declaration.
@@ -453,10 +538,10 @@ export function ensureServiceIfRecorded(config) {
453
538
  if (config?.serviceInstalled !== true || config?.serviceCliPath !== cliScript || config?.serviceNodePath !== nodePath) {
454
539
  saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
455
540
  }
456
- return true;
541
+ return restartScheduled;
457
542
  } catch (error) {
458
543
  console.error(`Could not ensure the background service: ${error instanceof Error ? error.message : String(error)}`);
459
- return false;
544
+ return null;
460
545
  }
461
546
  }
462
547
 
@@ -11,7 +11,7 @@ import { PACKAGE_NAME, VERSION } from '../version.mjs';
11
11
  import { loadConfig, saveConfig } from './config.mjs';
12
12
  import { installedVersion } from './doctor.mjs';
13
13
  import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
14
- import { currentInstallationInfo, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalInstall, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
14
+ import { currentInstallationInfo, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
15
15
 
16
16
  const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
17
17
 
@@ -242,7 +242,7 @@ export async function updateCommand(flags) {
242
242
 
243
243
  const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
244
244
  console.log(`Updating ReMCP to ${targets.clientSpec} with ${targets.runtimeSpec}…`);
245
- npmGlobalInstall(targets.clientSpec, targets.runtimeSpec);
245
+ npmGlobalUpdate([PACKAGE_NAME, cfg.runtime.packageName], targets.clientSpec, targets.runtimeSpec);
246
246
 
247
247
  // Converge every ReMCP installation this account has actually used on this machine. The canonical
248
248
  // service installation is mandatory; secondary nvm/Hermes/Homebrew copies are best-effort and
@@ -258,10 +258,13 @@ export async function updateCommand(flags) {
258
258
  saveConfig({ ...cfg, runtime:{ ...cfg.runtime, packageSpec:targets.runtimeSpec } });
259
259
  cfg = loadConfig();
260
260
  }
261
- ensureServiceIfRecorded(cfg);
261
+ const restartAlreadyScheduled = ensureServiceIfRecorded(cfg);
262
262
  cfg = loadConfig();
263
263
  const after = { cli: installedVersion(PACKAGE_NAME), runtime: installedVersion(cfg.runtime.packageName) };
264
- const restarted = restartPersistentServiceIfInstalled(cfg);
264
+ // A changed macOS plist is reloaded by an independent launchd helper. Do not schedule a second
265
+ // helper against the same job: two one-second handoffs can race bootout/bootstrap with kickstart
266
+ // and leave the device offline even though npm installed the new release successfully.
267
+ const restarted = restartAlreadyScheduled || restartPersistentServiceIfInstalled(cfg);
265
268
  if (restarted) {
266
269
  console.log(`ReMCP updated and ${restarted} restarted (client ${before.cli} → ${after.cli ?? '?'}, runtime ${before.runtime ?? '?'} → ${after.runtime ?? '?'}).`);
267
270
  return;