@remcp/runtime 0.2.47 → 0.2.49

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.
@@ -0,0 +1,209 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { execFile, spawn } from 'node:child_process';
5
+ import { existsSync } from 'node:fs';
6
+ import { mkdtemp, rm } from 'node:fs/promises';
7
+ import { promisify } from 'node:util';
8
+ import { ToolError, fail, structured, text } from '../util.mjs';
9
+
10
+ const execFileAsync = promisify(execFile);
11
+ const MAX_BUFFER = 16 * 1024 * 1024;
12
+
13
+ export function clamp(value, fallback, min, max) {
14
+ const parsed = Number(value);
15
+ if (!Number.isFinite(parsed)) return fallback;
16
+ return Math.min(max, Math.max(min, Math.trunc(parsed)));
17
+ }
18
+
19
+ export function optionalString(value) {
20
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
21
+ }
22
+
23
+ export function requireEnum(value, field, allowed) {
24
+ const normalized = String(value ?? '');
25
+ if (!allowed.includes(normalized)) fail(`${field} must be one of: ${allowed.join(', ')}`);
26
+ return normalized;
27
+ }
28
+
29
+ export function jsonResult(value) {
30
+ return structured(value);
31
+ }
32
+
33
+ export async function runFile(file, args = [], options = {}) {
34
+ const timeout = clamp(options.timeout, 15_000, 100, 120_000);
35
+ try {
36
+ const result = await execFileAsync(file, args.map(String), {
37
+ cwd: options.cwd,
38
+ env: options.env || process.env,
39
+ timeout,
40
+ windowsHide: true,
41
+ maxBuffer: options.maxBuffer || MAX_BUFFER,
42
+ encoding: 'utf8',
43
+ });
44
+ return { ...result, code: 0 };
45
+ } catch (error) {
46
+ if (options.allowFailure) {
47
+ return { stdout: String(error?.stdout || ''), stderr: String(error?.stderr || ''), code: error?.code ?? 1, error };
48
+ }
49
+ const detail = String(error?.stderr || error?.message || error).trim();
50
+ throw new ToolError(`${options.label || file} failed: ${detail}`);
51
+ }
52
+ }
53
+
54
+ export async function runWithInput(file, args = [], input = '', options = {}) {
55
+ const timeout = clamp(options.timeout, 15_000, 100, 120_000);
56
+ const maxBuffer = clamp(options.maxBuffer, MAX_BUFFER, 1024, 64 * 1024 * 1024);
57
+ return new Promise((resolve, reject) => {
58
+ const child = spawn(file, args.map(String), {
59
+ cwd: options.cwd,
60
+ env: options.env || process.env,
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ windowsHide: true,
63
+ });
64
+ const out = [];
65
+ const err = [];
66
+ let buffered = 0;
67
+ let settled = false;
68
+ const finish = (fn, value) => {
69
+ if (settled) return;
70
+ settled = true;
71
+ clearTimeout(timer);
72
+ fn(value);
73
+ };
74
+ const timer = setTimeout(() => {
75
+ child.kill('SIGKILL');
76
+ finish(reject, new ToolError(`${options.label || file} timed out after ${timeout} ms`));
77
+ }, timeout);
78
+ const collect = target => chunk => {
79
+ if (settled) return;
80
+ buffered += chunk.length;
81
+ if (buffered > maxBuffer) {
82
+ child.kill('SIGKILL');
83
+ finish(reject, new ToolError(`${options.label || file} exceeded the ${maxBuffer} byte output limit`));
84
+ return;
85
+ }
86
+ target.push(chunk);
87
+ };
88
+ child.stdout.on('data', collect(out));
89
+ child.stderr.on('data', collect(err));
90
+ child.on('error', error => {
91
+ finish(reject, new ToolError(`${options.label || file} failed: ${error.message}`));
92
+ });
93
+ child.on('close', code => {
94
+ if (settled) return;
95
+ const stdout = Buffer.concat(out).toString('utf8');
96
+ const stderr = Buffer.concat(err).toString('utf8');
97
+ if (code === 0 || options.allowFailure) finish(resolve, { stdout, stderr, code });
98
+ else finish(reject, new ToolError(`${options.label || file} failed: ${stderr.trim() || `exit ${code}`}`));
99
+ });
100
+ child.stdin.end(String(input));
101
+ });
102
+ }
103
+
104
+ export async function runPowerShell(script, options = {}) {
105
+ const executable = process.env.SystemRoot
106
+ ? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
107
+ : 'powershell.exe';
108
+ return runFile(executable, ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
109
+ ...options,
110
+ label: options.label || 'PowerShell',
111
+ });
112
+ }
113
+
114
+ export async function runOsa(script, options = {}) {
115
+ const args = options.javascript ? ['-l', 'JavaScript', '-e', script] : ['-e', script];
116
+ return runFile('/usr/bin/osascript', args, { ...options, label: options.label || 'osascript' });
117
+ }
118
+
119
+ export function escapeAppleScript(value) {
120
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, '\\n');
121
+ }
122
+
123
+ export function escapePowerShellSingle(value) {
124
+ return String(value).replace(/'/g, "''");
125
+ }
126
+
127
+ export function commandExists(command) {
128
+ if (!command) return false;
129
+ if (path.isAbsolute(command)) return existsSync(command);
130
+ const dirs = String(process.env.PATH || '').split(path.delimiter).filter(Boolean);
131
+ const extensions = process.platform === 'win32'
132
+ ? String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
133
+ : [''];
134
+ return dirs.some(dir => extensions.some(ext => existsSync(path.join(dir, process.platform === 'win32' ? `${command}${ext}` : command))));
135
+ }
136
+
137
+ export function spawnDetached(file, args = [], options = {}) {
138
+ const child = spawn(file, args.map(String), {
139
+ cwd: options.cwd,
140
+ env: options.env || process.env,
141
+ detached: true,
142
+ stdio: 'ignore',
143
+ windowsHide: true,
144
+ });
145
+ child.unref();
146
+ return child.pid;
147
+ }
148
+
149
+ export async function spawnDetachedWithInput(file, args = [], input = '', options = {}) {
150
+ const timeout = clamp(options.timeout, 3000, 100, 30_000);
151
+ return new Promise((resolve, reject) => {
152
+ const child = spawn(file, args.map(String), {
153
+ cwd: options.cwd,
154
+ env: options.env || process.env,
155
+ detached: true,
156
+ stdio: ['pipe', 'ignore', 'ignore'],
157
+ windowsHide: true,
158
+ });
159
+ let settled = false;
160
+ const finish = (fn, value) => {
161
+ if (settled) return;
162
+ settled = true;
163
+ clearTimeout(timer);
164
+ fn(value);
165
+ };
166
+ const timer = setTimeout(() => {
167
+ try { child.kill('SIGKILL'); } catch {}
168
+ finish(reject, new ToolError(`${options.label || file} did not accept input within ${timeout} ms`));
169
+ }, timeout);
170
+ child.once('error', error => finish(reject, new ToolError(`${options.label || file} failed: ${error.message}`)));
171
+ child.once('spawn', () => {
172
+ child.unref();
173
+ child.stdin.once('error', error => finish(reject, new ToolError(`${options.label || file} failed while writing input: ${error.message}`)));
174
+ child.stdin.end(String(input), error => {
175
+ if (error) finish(reject, new ToolError(`${options.label || file} failed while writing input: ${error.message}`));
176
+ else finish(resolve, child.pid);
177
+ });
178
+ });
179
+ });
180
+ }
181
+
182
+ export async function tempDir(prefix = 'remcp-') {
183
+ return mkdtemp(path.join(os.tmpdir(), prefix));
184
+ }
185
+
186
+ export async function removeTemp(dir) {
187
+ await rm(dir, { recursive: true, force: true }).catch(() => {});
188
+ }
189
+
190
+ export function secretLike(key) {
191
+ return /(TOKEN|SECRET|PASSWORD|PASSWD|COOKIE|AUTH|BEARER|API[_-]?KEY|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|CREDENTIAL|SESSION|DATABASE[_-]?URL|REDIS[_-]?URL|CONNECTION[_-]?STRING|DSN)/i.test(String(key));
192
+ }
193
+
194
+ function credentialBearingUrl(value) {
195
+ try {
196
+ const url = new URL(String(value || ''));
197
+ return Boolean(url.username || url.password);
198
+ } catch { return false; }
199
+ }
200
+
201
+ export function safeEnvironment() {
202
+ return Object.fromEntries(Object.entries(process.env)
203
+ .sort(([a], [b]) => a.localeCompare(b))
204
+ .map(([key, value]) => [key, secretLike(key) || credentialBearingUrl(value) ? '***' : String(value ?? '')]));
205
+ }
206
+
207
+ export function unavailable(feature, hint = '') {
208
+ fail(`${feature} is not available on this computer${hint ? `: ${hint}` : ''}`);
209
+ }