@remcp/remcp 0.2.239 → 0.2.240

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.239",
3
+ "version": "0.2.240",
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,7 +29,7 @@ export function persistentServiceState(config) {
29
29
  const system = linuxSystemAgentOwned();
30
30
  const installed = system || fs.existsSync(linuxServiceFile);
31
31
  const active = installed && spawnSync('systemctl', [...(system ? [] : ['--user']), 'is-active', '--quiet', 'remcp-agent.service'], { stdio: 'ignore' }).status === 0;
32
- return { expected, installed, active, manager:system ? 'systemd-system' : 'systemd-user', name:'remcp-agent.service' };
32
+ return { expected, installed, active, manager:installed ? (system ? 'systemd-system' : 'systemd-user') : 'none', name:'remcp-agent.service' };
33
33
  }
34
34
  if (platform === 'darwin') {
35
35
  const installed = fs.existsSync(macServiceFile);
@@ -19,6 +19,76 @@ export function isManagedLinuxSystemUnit(text, launcher = linuxServiceLauncherFi
19
19
  return text.split(/\r?\n/).some(line => expected.has(line.trim()));
20
20
  }
21
21
 
22
+ // Discover BOTH supervisors before uninstalling either. A PID-1 migration may
23
+ // leave a disabled historical user unit behind; removing only the active scope
24
+ // makes status incorrectly report an installed service after uninstall.
25
+ export function discoverOwnedLinuxUnits({
26
+ systemFile = linuxSystemServiceFile,
27
+ userFile = linuxServiceFile,
28
+ launcher = linuxServiceLauncherFile,
29
+ includeSystem = process.getuid?.() === 0,
30
+ } = {}) {
31
+ const candidates = [
32
+ ...(includeSystem ? [{ scope:'system', file:systemFile }] : []),
33
+ { scope:'user', file:userFile },
34
+ ];
35
+ const found = [];
36
+ for (const unit of candidates) {
37
+ let stat;
38
+ try { stat = fs.lstatSync(unit.file); }
39
+ catch (error) {
40
+ if (error?.code === 'ENOENT') continue;
41
+ throw error;
42
+ }
43
+ if (!stat.isFile() || stat.isSymbolicLink() ||
44
+ !isManagedLinuxSystemUnit(fs.readFileSync(unit.file, 'utf8'), launcher)) {
45
+ throw new Error('Refusing to uninstall an unrecognized ReMCP systemd unit: ' + unit.file);
46
+ }
47
+ found.push(unit);
48
+ }
49
+ return found;
50
+ }
51
+
52
+ // Stop and remove all owned scopes, not just the newest one. Require the
53
+ // stop and daemon-reload to succeed; never claim success after a failed stop.
54
+ export function uninstallOwnedLinuxUnits({
55
+ systemFile = linuxSystemServiceFile,
56
+ userFile = linuxServiceFile,
57
+ launcher = linuxServiceLauncherFile,
58
+ includeSystem = process.getuid?.() === 0,
59
+ runner = spawnSync,
60
+ } = {}) {
61
+ const units = discoverOwnedLinuxUnits({ systemFile, userFile, launcher, includeSystem });
62
+ for (const { scope, file } of units) {
63
+ const option = scope === 'user' ? ['--user'] : [];
64
+ const stop = runner('systemctl', [...option, 'disable', '--now', 'remcp-agent.service'],
65
+ { encoding:'utf8', timeout:20000 });
66
+ if (stop.status !== 0) {
67
+ throw new Error('Failed to stop ReMCP ' + scope +
68
+ ' systemd service; refusing to report a successful uninstall');
69
+ }
70
+ const active = runner('systemctl', [...option, 'is-active', '--quiet', 'remcp-agent.service'],
71
+ { stdio:'ignore', timeout:10000 });
72
+ if (active.status === 0) {
73
+ throw new Error('ReMCP ' + scope + ' systemd service remains active after stop');
74
+ }
75
+ // Another actor must not replace an owned unit with a symlink or foreign
76
+ // content between discovery and removal.
77
+ const stat = fs.lstatSync(file);
78
+ if (!stat.isFile() || stat.isSymbolicLink() ||
79
+ !isManagedLinuxSystemUnit(fs.readFileSync(file, 'utf8'), launcher)) {
80
+ throw new Error('ReMCP systemd unit changed during uninstall: ' + file);
81
+ }
82
+ fs.unlinkSync(file);
83
+ const reload = runner('systemctl', [...option, 'daemon-reload'],
84
+ { encoding:'utf8', timeout:20000 });
85
+ if (reload.status !== 0) {
86
+ throw new Error('Failed to reload ' + scope + ' systemd after removing ReMCP service');
87
+ }
88
+ }
89
+ return units.map(unit => unit.scope);
90
+ }
91
+
22
92
  export function linuxSystemAgentOwned() {
23
93
  if (process.platform !== 'linux' || process.getuid?.() !== 0) return false;
24
94
  try {
@@ -18,7 +18,7 @@ import { PACKAGE_NAME, VERSION } from '../version.mjs';
18
18
  import { saveConfig } from './config.mjs';
19
19
  import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macServiceFile, macServiceLabel, windowsServiceLauncherFile, windowsStartupScriptFile, windowsTaskName } from './env.mjs';
20
20
  import { run } from './shell.mjs';
21
- import { linuxActiveServiceFile, linuxServiceScope, linuxSystemAgentOwned, linuxSystemServiceFile, ensureLinuxLinger, ensureLinuxCliCommand } from './service-linux.mjs';
21
+ import { linuxActiveServiceFile, linuxServiceScope, linuxSystemAgentOwned, linuxSystemServiceFile, ensureLinuxLinger, ensureLinuxCliCommand, uninstallOwnedLinuxUnits } from './service-linux.mjs';
22
22
  import { windowsServiceLauncherText, windowsTaskAbsenceCommand, windowsStartupScriptText, windowsStartupScriptInstalled } from './windows-supervisor.mjs';
23
23
  import { ensureWindowsSupervisorKey, windowsSupervisorCommand } from './windows-user-supervisor.mjs';
24
24
 
@@ -386,11 +386,7 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
386
386
  export function uninstallPersistentService() {
387
387
  const platform = servicePlatform();
388
388
  if (platform === 'linux') {
389
- const scope = linuxServiceScope();
390
- const option = scope === 'user' ? ['--user'] : [];
391
- spawnSync('systemctl', [...option, 'disable', '--now', 'remcp-agent.service'], { stdio:'inherit' });
392
- try { fs.unlinkSync(linuxActiveServiceFile()); } catch {}
393
- spawnSync('systemctl', [...option, 'daemon-reload'], { stdio:'inherit' });
389
+ uninstallOwnedLinuxUnits();
394
390
  } else if (platform === 'darwin') {
395
391
  const domain = macLaunchDomain();
396
392
  spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
package/src/cli.mjs CHANGED
@@ -360,6 +360,7 @@ export async function main(argv = process.argv.slice(2)) {
360
360
  },
361
361
  agent:{
362
362
  mode:service.installed ? 'service' : 'manual',
363
+ installed:service.installed,
363
364
  running:service.active,
364
365
  manager:service.manager,
365
366
  serviceCliVersion:serviceInstall.cliVersion,
@@ -419,6 +420,9 @@ export async function main(argv = process.argv.slice(2)) {
419
420
  if (flags.purge) {
420
421
  const specs = [PACKAGE_NAME, ...(cfg?.runtime?.packageName ? [cfg.runtime.packageName] : [])];
421
422
  run(npm.command, [...npm.args, 'uninstall', '--global', ...specs, '--no-audit', '--no-fund', '--loglevel=error']);
423
+ console.log('ReMCP background service removed and global npm packages uninstalled. Device pairing was retained.');
424
+ } else {
425
+ console.log('ReMCP background service removed. The CLI, runtime packages and device pairing were retained. Use remcp uninstall --purge to also uninstall global npm packages.');
422
426
  }
423
427
  return;
424
428
  }