@remcp/remcp 0.2.54 → 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.54",
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);
@@ -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