@remcp/remcp 0.2.9 → 0.2.10
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/agent.mjs +102 -15
- package/src/cli.mjs +73 -14
- package/src/runtime.mjs +13 -2
package/package.json
CHANGED
package/src/agent.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import WebSocket from 'ws';
|
|
7
7
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
8
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
9
|
-
import { normalizeRuntime } from './runtime.mjs';
|
|
9
|
+
import { isRuntimeSpecFor, normalizeRuntime } from './runtime.mjs';
|
|
10
10
|
import { VERSION } from './version.mjs';
|
|
11
11
|
|
|
12
12
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
@@ -82,18 +82,74 @@ function isNewer(candidate, current) {
|
|
|
82
82
|
// What the agent should install, if anything. The client version alone is not enough: a machine
|
|
83
83
|
// that already runs the newest client but an older local runtime would otherwise never catch up,
|
|
84
84
|
// because its runtime is what executes the tools.
|
|
85
|
-
export function updateDecision({ advertised, cliVersion, runtimeVersion }) {
|
|
85
|
+
export function updateDecision({ advertised, cliVersion, runtimeVersion, runtimePackageName, runtimeDown = false }) {
|
|
86
86
|
const cliSpec = String(advertised?.cli || '');
|
|
87
|
-
const
|
|
87
|
+
const advertisedRuntime = String(advertised?.runtime || '');
|
|
88
|
+
// A spec that is not a plain version of the configured runtime package is ignored rather than
|
|
89
|
+
// installed: this is the only place a server-chosen string reaches npm.
|
|
90
|
+
const runtimeSpec = runtimePackageName && advertisedRuntime && !isRuntimeSpecFor(runtimePackageName, advertisedRuntime) ? '' : advertisedRuntime;
|
|
88
91
|
const installedRuntime = String(runtimeVersion || '');
|
|
89
92
|
const runtimeKnown = Boolean(installedRuntime) && !/^unknown$/i.test(installedRuntime);
|
|
90
93
|
if (isNewer(cliSpec, cliVersion)) return { needed: true, target: cliSpec, runtime: runtimeSpec, reason: 'client' };
|
|
91
|
-
if (
|
|
94
|
+
if (runtimeSpec && runtimeKnown && isNewer(runtimeSpec, installedRuntime)) {
|
|
92
95
|
return { needed: true, target: cliSpec || `@remcp/remcp@${cliVersion}`, runtime: runtimeSpec, reason: 'runtime' };
|
|
93
96
|
}
|
|
97
|
+
// A runtime that never reported a version cannot be compared, so a device whose runtime is down
|
|
98
|
+
// (or was never installed) would never repair itself. The cooldown in checkForUpdate keeps this
|
|
99
|
+
// from becoming an install loop.
|
|
100
|
+
if (runtimeSpec && !runtimeKnown && runtimeDown) {
|
|
101
|
+
return { needed: true, target: cliSpec || `@remcp/remcp@${cliVersion}`, runtime: runtimeSpec, reason: 'runtime-repair' };
|
|
102
|
+
}
|
|
94
103
|
return { needed: false, target: cliSpec, runtime: runtimeSpec, reason: 'current' };
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
// Applies a freshly installed version. Exiting is what a supervisor needs; without one the new CLI is
|
|
107
|
+
// started in this process' place. Either way the agent stops holding a stale runtime, which is what
|
|
108
|
+
// makes an update actually take effect on a machine that no service manager watches.
|
|
109
|
+
async function restartToApplyUpdate(cli, stopAgent, markStopping) {
|
|
110
|
+
try {
|
|
111
|
+
const installed = globalInstalledVersion();
|
|
112
|
+
if (installed && !isNewer(installed, VERSION)) {
|
|
113
|
+
console.log(`ReMCP ${VERSION} is already the installed version; nothing to restart.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`ReMCP ${installed || 'a newer version'} installed; restarting to apply it.`);
|
|
117
|
+
if (!supervisorRestart()) {
|
|
118
|
+
spawn(process.execPath, [cli, 'start'], { detached: true, stdio: 'ignore', env: { ...process.env } }).unref();
|
|
119
|
+
}
|
|
120
|
+
markStopping();
|
|
121
|
+
await stopAgent().catch(() => {});
|
|
122
|
+
setTimeout(() => process.exit(0), 100);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`Could not restart after the update: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The version of the globally installed client, read from the package the CLI resolves to.
|
|
129
|
+
function globalInstalledVersion() {
|
|
130
|
+
try {
|
|
131
|
+
const cli = globalCliEntry();
|
|
132
|
+
if (!cli) return null;
|
|
133
|
+
const base = path.dirname(cli);
|
|
134
|
+
const candidates = [
|
|
135
|
+
path.join(base, '..', 'lib', 'node_modules', '@remcp', 'remcp', 'package.json'),
|
|
136
|
+
path.join(base, '..', 'node_modules', '@remcp', 'remcp', 'package.json'),
|
|
137
|
+
];
|
|
138
|
+
for (const manifest of candidates) {
|
|
139
|
+
try { return JSON.parse(readFileSync(manifest, 'utf8')).version || null; } catch {}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function supervisorRestart() {
|
|
148
|
+
if (process.env.INVOCATION_ID || process.env.JOURNAL_STREAM) return 'systemd';
|
|
149
|
+
try { if (existsSync('/.dockerenv')) return 'docker'; } catch {}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
97
153
|
function globalCliEntry() {
|
|
98
154
|
const prefix = spawnSync(npmCommand, ['prefix', '--global'], { encoding: 'utf8' });
|
|
99
155
|
if (prefix.error || prefix.status !== 0) return null;
|
|
@@ -146,7 +202,15 @@ export async function runAgent(options) {
|
|
|
146
202
|
let runtimeDown = false;
|
|
147
203
|
const telemetryQueue = [];
|
|
148
204
|
let telemetryTimer = null;
|
|
149
|
-
|
|
205
|
+
// A device whose runtime is missing or unreadable must still run the agent: the agent is what
|
|
206
|
+
// installs and repairs the runtime, so failing here would remove the only path back.
|
|
207
|
+
let runtimeEntry = '';
|
|
208
|
+
try {
|
|
209
|
+
runtimeEntry = localRuntimeEntry(options.runtime);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
runtimeDown = true;
|
|
212
|
+
console.error(`${error instanceof Error ? error.message : String(error)} The agent keeps running and retries; remcp update reinstalls the runtime.`);
|
|
213
|
+
}
|
|
150
214
|
|
|
151
215
|
// --- local runtime supervision ------------------------------------------------------
|
|
152
216
|
// If the runtime dies (a bad shell, a broken pipe, an OOM) the agent used to stay
|
|
@@ -324,10 +388,12 @@ export async function runAgent(options) {
|
|
|
324
388
|
return;
|
|
325
389
|
}
|
|
326
390
|
if (code === 1012) {
|
|
327
|
-
// 1012 ('service restart') is what the relay sends when another agent process took over
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
console.error('Another ReMCP agent connected for this device; this process will stop. Run one agent per machine (
|
|
391
|
+
// 1012 ('service restart') is what the relay sends when another agent process took over this
|
|
392
|
+
// device. Staying alive would keep a second runtime and a reconnect loop, so this process
|
|
393
|
+
// stops and leaves the device to the agent that owns the connection.
|
|
394
|
+
console.error('Another ReMCP agent connected for this device; this process will stop. Run one agent per machine (a service manager or the remcp start command).');
|
|
395
|
+
stopping = true;
|
|
396
|
+
void stop().finally(() => setTimeout(() => process.exit(0), 100));
|
|
331
397
|
return;
|
|
332
398
|
}
|
|
333
399
|
reconnects += 1;
|
|
@@ -358,7 +424,7 @@ export async function runAgent(options) {
|
|
|
358
424
|
if (minimum && isNewer(minimum, VERSION)) {
|
|
359
425
|
console.error(`ReMCP ${VERSION} is older than the minimum supported agent ${minimum}; update with: remcp update`);
|
|
360
426
|
}
|
|
361
|
-
const decision = updateDecision({ advertised, cliVersion: VERSION, runtimeVersion });
|
|
427
|
+
const decision = updateDecision({ advertised, cliVersion: VERSION, runtimeVersion, runtimePackageName: options.runtime?.packageName, runtimeDown });
|
|
362
428
|
if (!decision.needed) return;
|
|
363
429
|
const target = decision.target;
|
|
364
430
|
queueEvent({ event: 'agent_update', at: Date.now(), reason: `${decision.reason}:${target}`.slice(0, 32), success: true });
|
|
@@ -375,13 +441,34 @@ export async function runAgent(options) {
|
|
|
375
441
|
lastAttemptAt = Date.now();
|
|
376
442
|
updateInFlight = true;
|
|
377
443
|
console.log(`Updating ReMCP to ${target}${decision.runtime ? ` with ${decision.runtime}` : ''} (${decision.reason})…`);
|
|
378
|
-
|
|
444
|
+
// The trust flag is only forwarded when this machine's owner trusted the server at pairing
|
|
445
|
+
// time; otherwise the update stops at the server's own version and asks the user.
|
|
446
|
+
const trustFlag = options.trustRuntime === true ? ['--trust-runtime'] : [];
|
|
447
|
+
const child = spawn(process.execPath, [cli, 'update', ...trustFlag, ...(decision.runtime ? ['--runtime', decision.runtime] : [])], {
|
|
379
448
|
detached: true,
|
|
380
|
-
|
|
449
|
+
// The updater's own output is the only record of why an install failed, so it is piped back
|
|
450
|
+
// into this agent's log instead of being discarded.
|
|
451
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
381
452
|
env: { ...process.env },
|
|
382
453
|
});
|
|
383
|
-
|
|
384
|
-
|
|
454
|
+
const forward = chunk => {
|
|
455
|
+
const line = String(chunk).trim();
|
|
456
|
+
if (line) console.error(`remcp update: ${line}`);
|
|
457
|
+
};
|
|
458
|
+
child.stdout?.on('data', forward);
|
|
459
|
+
child.stderr?.on('data', forward);
|
|
460
|
+
child.on('exit', code => {
|
|
461
|
+
updateInFlight = false;
|
|
462
|
+
if (code !== 0) {
|
|
463
|
+
console.error(`remcp update exited with ${code}; keeping ${VERSION} and retrying after the cooldown.`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
void restartToApplyUpdate(cli, stop, () => { stopping = true; });
|
|
467
|
+
});
|
|
468
|
+
child.on('error', error => {
|
|
469
|
+
updateInFlight = false;
|
|
470
|
+
console.error(`remcp update could not start: ${error.message}`);
|
|
471
|
+
});
|
|
385
472
|
child.unref();
|
|
386
473
|
} catch {
|
|
387
474
|
// Offline, DNS failure, older server without the endpoint: keep running as-is.
|
package/src/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import process from 'node:process';
|
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { runAgent } from './agent.mjs';
|
|
8
|
-
import { normalizeRuntime } from './runtime.mjs';
|
|
8
|
+
import { isRuntimeSpecFor, normalizeRuntime } from './runtime.mjs';
|
|
9
9
|
import { PACKAGE_NAME, VERSION } from './version.mjs';
|
|
10
10
|
|
|
11
11
|
const home = os.homedir();
|
|
@@ -200,12 +200,41 @@ function restartPersistentServiceIfInstalled() {
|
|
|
200
200
|
if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
|
|
201
201
|
run('systemctl', ['--user', 'daemon-reload']);
|
|
202
202
|
run('systemctl', ['--user', 'restart', 'remcp-agent.service']);
|
|
203
|
-
|
|
203
|
+
return 'remcp-agent.service';
|
|
204
|
+
}
|
|
205
|
+
if (platform === 'darwin' && fs.existsSync(macServiceFile)) {
|
|
204
206
|
run('launchctl', ['kickstart', '-k', `${macLaunchDomain()}/${macServiceLabel}`]);
|
|
205
|
-
|
|
207
|
+
return macServiceLabel;
|
|
208
|
+
}
|
|
209
|
+
if (platform === 'win32') {
|
|
206
210
|
const result = spawnSync('schtasks.exe', ['/Query', '/TN', windowsTaskName], { stdio: 'ignore' });
|
|
207
|
-
if (result.status === 0)
|
|
211
|
+
if (result.status === 0) {
|
|
212
|
+
run('schtasks.exe', ['/Run', '/TN', windowsTaskName]);
|
|
213
|
+
return windowsTaskName;
|
|
214
|
+
}
|
|
208
215
|
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The agent the user installed with `remcp install` is the one this CLI manages. A machine can also
|
|
220
|
+
// be supervised by its own systemd unit, by Docker, or by a terminal, and in those cases installing
|
|
221
|
+
// a new version is not enough: the running process keeps the old code until something restarts it.
|
|
222
|
+
// systemd marks every unit process with INVOCATION_ID and Docker leaves /.dockerenv, so those two
|
|
223
|
+
// cases can be handed over by exiting (the supervisor starts the new build); anything else gets an
|
|
224
|
+
// explicit instruction instead of a silent exit that would take the device offline.
|
|
225
|
+
function supervisorRestart() {
|
|
226
|
+
if (process.env.INVOCATION_ID || process.env.JOURNAL_STREAM) return 'systemd';
|
|
227
|
+
try { if (fs.existsSync('/.dockerenv')) return 'docker'; } catch {}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Reads the version a freshly installed global package reports, so an update that installed
|
|
232
|
+
// nothing (wrong prefix, npm cache, permissions) is reported instead of assumed successful.
|
|
233
|
+
function installedVersion(packageName) {
|
|
234
|
+
const prefix = spawnSync(npmCommand, ['prefix', '--global'], { encoding: 'utf8' });
|
|
235
|
+
if (prefix.error || prefix.status !== 0) return null;
|
|
236
|
+
const manifest = path.join(String(prefix.stdout || '').trim(), 'lib', 'node_modules', ...packageName.split('/'), 'package.json');
|
|
237
|
+
try { return JSON.parse(fs.readFileSync(manifest, 'utf8')).version || null; } catch { return null; }
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
function uninstallPersistentService() {
|
|
@@ -262,6 +291,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
262
291
|
deviceName: String(flags.name || os.hostname()),
|
|
263
292
|
runtime: normalizeRuntime(paired.runtime),
|
|
264
293
|
machineId: ensureMachineId(),
|
|
294
|
+
// Remember whether this machine's owner trusted the server to name a runtime version. The
|
|
295
|
+
// auto-updater must not widen that decision on its own later.
|
|
296
|
+
trustRuntime: Boolean(flags['trust-runtime']) || new URL(server).origin === officialOrigin,
|
|
265
297
|
};
|
|
266
298
|
saveConfig(config);
|
|
267
299
|
console.log(`Paired ${os.hostname()} with ${server}`);
|
|
@@ -275,6 +307,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
275
307
|
await runAgent({
|
|
276
308
|
...cfg,
|
|
277
309
|
autoUpdate: cfg.autoUpdate !== false,
|
|
310
|
+
trustRuntime: cfg.trustRuntime === true
|
|
311
|
+
|| process.env.REMCP_TRUST_RUNTIME === '1'
|
|
312
|
+
|| new URL(cfg.serverUrl).origin === officialOrigin,
|
|
278
313
|
telemetryEnabled: telemetry.enabled,
|
|
279
314
|
installReported: telemetry.installReported,
|
|
280
315
|
installSpec: `${PACKAGE_NAME}@${VERSION}`,
|
|
@@ -338,26 +373,50 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
338
373
|
// parses, and an explicit --trust-runtime before a custom server may change it.
|
|
339
374
|
let runtimeSpec = cfg.runtime.packageSpec;
|
|
340
375
|
if (requested) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
throw new Error(`--runtime must
|
|
376
|
+
// Only `<configured package>@<semver>` is installable: an alias, a git/URL/file spec, a tag or
|
|
377
|
+
// a range would run code the user never agreed to.
|
|
378
|
+
if (!isRuntimeSpecFor(cfg.runtime.packageName, requested)) {
|
|
379
|
+
throw new Error(`--runtime must be ${cfg.runtime.packageName}@<version>`);
|
|
345
380
|
}
|
|
346
|
-
|
|
347
|
-
|
|
381
|
+
const trusted = cfg.trustRuntime === true
|
|
382
|
+
|| Boolean(flags['trust-runtime'])
|
|
383
|
+
|| process.env.REMCP_TRUST_RUNTIME === '1'
|
|
384
|
+
// A configuration written before the field existed paired with the official server, which is
|
|
385
|
+
// trusted by definition; refusing it would silently stop every existing device updating.
|
|
386
|
+
|| new URL(cfg.serverUrl).origin === officialOrigin;
|
|
387
|
+
if (!trusted) {
|
|
388
|
+
throw new Error(`This machine was paired without trusting ${cfg.serverUrl} to choose a runtime version. Re-run with --trust-runtime if you trust that server.`);
|
|
348
389
|
}
|
|
349
|
-
runtimeSpec = normalizeRuntime({ kind: 'npm', packageName:
|
|
390
|
+
runtimeSpec = normalizeRuntime({ kind: 'npm', packageName: cfg.runtime.packageName, packageSpec: requested, entry: cfg.runtime.entry }).packageSpec;
|
|
350
391
|
}
|
|
351
392
|
if (flags.check) {
|
|
352
|
-
console.log(JSON.stringify({
|
|
393
|
+
console.log(JSON.stringify({
|
|
394
|
+
current: VERSION,
|
|
395
|
+
installedRuntime: installedVersion(cfg.runtime.packageName),
|
|
396
|
+
runtimeSpec: runtimeSpec,
|
|
397
|
+
available: `${PACKAGE_NAME}@latest`,
|
|
398
|
+
managedService: fs.existsSync(linuxServiceFile) || fs.existsSync(macServiceFile),
|
|
399
|
+
supervisor: supervisorRestart() ?? 'none',
|
|
400
|
+
}, null, 2));
|
|
353
401
|
return;
|
|
354
402
|
}
|
|
403
|
+
const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
|
|
355
404
|
console.log(`Updating ReMCP to the latest published version (${runtimeSpec})…`);
|
|
356
405
|
npmGlobalInstall(`${PACKAGE_NAME}@latest`, runtimeSpec);
|
|
357
406
|
// Only a validated spec is persisted, so a failed update cannot leave the install unable to start.
|
|
358
407
|
if (requested && runtimeSpec !== cfg.runtime.packageSpec) saveConfig({ ...cfg, runtime: { ...cfg.runtime, packageSpec: runtimeSpec } });
|
|
359
|
-
|
|
360
|
-
|
|
408
|
+
const after = { cli: installedVersion(PACKAGE_NAME), runtime: installedVersion(cfg.runtime.packageName) };
|
|
409
|
+
const restarted = restartPersistentServiceIfInstalled();
|
|
410
|
+
if (restarted) {
|
|
411
|
+
console.log(`ReMCP updated and ${restarted} restarted (client ${before.cli} → ${after.cli ?? '?'}, runtime ${before.runtime ?? '?'} → ${after.runtime ?? '?'}).`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// This process is the updater, not the agent: exiting here would restart nothing. The agent sees
|
|
415
|
+
// the exit status, verifies the installed version, and restarts itself.
|
|
416
|
+
console.log(`ReMCP updated (client ${before.cli} → ${after.cli ?? '?'}, runtime ${before.runtime ?? '?'} → ${after.runtime ?? '?'}). The running agent restarts itself to apply it.`);
|
|
417
|
+
if (after.cli === before.cli && after.runtime === before.runtime) {
|
|
418
|
+
console.log('Nothing changed: the installed versions already match the requested ones.');
|
|
419
|
+
}
|
|
361
420
|
return;
|
|
362
421
|
}
|
|
363
422
|
|
package/src/runtime.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
3
|
const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
4
|
-
|
|
4
|
+
// A runtime spec is a plain published version of one package. Anything else — an npm alias
|
|
5
|
+
// (`npm:@other/pkg`), a git/https/file spec, a tag, or a version range — is a way to make a device
|
|
6
|
+
// install and execute code the user never agreed to, so only `name@x.y.z[-pre][+build]` is accepted.
|
|
7
|
+
const SEMVER = '\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?';
|
|
5
8
|
|
|
6
9
|
export function normalizeRuntime(value) {
|
|
7
10
|
if (!value || value.kind !== 'npm') throw new Error('Pairing server did not provide a supported local runtime');
|
|
@@ -9,7 +12,15 @@ export function normalizeRuntime(value) {
|
|
|
9
12
|
const packageSpec = String(value.packageSpec || '');
|
|
10
13
|
const entry = String(value.entry || '');
|
|
11
14
|
if (!PACKAGE_NAME.test(packageName)) throw new Error('Pairing server returned an invalid runtime package name');
|
|
12
|
-
if (!
|
|
15
|
+
if (!isRuntimeSpecFor(packageName, packageSpec)) throw new Error(`Runtime package spec must be ${packageName}@<version>`);
|
|
13
16
|
if (!entry || path.isAbsolute(entry) || entry.split(/[\\/]+/).includes('..')) throw new Error('Pairing server returned an invalid runtime entry');
|
|
14
17
|
return { kind: 'npm', packageName, packageSpec, entry };
|
|
15
18
|
}
|
|
19
|
+
|
|
20
|
+
// True only for `<packageName>@<semver>` of exactly that package. Exported so the CLI and the agent
|
|
21
|
+
// can validate a server-advertised version without repeating the rule.
|
|
22
|
+
export function isRuntimeSpecFor(packageName, packageSpec) {
|
|
23
|
+
if (!PACKAGE_NAME.test(String(packageName || ''))) return false;
|
|
24
|
+
const escaped = String(packageName).replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
25
|
+
return new RegExp(`^${escaped}@(${SEMVER})$`).test(String(packageSpec || ''));
|
|
26
|
+
}
|