@remcp/remcp 0.2.238 → 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 +1 -1
- package/src/cli/service-inventory.mjs +1 -1
- package/src/cli/service-linux.mjs +78 -5
- package/src/cli/service.mjs +5 -7
- package/src/cli.mjs +4 -0
package/package.json
CHANGED
|
@@ -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 {
|
|
@@ -40,13 +110,16 @@ export function linuxActiveServiceFile() {
|
|
|
40
110
|
// even if systemctl --user enable --now reported success.
|
|
41
111
|
export function ensureLinuxLinger() {
|
|
42
112
|
if (process.platform !== 'linux') return;
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
if (
|
|
46
|
-
|
|
113
|
+
// Test-only synthetic HOME has no logind account and must never mutate the host.
|
|
114
|
+
// Guard before os.userInfo(): an isolated CI UID may not have a passwd entry.
|
|
115
|
+
if (process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM === 'linux') return;
|
|
116
|
+
let account;
|
|
117
|
+
try { account = os.userInfo(); }
|
|
118
|
+
catch { throw new Error('ReMCP cannot resolve the current Linux account for persistent systemd supervision.'); }
|
|
119
|
+
if (path.resolve(home) !== path.resolve(account.homedir)) {
|
|
47
120
|
throw new Error('Refusing a persistent Linux service with HOME different from the account home directory.');
|
|
48
121
|
}
|
|
49
|
-
const username =
|
|
122
|
+
const username = account.username;
|
|
50
123
|
const query = () => spawnSync('loginctl', ['show-user', username, '-p', 'Linger', '--value'],
|
|
51
124
|
{ encoding:'utf8', timeout:10000 });
|
|
52
125
|
let state = query();
|
package/src/cli/service.mjs
CHANGED
|
@@ -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
|
|
|
@@ -66,7 +66,9 @@ export function installLinuxService(cliPath = globalCliPath(), nodePath = proces
|
|
|
66
66
|
const launcherFile = writeLinuxServiceLauncher(cliPath, nodePath);
|
|
67
67
|
// Fresh root installs are boot services owned by PID 1. Legacy per-user units are
|
|
68
68
|
// left under their existing supervisor, but must have linger enabled.
|
|
69
|
-
const
|
|
69
|
+
const syntheticLinuxFixture = process.env.NODE_ENV === 'test' && process.env.REMCP_TEST_PLATFORM === 'linux';
|
|
70
|
+
const privileged = !syntheticLinuxFixture && process.getuid?.() === 0
|
|
71
|
+
&& path.resolve(home) === path.resolve(os.userInfo().homedir);
|
|
70
72
|
if (privileged && fs.existsSync(linuxSystemServiceFile) && !linuxSystemAgentOwned()) {
|
|
71
73
|
throw new Error('Refusing to replace an unmanaged systemd remcp-agent.service');
|
|
72
74
|
}
|
|
@@ -384,11 +386,7 @@ export function restartPersistentServiceIfInstalled(config, { deferLinux = false
|
|
|
384
386
|
export function uninstallPersistentService() {
|
|
385
387
|
const platform = servicePlatform();
|
|
386
388
|
if (platform === 'linux') {
|
|
387
|
-
|
|
388
|
-
const option = scope === 'user' ? ['--user'] : [];
|
|
389
|
-
spawnSync('systemctl', [...option, 'disable', '--now', 'remcp-agent.service'], { stdio:'inherit' });
|
|
390
|
-
try { fs.unlinkSync(linuxActiveServiceFile()); } catch {}
|
|
391
|
-
spawnSync('systemctl', [...option, 'daemon-reload'], { stdio:'inherit' });
|
|
389
|
+
uninstallOwnedLinuxUnits();
|
|
392
390
|
} else if (platform === 'darwin') {
|
|
393
391
|
const domain = macLaunchDomain();
|
|
394
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
|
}
|