@remcp/remcp 0.2.53 → 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 +1 -1
- package/src/cli/service.mjs +92 -7
- package/src/cli/update.mjs +7 -4
package/package.json
CHANGED
package/src/cli/service.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -414,11 +497,12 @@ export function installPersistentAgent(config) {
|
|
|
414
497
|
// (a failed install, a cleaned LaunchAgents directory, a re-imaged user), the next update recreates
|
|
415
498
|
// it instead of leaving a hand-over to a process nobody supervises.
|
|
416
499
|
export function ensureServiceIfRecorded(config) {
|
|
417
|
-
if (!persistentServiceExpected(config)) return
|
|
500
|
+
if (!persistentServiceExpected(config)) return null;
|
|
418
501
|
try {
|
|
419
502
|
const cliPath = canonicalServiceCliPath(config);
|
|
420
503
|
const nodePath = canonicalServiceNodePath(config);
|
|
421
504
|
const platform = servicePlatform();
|
|
505
|
+
let restartScheduled = null;
|
|
422
506
|
if (platform === 'linux') {
|
|
423
507
|
if (!fs.existsSync(linuxServiceFile)) {
|
|
424
508
|
installLinuxService(cliPath, nodePath);
|
|
@@ -445,7 +529,8 @@ export function ensureServiceIfRecorded(config) {
|
|
|
445
529
|
// boot out a loaded agent inline: this updater may itself be a descendant of that LaunchAgent.
|
|
446
530
|
// installMacService either leaves an unchanged loaded job alone, bootstraps an unloaded job, or
|
|
447
531
|
// hands a changed launcher to an independent transient launchd helper.
|
|
448
|
-
installMacService(cliPath, { restart: false, nodePath });
|
|
532
|
+
const state = installMacService(cliPath, { restart: false, nodePath });
|
|
533
|
+
if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
|
|
449
534
|
} else if (platform === 'win32') installWindowsService(cliPath);
|
|
450
535
|
// Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
|
|
451
536
|
// must not turn a stale artifact into a permanent "managed service" declaration.
|
|
@@ -453,10 +538,10 @@ export function ensureServiceIfRecorded(config) {
|
|
|
453
538
|
if (config?.serviceInstalled !== true || config?.serviceCliPath !== cliScript || config?.serviceNodePath !== nodePath) {
|
|
454
539
|
saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
|
|
455
540
|
}
|
|
456
|
-
return
|
|
541
|
+
return restartScheduled;
|
|
457
542
|
} catch (error) {
|
|
458
543
|
console.error(`Could not ensure the background service: ${error instanceof Error ? error.message : String(error)}`);
|
|
459
|
-
return
|
|
544
|
+
return null;
|
|
460
545
|
}
|
|
461
546
|
}
|
|
462
547
|
|
package/src/cli/update.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
@@ -258,10 +258,13 @@ export async function updateCommand(flags) {
|
|
|
258
258
|
saveConfig({ ...cfg, runtime:{ ...cfg.runtime, packageSpec:targets.runtimeSpec } });
|
|
259
259
|
cfg = loadConfig();
|
|
260
260
|
}
|
|
261
|
-
ensureServiceIfRecorded(cfg);
|
|
261
|
+
const restartAlreadyScheduled = ensureServiceIfRecorded(cfg);
|
|
262
262
|
cfg = loadConfig();
|
|
263
263
|
const after = { cli: installedVersion(PACKAGE_NAME), runtime: installedVersion(cfg.runtime.packageName) };
|
|
264
|
-
|
|
264
|
+
// A changed macOS plist is reloaded by an independent launchd helper. Do not schedule a second
|
|
265
|
+
// helper against the same job: two one-second handoffs can race bootout/bootstrap with kickstart
|
|
266
|
+
// and leave the device offline even though npm installed the new release successfully.
|
|
267
|
+
const restarted = restartAlreadyScheduled || restartPersistentServiceIfInstalled(cfg);
|
|
265
268
|
if (restarted) {
|
|
266
269
|
console.log(`ReMCP updated and ${restarted} restarted (client ${before.cli} → ${after.cli ?? '?'}, runtime ${before.runtime ?? '?'} → ${after.runtime ?? '?'}).`);
|
|
267
270
|
return;
|