@remcp/remcp 0.2.116 → 0.2.118
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/README.md +6 -4
- package/package.json +1 -1
- package/src/cli/env.mjs +1 -0
- package/src/cli/service.mjs +141 -21
package/README.md
CHANGED
|
@@ -9,10 +9,12 @@ remcp --version
|
|
|
9
9
|
remcp status
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Pairing commands are generated in the workspace at <https://remcp.site/app/connect>.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Pairing commands are generated in the workspace at <https://remcp.site/app/connect>. On Windows,
|
|
13
|
+
the browser shows a PowerShell bootstrap that needs no preinstalled system Node.js/npm/npx/winget;
|
|
14
|
+
it keeps the ReMCP runtime under `%LOCALAPPDATA%\ReMCP`. Generated one-time commands ultimately run
|
|
15
|
+
`remcp connect --server … --code … --install`, which stores a per-device credential, installs the
|
|
16
|
+
runtime, and registers a user service (systemd on Linux, LaunchAgent on macOS, Scheduled Task on
|
|
17
|
+
Windows).
|
|
16
18
|
|
|
17
19
|
## Commands
|
|
18
20
|
|
package/package.json
CHANGED
package/src/cli/env.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export const macServiceLabel = 'com.remcp.agent';
|
|
|
16
16
|
export const macServiceFile = path.join(home, 'Library', 'LaunchAgents', `${macServiceLabel}.plist`);
|
|
17
17
|
export const macLogFile = path.join(home, 'Library', 'Logs', 'remcp-agent.log');
|
|
18
18
|
export const windowsTaskName = 'ReMCP Agent';
|
|
19
|
+
export const windowsServiceLauncherFile = path.join(configDir, 'remcp-agent.cmd');
|
|
19
20
|
// How npm is invoked is resolved from the running node when possible: a background service has a
|
|
20
21
|
// minimal PATH, which is why auto-update used to find no npm on macOS. See src/npm.mjs.
|
|
21
22
|
export const npm = resolveNpm();
|
package/src/cli/service.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveNpm } from '../npm.mjs';
|
|
|
9
9
|
import { PACKAGE_NAME, VERSION } from '../version.mjs';
|
|
10
10
|
|
|
11
11
|
import { saveConfig } from './config.mjs';
|
|
12
|
-
import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsTaskName } from './env.mjs';
|
|
12
|
+
import { configDir, home, linuxServiceFile, linuxServiceLauncherFile, macLogFile, macServiceFile, macServiceLabel, npm, windowsServiceLauncherFile, windowsTaskName } from './env.mjs';
|
|
13
13
|
import { output, run } from './shell.mjs';
|
|
14
14
|
|
|
15
15
|
export function servicePlatform() {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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) {
|
|
@@ -350,8 +434,32 @@ export function installMacService(cliPath = globalCliPath(), { restart = true, n
|
|
|
350
434
|
return 'bootstrapped';
|
|
351
435
|
}
|
|
352
436
|
|
|
353
|
-
|
|
354
|
-
|
|
437
|
+
function windowsBatchValue(value) {
|
|
438
|
+
return String(value).replaceAll('%', '%%');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function writeWindowsServiceLauncher(cliPath = globalCliPath(), nodePath = process.execPath) {
|
|
442
|
+
const prefix = path.dirname(cliPath);
|
|
443
|
+
const nodeDir = path.dirname(nodePath);
|
|
444
|
+
const launcher = [
|
|
445
|
+
'@echo off',
|
|
446
|
+
'setlocal DisableDelayedExpansion',
|
|
447
|
+
`set "NPM_CONFIG_PREFIX=${windowsBatchValue(prefix)}"`,
|
|
448
|
+
`set "PATH=${windowsBatchValue(nodeDir)};%PATH%"`,
|
|
449
|
+
`call "${windowsBatchValue(cliPath)}" start --service`,
|
|
450
|
+
'exit /b %ERRORLEVEL%',
|
|
451
|
+
'',
|
|
452
|
+
].join('\r\n');
|
|
453
|
+
fs.mkdirSync(configDir, { recursive:true, mode:0o700 });
|
|
454
|
+
const temporary = `${windowsServiceLauncherFile}.${process.pid}.${Date.now()}.tmp`;
|
|
455
|
+
fs.writeFileSync(temporary, launcher, { mode:0o700 });
|
|
456
|
+
fs.renameSync(temporary, windowsServiceLauncherFile);
|
|
457
|
+
return windowsServiceLauncherFile;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function installWindowsService(cliPath = globalCliPath(), { nodePath = process.execPath } = {}) {
|
|
461
|
+
const launcherFile = writeWindowsServiceLauncher(cliPath, nodePath);
|
|
462
|
+
const command = `"${launcherFile}"`;
|
|
355
463
|
run('schtasks.exe', ['/Create', '/TN', windowsTaskName, '/TR', command, '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/F']);
|
|
356
464
|
run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
357
465
|
}
|
|
@@ -450,13 +558,24 @@ function packageVersionBesideCli(cliPath, packageName) {
|
|
|
450
558
|
return versionFromPackageJson(path.join(nodeModules, ...String(packageName).split('/'), 'package.json'));
|
|
451
559
|
}
|
|
452
560
|
|
|
561
|
+
function packageVersionForCli(cliPath, packageName) {
|
|
562
|
+
const beside = packageVersionBesideCli(cliPath, packageName);
|
|
563
|
+
if (beside || servicePlatform() !== 'win32' || !cliPath || !packageName) return beside;
|
|
564
|
+
// npm's Windows global executable is a .cmd shim in the prefix root, not a symlink into
|
|
565
|
+
// node_modules. realpath() therefore cannot reveal the package directory like it does on Unix.
|
|
566
|
+
// Resolve the package from the same prefix layout npm uses on Windows.
|
|
567
|
+
if (path.basename(cliPath).toLowerCase() !== 'remcp.cmd') return null;
|
|
568
|
+
const prefix = path.dirname(cliPath);
|
|
569
|
+
return versionFromPackageJson(path.join(prefix, 'node_modules', ...String(packageName).split('/'), 'package.json'));
|
|
570
|
+
}
|
|
571
|
+
|
|
453
572
|
export function installationVersionsAtCliPath(cliPath, runtimePackageName) {
|
|
454
573
|
if (!cliPath) return { cliVersion:null, runtimeVersion:null, cliPath:null };
|
|
455
574
|
let resolvedCliPath = cliPath;
|
|
456
575
|
try { if (fs.existsSync(cliPath)) resolvedCliPath = fs.realpathSync(cliPath); } catch {}
|
|
457
576
|
return {
|
|
458
|
-
cliVersion:
|
|
459
|
-
runtimeVersion:
|
|
577
|
+
cliVersion:packageVersionForCli(resolvedCliPath, PACKAGE_NAME),
|
|
578
|
+
runtimeVersion:packageVersionForCli(resolvedCliPath, runtimePackageName),
|
|
460
579
|
cliPath:resolvedCliPath,
|
|
461
580
|
};
|
|
462
581
|
}
|
|
@@ -468,8 +587,8 @@ export function serviceInstallationInfo(config) {
|
|
|
468
587
|
let resolvedCliPath = cliPath;
|
|
469
588
|
try { if (cliPath && fs.existsSync(cliPath)) resolvedCliPath = fs.realpathSync(cliPath); } catch {}
|
|
470
589
|
return {
|
|
471
|
-
cliVersion:
|
|
472
|
-
runtimeVersion:
|
|
590
|
+
cliVersion:packageVersionForCli(resolvedCliPath, PACKAGE_NAME),
|
|
591
|
+
runtimeVersion:packageVersionForCli(resolvedCliPath, config?.runtime?.packageName),
|
|
473
592
|
cliPath:resolvedCliPath || cliPath,
|
|
474
593
|
nodePath,
|
|
475
594
|
};
|
|
@@ -480,7 +599,7 @@ export function currentInstallationInfo(config) {
|
|
|
480
599
|
try { if (cliPath && fs.existsSync(cliPath)) cliPath = fs.realpathSync(cliPath); } catch {}
|
|
481
600
|
return {
|
|
482
601
|
cliVersion:VERSION,
|
|
483
|
-
runtimeVersion:config?.runtime?.packageName ?
|
|
602
|
+
runtimeVersion:config?.runtime?.packageName ? packageVersionForCli(cliPath, config.runtime.packageName) : null,
|
|
484
603
|
cliPath:cliPath || null,
|
|
485
604
|
nodePath:process.execPath,
|
|
486
605
|
};
|
|
@@ -599,7 +718,7 @@ export function installPersistentAgent(config) {
|
|
|
599
718
|
else if (platform === 'darwin') {
|
|
600
719
|
installMacService(cliPath, { nodePath });
|
|
601
720
|
ensureMacCliCommand(config, { cliPath:cliScript, nodePath });
|
|
602
|
-
} else installWindowsService(cliPath);
|
|
721
|
+
} else installWindowsService(cliPath, { nodePath });
|
|
603
722
|
configurePostInstallAccess();
|
|
604
723
|
saveConfig({ ...config, serviceInstalled: true, serviceCliPath: cliScript, serviceNodePath: nodePath });
|
|
605
724
|
console.log('ReMCP is installed as a background service. Future updates: remcp update');
|
|
@@ -663,7 +782,7 @@ export function ensureServiceIfRecorded(config) {
|
|
|
663
782
|
const state = installMacService(cliPath, { restart: false, nodePath });
|
|
664
783
|
ensureMacCliCommand(config, { cliPath, nodePath });
|
|
665
784
|
if (state === 'reload-scheduled') restartScheduled = macServiceLabel;
|
|
666
|
-
} else if (platform === 'win32') installWindowsService(cliPath);
|
|
785
|
+
} else if (platform === 'win32') installWindowsService(cliPath, { nodePath });
|
|
667
786
|
// Upgrade the legacy inferred state only after the supervisor repair succeeded. A failed repair
|
|
668
787
|
// must not turn a stale artifact into a permanent "managed service" declaration.
|
|
669
788
|
const cliScript = resolvedCliScript(cliPath);
|
|
@@ -722,5 +841,6 @@ export function uninstallPersistentService() {
|
|
|
722
841
|
} else if (platform === 'win32') {
|
|
723
842
|
spawnSync('schtasks.exe', ['/End', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
724
843
|
spawnSync('schtasks.exe', ['/Delete', '/TN', windowsTaskName, '/F'], { stdio: 'ignore' });
|
|
844
|
+
try { fs.unlinkSync(windowsServiceLauncherFile); } catch {}
|
|
725
845
|
}
|
|
726
846
|
}
|