@remcp/remcp 0.2.117 → 0.2.119

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 +102 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.117",
3
+ "version": "0.2.119",
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) {
@@ -376,7 +460,10 @@ export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath
376
460
  export function installWindowsService(cliPath = globalCliPath(), { nodePath = process.execPath } = {}) {
377
461
  const launcherFile = writeWindowsServiceLauncher(cliPath, nodePath);
378
462
  const command = `"${launcherFile}"`;
379
- run('schtasks.exe', ['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/F']);
463
+ // Computer-use needs the signed-in interactive desktop. Create the task with /IT from the start
464
+ // instead of changing it afterwards: schtasks /Change can request the account password even when
465
+ // the task already belongs to the current user, which turns a one-command install into a prompt.
466
+ run('schtasks.exe', ['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/IT', '/F']);
380
467
  run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
381
468
  }
382
469
 
@@ -601,12 +688,13 @@ export function configureMacWriteAccess() {
601
688
  }
602
689
 
603
690
  export function configureWindowsWriteAccess() {
604
- try { run('schtasks.exe', ['/Change', '/TN', windowsTaskName, '/RL', 'HIGHEST', '/IT']); } catch {}
691
+ // installWindowsService already creates the task as HighestAvailable + InteractiveToken. Keep
692
+ // post-install access setup non-interactive so a fresh install never asks for the Windows password.
605
693
  try {
606
694
  const workspaceDir = path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'ReMCP');
607
695
  fs.mkdirSync(workspaceDir, { recursive: true });
608
696
  } catch {}
609
- console.log('ReMCP configured with elevated privileges for full write access.');
697
+ console.log('ReMCP configured with the highest available user privileges for interactive desktop access.');
610
698
  }
611
699
 
612
700
  export function configureLinuxWriteAccess() {