@remcp/remcp 0.2.117 → 0.2.118

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/package.json +1 -1
  2. package/src/cli/service.mjs +95 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.117",
3
+ "version": "0.2.118",
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",
@@ -51,13 +51,30 @@ function pathEntryExists(file) {
51
51
  }
52
52
  }
53
53
 
54
- function globalPackagePath(prefix, packageName) {
54
+ function globalPackagePath(prefix, packageName, platform = servicePlatform()) {
55
55
  const name = String(packageName || '').trim();
56
56
  const parts = name.split('/');
57
57
  const validUnscoped = parts.length === 1 && parts[0] && !parts[0].startsWith('@') && parts[0] !== '.' && parts[0] !== '..';
58
58
  const validScoped = parts.length === 2 && /^@[^/]+$/.test(parts[0]) && parts[1] && parts[1] !== '.' && parts[1] !== '..';
59
59
  if (!validUnscoped && !validScoped) throw new Error(`Invalid npm package name for update: ${name || '(empty)'}`);
60
- return path.join(prefix, 'lib', 'node_modules', ...parts);
60
+ const modulesRoot = platform === 'win32'
61
+ ? path.join(prefix, 'node_modules')
62
+ : path.join(prefix, 'lib', 'node_modules');
63
+ return path.join(modulesRoot, ...parts);
64
+ }
65
+
66
+ function managedWindowsRuntimePrefix() {
67
+ return path.resolve(
68
+ process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'),
69
+ 'ReMCP',
70
+ 'runtime',
71
+ );
72
+ }
73
+
74
+ function isManagedWindowsRuntimePrefix(prefix) {
75
+ const actual = path.resolve(String(prefix || ''));
76
+ const expected = managedWindowsRuntimePrefix();
77
+ return actual.toLowerCase() === expected.toLowerCase();
61
78
  }
62
79
 
63
80
  function transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline = false }) {
@@ -111,26 +128,93 @@ function transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs,
111
128
  }
112
129
  }
113
130
 
131
+ function transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline = false }) {
132
+ prefix = String(prefix || '').trim();
133
+ if (!path.isAbsolute(prefix)) throw new Error(`npm global prefix must be absolute for a Windows update: ${prefix || '(empty)'}`);
134
+ if (!isManagedWindowsRuntimePrefix(prefix)) {
135
+ run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline })]);
136
+ return;
137
+ }
138
+
139
+ const backupRoot = path.join(prefix, `.remcp-update-backup-${process.pid}-${Date.now()}`);
140
+ const moved = [];
141
+ const moveAside = (source, label) => {
142
+ if (!pathEntryExists(source)) return;
143
+ const backup = path.join(backupRoot, label);
144
+ fs.renameSync(source, backup);
145
+ moved.push({ source, backup });
146
+ };
147
+ const restore = originalError => {
148
+ let rollbackError = null;
149
+ for (const item of [...moved].reverse()) {
150
+ try {
151
+ if (pathEntryExists(item.source)) fs.rmSync(item.source, { recursive:true, force:true });
152
+ fs.mkdirSync(path.dirname(item.source), { recursive:true });
153
+ fs.renameSync(item.backup, item.source);
154
+ } catch (error) {
155
+ rollbackError ??= error;
156
+ }
157
+ }
158
+ try { fs.rmSync(backupRoot, { recursive:true, force:true }); } catch (error) { rollbackError ??= error; }
159
+ if (rollbackError) {
160
+ throw new Error(
161
+ `ReMCP update failed and the previous Windows installation could not be fully restored: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
162
+ { cause:originalError },
163
+ );
164
+ }
165
+ };
166
+
167
+ fs.mkdirSync(prefix, { recursive:true });
168
+ fs.mkdirSync(backupRoot, { recursive:false, mode:0o700 });
169
+ try {
170
+ const uniqueNames = [...new Set(packageNames.map(name => String(name || '').trim()).filter(Boolean))];
171
+ uniqueNames.forEach((name, index) => moveAside(globalPackagePath(prefix, name, 'win32'), `package-${index}`));
172
+ if (uniqueNames.includes(PACKAGE_NAME)) {
173
+ for (const shim of ['remcp', 'remcp.cmd', 'remcp.ps1']) moveAside(path.join(prefix, shim), `bin-${shim}`);
174
+ }
175
+ run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline })]);
176
+ } catch (error) {
177
+ restore(error);
178
+ throw error;
179
+ }
180
+
181
+ try {
182
+ fs.rmSync(backupRoot, { recursive:true, force:true });
183
+ } catch (error) {
184
+ console.error(`ReMCP update: could not remove Windows rollback backup ${backupRoot}: ${error instanceof Error ? error.message : String(error)}`);
185
+ }
186
+ }
187
+
114
188
  // Release updates use exact versions that may have been published only moments ago. npm can keep a
115
189
  // cached pre-publication packument long enough to answer ETARGET after the registry already serves the
116
190
  // version, so updates revalidate metadata while retaining npm's cache for package bytes.
117
191
  export function npmGlobalUpdate(packageNames, ...specs) {
118
- if (servicePlatform() !== 'darwin') {
119
- run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
192
+ const platform = servicePlatform();
193
+ const prefix = globalPrefix();
194
+ if (platform === 'darwin') {
195
+ transactionalMacGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
120
196
  return;
121
197
  }
122
- const prefix = globalPrefix();
123
- transactionalMacGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
198
+ if (platform === 'win32') {
199
+ transactionalWindowsGlobalInstall({ resolved:npm, prefix, packageNames, specs, preferOnline:true });
200
+ return;
201
+ }
202
+ run(npm.command, [...npm.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
124
203
  }
125
204
 
126
205
  export function npmGlobalUpdateForNode(nodePath, packageNames, ...specs) {
127
- const resolved = resolveNpm({ nodePath, home, platform:servicePlatform() });
128
- if (servicePlatform() !== 'darwin') {
129
- run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
206
+ const platform = servicePlatform();
207
+ const resolved = resolveNpm({ nodePath, home, platform });
208
+ const prefix = output(resolved.command, [...resolved.args, 'prefix', '--global']);
209
+ if (platform === 'darwin') {
210
+ transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
130
211
  return;
131
212
  }
132
- const prefix = output(resolved.command, [...resolved.args, 'prefix', '--global']);
133
- transactionalMacGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
213
+ if (platform === 'win32') {
214
+ transactionalWindowsGlobalInstall({ resolved, prefix, packageNames, specs, preferOnline:true });
215
+ return;
216
+ }
217
+ run(resolved.command, [...resolved.args, ...npmGlobalInstallArgs(specs, { preferOnline:true })]);
134
218
  }
135
219
 
136
220
  export function quoteSystemd(value) {