@remcp/remcp 0.2.54 → 0.2.56
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 +186 -5
- package/src/cli/update.mjs +5 -2
- package/src/cli.mjs +5 -1
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) {
|
|
@@ -75,6 +158,100 @@ function shellQuote(value) {
|
|
|
75
158
|
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
76
159
|
}
|
|
77
160
|
|
|
161
|
+
const remcpPathBlockStart = '# >>> ReMCP CLI PATH >>>';
|
|
162
|
+
const remcpPathBlockEnd = '# <<< ReMCP CLI PATH <<<';
|
|
163
|
+
const remcpCliShimMarker = '# ReMCP managed CLI shim';
|
|
164
|
+
|
|
165
|
+
function macCliShimPath() {
|
|
166
|
+
return path.join(home, '.local', 'bin', 'remcp');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function interactiveShellProfile() {
|
|
170
|
+
const shell = String(process.env.REMCP_TEST_SHELL || process.env.SHELL || os.userInfo().shell || '/bin/zsh');
|
|
171
|
+
const name = path.basename(shell);
|
|
172
|
+
if (name === 'zsh') return path.join(home, '.zshrc');
|
|
173
|
+
if (name === 'bash') return path.join(home, '.bash_profile');
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function upsertManagedPathBlock(file) {
|
|
178
|
+
if (!file) return false;
|
|
179
|
+
const block = [
|
|
180
|
+
remcpPathBlockStart,
|
|
181
|
+
'case ":$PATH:" in',
|
|
182
|
+
' *":$HOME/.local/bin:"*) ;;',
|
|
183
|
+
' *) export PATH="$HOME/.local/bin:$PATH" ;;',
|
|
184
|
+
'esac',
|
|
185
|
+
remcpPathBlockEnd,
|
|
186
|
+
].join('\n');
|
|
187
|
+
let current = '';
|
|
188
|
+
try { current = fs.readFileSync(file, 'utf8'); } catch {}
|
|
189
|
+
const start = current.indexOf(remcpPathBlockStart);
|
|
190
|
+
const end = start >= 0 ? current.indexOf(remcpPathBlockEnd, start + remcpPathBlockStart.length) : -1;
|
|
191
|
+
let next;
|
|
192
|
+
if (start >= 0 && end >= 0) {
|
|
193
|
+
next = current.slice(0, start) + block + current.slice(end + remcpPathBlockEnd.length);
|
|
194
|
+
} else {
|
|
195
|
+
const prefix = current && !current.endsWith('\n') ? current + '\n' : current;
|
|
196
|
+
next = prefix + (prefix ? '\n' : '') + block + '\n';
|
|
197
|
+
}
|
|
198
|
+
if (next === current) return false;
|
|
199
|
+
fs.writeFileSync(file, next, { mode:0o600 });
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function removeManagedPathBlock(file) {
|
|
204
|
+
if (!file) return;
|
|
205
|
+
let current = '';
|
|
206
|
+
try { current = fs.readFileSync(file, 'utf8'); } catch { return; }
|
|
207
|
+
const start = current.indexOf(remcpPathBlockStart);
|
|
208
|
+
const end = start >= 0 ? current.indexOf(remcpPathBlockEnd, start + remcpPathBlockStart.length) : -1;
|
|
209
|
+
if (start < 0 || end < 0) return;
|
|
210
|
+
let before = current.slice(0, start);
|
|
211
|
+
let after = current.slice(end + remcpPathBlockEnd.length);
|
|
212
|
+
if (before.endsWith('\n') && after.startsWith('\n')) after = after.slice(1);
|
|
213
|
+
const next = before + after;
|
|
214
|
+
fs.writeFileSync(file, next, { mode:0o600 });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function ensureMacCliCommand(config = {}, { cliPath = '', nodePath = '' } = {}) {
|
|
218
|
+
if (servicePlatform() !== 'darwin') return null;
|
|
219
|
+
const effectiveCli = cliPath || (persistentServiceExpected(config) ? canonicalServiceCliPath(config) : globalCliPath());
|
|
220
|
+
const effectiveNode = nodePath || (persistentServiceExpected(config) ? canonicalServiceNodePath(config) : process.execPath);
|
|
221
|
+
const cliScript = resolvedCliScript(effectiveCli);
|
|
222
|
+
const shim = macCliShimPath();
|
|
223
|
+
try {
|
|
224
|
+
const existing = fs.readFileSync(shim, 'utf8');
|
|
225
|
+
if (!existing.includes(remcpCliShimMarker)) {
|
|
226
|
+
console.error(`ReMCP did not replace ${shim} because it is not a ReMCP-managed command.`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error?.code && error.code !== 'ENOENT') {
|
|
231
|
+
console.error(`ReMCP could not inspect ${shim}: ${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const launcher = `#!/bin/sh\n${remcpCliShimMarker}\nexec ${shellQuote(effectiveNode)} ${shellQuote(cliScript)} "$@"\n`;
|
|
236
|
+
fs.mkdirSync(path.dirname(shim), { recursive:true, mode:0o755 });
|
|
237
|
+
const temporary = `${shim}.${process.pid}.${Date.now()}.tmp`;
|
|
238
|
+
fs.writeFileSync(temporary, launcher, { mode:0o755 });
|
|
239
|
+
fs.chmodSync(temporary, 0o755);
|
|
240
|
+
fs.renameSync(temporary, shim);
|
|
241
|
+
upsertManagedPathBlock(interactiveShellProfile());
|
|
242
|
+
return shim;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function removeMacCliCommand() {
|
|
246
|
+
if (servicePlatform() !== 'darwin') return;
|
|
247
|
+
const shim = macCliShimPath();
|
|
248
|
+
try {
|
|
249
|
+
const existing = fs.readFileSync(shim, 'utf8');
|
|
250
|
+
if (existing.includes(remcpCliShimMarker)) fs.unlinkSync(shim);
|
|
251
|
+
} catch {}
|
|
252
|
+
removeManagedPathBlock(interactiveShellProfile());
|
|
253
|
+
}
|
|
254
|
+
|
|
78
255
|
function macServicePlist(cliPath, nodePath = process.execPath) {
|
|
79
256
|
// launchd wants an absolute path; a symlinked prefix that npm has not materialised yet (or a path
|
|
80
257
|
// that is about to be replaced by the next install) must not abort the repair — a stale plist is
|
|
@@ -337,7 +514,7 @@ export function syncKnownInstallations(config, ...specs) {
|
|
|
337
514
|
seen.add(resolvedNode);
|
|
338
515
|
const required = Boolean(candidate?.service) || resolvedNode === serviceInfo.nodePath;
|
|
339
516
|
try {
|
|
340
|
-
|
|
517
|
+
npmGlobalUpdateForNode(resolvedNode, [PACKAGE_NAME, config.runtime.packageName], ...specs);
|
|
341
518
|
results.push({ nodePath:resolvedNode, ok:true, required });
|
|
342
519
|
} catch (error) {
|
|
343
520
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -403,8 +580,10 @@ export function installPersistentAgent(config) {
|
|
|
403
580
|
const cliScript = resolvedCliScript(cliPath);
|
|
404
581
|
const nodePath = process.execPath;
|
|
405
582
|
if (platform === 'linux') installLinuxService(cliPath, nodePath);
|
|
406
|
-
else if (platform === 'darwin')
|
|
407
|
-
|
|
583
|
+
else if (platform === 'darwin') {
|
|
584
|
+
installMacService(cliPath, { nodePath });
|
|
585
|
+
ensureMacCliCommand(config, { cliPath:cliScript, nodePath });
|
|
586
|
+
} else installWindowsService(cliPath);
|
|
408
587
|
configurePostInstallAccess();
|
|
409
588
|
saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
|
|
410
589
|
console.log('ReMCP is installed as a background service. Future updates: remcp update');
|
|
@@ -447,6 +626,7 @@ export function ensureServiceIfRecorded(config) {
|
|
|
447
626
|
// installMacService either leaves an unchanged loaded job alone, bootstraps an unloaded job, or
|
|
448
627
|
// hands a changed launcher to an independent transient launchd helper.
|
|
449
628
|
const state = installMacService(cliPath, { restart: false, nodePath });
|
|
629
|
+
ensureMacCliCommand(config, { cliPath, nodePath });
|
|
450
630
|
if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
|
|
451
631
|
} else if (platform === 'win32') installWindowsService(cliPath);
|
|
452
632
|
// Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
|
|
@@ -503,6 +683,7 @@ export function uninstallPersistentService() {
|
|
|
503
683
|
const domain = macLaunchDomain();
|
|
504
684
|
spawnSync('launchctl', ['bootout', domain, macServiceFile], { stdio: 'ignore' });
|
|
505
685
|
try { fs.unlinkSync(macServiceFile); } catch {}
|
|
686
|
+
removeMacCliCommand();
|
|
506
687
|
} else if (platform === 'win32') {
|
|
507
688
|
spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
508
689
|
spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
|
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, ensureMacCliCommand, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
|
|
15
15
|
|
|
16
16
|
const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
|
|
17
17
|
|
|
@@ -232,6 +232,9 @@ export async function updateCommand(flags) {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
if (alreadyCurrent) {
|
|
235
|
+
// `npx @remcp/remcp@latest update` is the recovery path when a Node-manager prefix was never
|
|
236
|
+
// exported into the user's shell. Even with no package delta, repair the stable macOS command.
|
|
237
|
+
ensureMacCliCommand(cfg);
|
|
235
238
|
if (targets.persistRuntime) {
|
|
236
239
|
saveConfig({ ...cfg, runtime:{ ...cfg.runtime, packageSpec:targets.runtimeSpec } });
|
|
237
240
|
}
|
|
@@ -242,7 +245,7 @@ export async function updateCommand(flags) {
|
|
|
242
245
|
|
|
243
246
|
const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
|
|
244
247
|
console.log(`Updating ReMCP to ${targets.clientSpec} with ${targets.runtimeSpec}…`);
|
|
245
|
-
|
|
248
|
+
npmGlobalUpdate([PACKAGE_NAME, cfg.runtime.packageName], targets.clientSpec, targets.runtimeSpec);
|
|
246
249
|
|
|
247
250
|
// Converge every ReMCP installation this account has actually used on this machine. The canonical
|
|
248
251
|
// service installation is mandatory; secondary nvm/Hermes/Homebrew copies are best-effort and
|
package/src/cli.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, te
|
|
|
11
11
|
import { assertRuntimeTrust, pairWithDeviceCode } from './cli/connect.mjs';
|
|
12
12
|
import { diagnoseLocalRuntime, installedVersion, runtimeAllowedRoots } from './cli/doctor.mjs';
|
|
13
13
|
import { configFile, npm, officialOrigin, runtimeConfigFile } from './cli/env.mjs';
|
|
14
|
-
import { currentInstallationInfo, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
|
|
14
|
+
import { currentInstallationInfo, ensureMacCliCommand, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
|
|
15
15
|
import { run } from './cli/shell.mjs';
|
|
16
16
|
import { updateCommand } from './cli/update.mjs';
|
|
17
17
|
function parse(argv) {
|
|
@@ -97,6 +97,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
97
97
|
|| Boolean(process.env.INVOCATION_ID)
|
|
98
98
|
|| String(process.env.XPC_SERVICE_NAME || '').includes('com.remcp.agent');
|
|
99
99
|
const cfg = rememberCurrentInstallation(loadConfig(), { service:supervised });
|
|
100
|
+
// A service can start from an absolute fnm/nvm Node path even when an interactive macOS shell
|
|
101
|
+
// cannot resolve the npm-global `remcp` binary. Refresh the stable user shim on every start so
|
|
102
|
+
// an auto-update repairs the next Terminal session without requiring a manual PATH edit.
|
|
103
|
+
ensureMacCliCommand(cfg);
|
|
100
104
|
const service = persistentServiceState(cfg);
|
|
101
105
|
if (!supervised && service.active) {
|
|
102
106
|
console.log(`ReMCP is already running as a background service (${service.name}). Use \`remcp status\` to inspect it or \`remcp doctor\` to diagnose it.`);
|