@remcp/runtime 0.2.153 → 0.2.155

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,415 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { constants, createReadStream } from 'node:fs';
6
+ import { access, chmod, chown, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { liveConfig, runtimeConfig } from '../config.mjs';
9
+ import { documentKind, readDocxText, readPdfText } from '../documents.mjs';
10
+ import { diffStats, unifiedDiff } from '../diff.mjs';
11
+ import { describeFilesystemFailure } from '../permissions.mjs';
12
+ import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
13
+ import { applyHunks, parseUnifiedDiff } from '../patch.mjs';
14
+ import { countEvent, recordEvent } from '../telemetry.mjs';
15
+ import { clampInteger, decodeText, displayPath, fail, fitsOutput, globToRegExp, image, isInsideRoot, looksBinary, multi, pageLines, resolveSafePath, splitLines, text, throwIfCancelled } from '../util.mjs';
16
+
17
+
18
+ import { READ_NOFOLLOW, ensureDirectoryPath, openDirectoryPath, openRegularFile, withExistingParent, withWritableParent, writeResolvedFile } from './files-secure-paths.mjs';
19
+ import { collectTree, confineAll, getPathInfo, readTextFile } from './files-read-traversal.mjs';
20
+ import { assertWritableSize } from './files-write-edit.mjs';
21
+ import { copyTreeContents } from './files-archive-safety.mjs';
22
+
23
+ export async function readFilesTool(args, extra = {}) {
24
+ // Glob-first bulk read: one call fills the model's context with every file that matters
25
+ // instead of one round trip per path.
26
+ const root = await resolveSafePath(args.path || '.');
27
+ const pattern = typeof args.pattern === 'string' && args.pattern.trim() ? args.pattern.trim() : '**/*';
28
+ const maxFiles = clampInteger(args.max_files, 100, 1, 500);
29
+ const maxLinesPerFile = clampInteger(args.max_lines_per_file, liveConfig('maxReadLines'), 1, 20000);
30
+ const includeIgnored = args.include_ignored === true;
31
+ const matcher = globToRegExp(pattern);
32
+ const rootInfo = await getPathInfo(root).catch(() => null);
33
+ // A file path is matched directly; a directory is walked without following links.
34
+ const walk = rootInfo?.isFile()
35
+ ? { files: [root], denied: [] }
36
+ : await collectTree(root, {
37
+ maxFiles: maxFiles + 1,
38
+ skip: includeIgnored ? ['.remcp-trash*'] : ['node_modules', '.git', '.remcp-trash*'],
39
+ signal: extra.signal,
40
+ });
41
+ const candidates = await confineAll(walk.files);
42
+ const matched = candidates.filter(target => {
43
+ const relative = path.relative(root, target) || path.basename(target);
44
+ return matcher.test(relative.split(path.sep).join('/')) || matcher.test(path.basename(target));
45
+ });
46
+ const files = matched.slice(0, maxFiles);
47
+ if (!files.length) return text(`No files matched ${pattern} under ${displayPath(root)}.`);
48
+ const sections = [];
49
+ let skipped = 0;
50
+ for (const file of files.slice(0, maxFiles)) {
51
+ throwIfCancelled(extra.signal);
52
+ try {
53
+ const { content, encoding } = await readTextFile(file);
54
+ const lines = splitLines(content);
55
+ const slice = lines.slice(0, maxLinesPerFile);
56
+ const suffix = lines.length > maxLinesPerFile ? `\n… ${lines.length - maxLinesPerFile} more lines (use read_file with offset)` : '';
57
+ sections.push(`===== ${displayPath(file)} (${lines.length} lines${encoding === 'utf8' ? '' : `, ${encoding}`}) =====\n${slice.join('\n')}${suffix}`);
58
+ } catch (error) {
59
+ skipped += 1;
60
+ sections.push(`===== ${displayPath(file)} =====\n(skipped: ${describeFilesystemFailure(error, { path: error?.path })})`);
61
+ }
62
+ }
63
+ const notes = [];
64
+ if (matched.length > files.length) notes.push(`showing the first ${files.length}`);
65
+ if (skipped) notes.push(`${skipped} unreadable`);
66
+ if (walk.denied.length) notes.push(`${walk.denied.length} unreadable director${walk.denied.length === 1 ? 'y' : 'ies'} skipped`);
67
+ const header = `${matched.length} file(s) matched ${pattern} under ${displayPath(root)}${notes.length ? ` (${notes.join(', ')})` : ''}`;
68
+ return text(`${header}\n\n${sections.join('\n\n')}`);
69
+ }
70
+
71
+ export async function writeFilesTool(args, extra = {}) {
72
+ // Bulk write for scaffolding: one call creates or replaces many files.
73
+ const files = Array.isArray(args.files) ? args.files : fail('files must be an array of { path, content } objects');
74
+ if (!files.length) fail('files must not be empty');
75
+ if (files.length > 200) fail('files accepts at most 200 entries per call');
76
+ const results = [];
77
+ let totalBytes = 0;
78
+ for (const entry of files) {
79
+ throwIfCancelled(extra.signal);
80
+ const target = typeof entry?.path === 'string' ? entry.path : null;
81
+ if (!target) { results.push('skipped: entry without a path'); continue; }
82
+ if (typeof entry.content !== 'string') { results.push(`skipped ${target}: content must be a string`); continue; }
83
+ try {
84
+ const absolute = await resolveSafePath(target);
85
+ const content = entry.content;
86
+ if (content.includes('\0')) throw new Error('content contains NUL bytes; use write_binary for binary data');
87
+ const bytes = assertWritableSize(content);
88
+ totalBytes += bytes;
89
+ if (totalBytes > runtimeConfig.maxWriteBytes * 4) fail(`This call would write ${totalBytes} bytes, above the ${runtimeConfig.maxWriteBytes * 4}-byte batch limit`);
90
+ await writeResolvedFile(absolute, content, { append: entry.mode === 'append' });
91
+ results.push(`${entry.mode === 'append' ? 'appended' : 'wrote'} ${displayPath(absolute)} (${bytes} bytes)`);
92
+ } catch (error) {
93
+ results.push(`failed ${target}: ${describeFilesystemFailure(error, { path: error?.path })}`);
94
+ }
95
+ }
96
+ countEvent('bytesWritten', totalBytes);
97
+ const failed = results.filter(line => line.startsWith('failed') || line.startsWith('skipped')).length;
98
+ return text(`${results.length - failed}/${results.length} file(s) written, ${totalBytes} bytes total\n${results.join('\n')}`, failed > 0);
99
+ }
100
+
101
+ export async function deletePathTool(args) {
102
+ const absolute = await resolveSafePath(args.path);
103
+ if (path.dirname(absolute) === absolute) fail(`Refusing to delete the filesystem root ${displayPath(absolute)}`);
104
+ const recursive = args.recursive !== false;
105
+ let info;
106
+ let entries = [];
107
+ await withExistingParent(absolute, async target => {
108
+ info = await lstat(target).catch(() => fail(`Path not found: ${displayPath(absolute)}`));
109
+ if (info.isDirectory() && !recursive) {
110
+ entries = await readdir(target).catch(() => []);
111
+ if (entries.length) fail(`Directory is not empty: ${displayPath(absolute)}. Pass recursive: true to delete it with its contents.`);
112
+ }
113
+ if (info.isDirectory()) entries = await readdir(target).catch(() => []);
114
+ await rm(target, { recursive:true, force:false });
115
+ });
116
+ return text(`Deleted ${info.isDirectory() ? 'directory' : 'file'} ${displayPath(absolute)}${info.isDirectory() ? ` and its ${entries.length} top-level entr${entries.length === 1 ? 'y' : 'ies'}` : ''}.`);
117
+ }
118
+
119
+ export async function deletePathsTool(args, extra = {}) {
120
+ const paths = Array.isArray(args.paths) ? args.paths : fail('paths must be an array of absolute paths');
121
+ if (!paths.length) fail('paths must not be empty');
122
+ if (paths.length > 500) fail('paths accepts at most 500 entries per call');
123
+ const recursive = args.recursive !== false;
124
+ const results = [];
125
+ let deleted = 0;
126
+ for (const entry of paths) {
127
+ throwIfCancelled(extra.signal);
128
+ try {
129
+ const absolute = await resolveSafePath(entry, 'paths[]');
130
+ if (path.dirname(absolute) === absolute) throw new Error('refusing to delete the filesystem root');
131
+ await withExistingParent(absolute, async target => {
132
+ const info = await lstat(target).catch(() => null);
133
+ if (!info) throw new Error('not found');
134
+ if (info.isDirectory() && !recursive) {
135
+ const children = await readdir(target).catch(() => []);
136
+ if (children.length) throw new Error('directory is not empty (pass recursive: true)');
137
+ }
138
+ await rm(target, { recursive:true, force:false });
139
+ });
140
+ deleted += 1;
141
+ results.push(`deleted ${displayPath(absolute)}`);
142
+ } catch (error) {
143
+ results.push(`failed ${entry}: ${describeFilesystemFailure(error, { path: error?.path })}`);
144
+ }
145
+ }
146
+ return text(`${deleted}/${paths.length} path(s) deleted\n${results.join('\n')}`, deleted !== paths.length);
147
+ }
148
+
149
+ // Recursive copy for files and whole directories, so a project or a backup can be
150
+ // duplicated in one call.
151
+ export async function copyPathsTool(args, extra = {}) {
152
+ const pairs = Array.isArray(args.paths) ? args.paths : fail('paths must be an array of { source, destination } objects');
153
+ if (!pairs.length) fail('paths must not be empty');
154
+ if (pairs.length > 200) fail('paths accepts at most 200 entries per call');
155
+ const overwrite = args.overwrite !== false;
156
+ const results = [];
157
+ let copied = 0;
158
+ for (const entry of pairs) {
159
+ throwIfCancelled(extra.signal);
160
+ try {
161
+ const source = await resolveSafePath(entry?.source, 'paths[].source');
162
+ const destination = await resolveSafePath(entry?.destination, 'paths[].destination');
163
+ if (source === destination) throw new Error('source and destination are the same path');
164
+ await withWritableParent(destination, async anchoredDestination => {
165
+ const existing = await lstat(anchoredDestination).catch(() => null);
166
+ if (existing && !overwrite) throw new Error('destination already exists (pass overwrite: true)');
167
+ await copyTreeContents(source, anchoredDestination, { entries:0, bytes:0, maxBytes:Number.MAX_SAFE_INTEGER, signal: extra.signal });
168
+ });
169
+ copied += 1;
170
+ results.push(`copied ${displayPath(source)} → ${displayPath(destination)}`);
171
+ } catch (error) {
172
+ results.push(`failed ${entry?.source ?? '?'}: ${describeFilesystemFailure(error, { path: error?.path })}`);
173
+ }
174
+ }
175
+ return text(`${copied}/${pairs.length} path(s) copied\n${results.join('\n')}`, copied !== pairs.length);
176
+ }
177
+
178
+ export async function movePathsTool(args, extra = {}) {
179
+ const pairs = Array.isArray(args.paths) ? args.paths : fail('paths must be an array of { source, destination } objects');
180
+ if (!pairs.length) fail('paths must not be empty');
181
+ if (pairs.length > 200) fail('paths accepts at most 200 entries per call');
182
+ const overwrite = args.overwrite !== false;
183
+ const results = [];
184
+ let moved = 0;
185
+ for (const entry of pairs) {
186
+ throwIfCancelled(extra.signal);
187
+ try {
188
+ const source = await resolveSafePath(entry?.source, 'paths[].source');
189
+ const destination = await resolveSafePath(entry?.destination, 'paths[].destination');
190
+ if (source === destination) throw new Error('source and destination are the same path');
191
+ await withExistingParent(source, async anchoredSource => {
192
+ const sourceInfo = await lstat(anchoredSource).catch(() => null);
193
+ if (!sourceInfo) throw new Error('source not found');
194
+ if (sourceInfo.isSymbolicLink()) throw new Error('source cannot be a symbolic link');
195
+ await withWritableParent(destination, async anchoredDestination => {
196
+ const existing = await lstat(anchoredDestination).catch(() => null);
197
+ if (existing && !overwrite) throw new Error('destination already exists (pass overwrite: true)');
198
+ try {
199
+ await rename(anchoredSource, anchoredDestination);
200
+ } catch (error) {
201
+ if (error?.code !== 'EXDEV') throw error;
202
+ await copyTreeContents(anchoredSource, anchoredDestination, { entries:0, bytes:0, maxBytes:Number.MAX_SAFE_INTEGER, signal: extra.signal });
203
+ await rm(anchoredSource, { recursive:true, force:true });
204
+ }
205
+ });
206
+ });
207
+ moved += 1;
208
+ results.push(`moved ${displayPath(source)} → ${displayPath(destination)}`);
209
+ } catch (error) {
210
+ results.push(`failed ${entry?.source ?? '?'}: ${describeFilesystemFailure(error, { path: error?.path })}`);
211
+ }
212
+ }
213
+ return text(`${moved}/${pairs.length} path(s) moved\n${results.join('\n')}`, moved !== pairs.length);
214
+ }
215
+
216
+ // Applying a unified diff is the fastest path from "the model knows the change" to "the
217
+ // change is on disk": no exact-block matching, no re-sending whole files.
218
+ export async function applyPatchTool(args) {
219
+ const patch = typeof args.patch === 'string' && args.patch.trim() ? args.patch : fail('patch must be a unified diff');
220
+ const dryRun = args.dry_run === true;
221
+ const forcePath = typeof args.path === 'string' && args.path.trim() ? args.path : null;
222
+ const files = parseUnifiedDiff(patch);
223
+ if (!files.length) fail('patch does not contain any @@ hunks');
224
+ const results = [];
225
+ let changed = 0;
226
+ for (const file of files) {
227
+ const target = forcePath || (file.newPath && file.newPath !== '/dev/null' ? file.newPath : file.oldPath);
228
+ if (!target || target === '/dev/null') { results.push('failed: a hunk has no target path; pass path explicitly'); continue; }
229
+ try {
230
+ const absolute = await resolveSafePath(target);
231
+ let original = '';
232
+ try { original = (await readTextFile(absolute)).content; } catch (error) {
233
+ if (file.oldPath === '/dev/null' || /not found/i.test(error?.message || '')) original = '';
234
+ else throw error;
235
+ }
236
+ const { updated, applied, failed } = applyHunks(original, file.hunks);
237
+ if (failed.length && !applied.length) { results.push(`failed ${displayPath(absolute)}: none of the ${file.hunks.length} hunk(s) matched`); continue; }
238
+ const stats = diffStats(original, updated);
239
+ if (!dryRun) {
240
+ assertWritableSize(updated);
241
+ await writeResolvedFile(absolute, updated);
242
+ }
243
+ changed += 1;
244
+ const fuzzy = applied.filter(entry => entry.fuzz > 0).length;
245
+ results.push(`${dryRun ? 'would patch' : 'patched'} ${displayPath(absolute)} · ${applied.length}/${file.hunks.length} hunk(s), +${stats.added}/-${stats.removed} lines${fuzzy ? `, ${fuzzy} with fuzz` : ''}${failed.length ? `, ${failed.length} hunk(s) did not match` : ''}`);
246
+ if (dryRun) results.push(unifiedDiff(original, updated, { oldLabel: displayPath(absolute), newLabel: 'after' }));
247
+ } catch (error) {
248
+ results.push(`failed ${target}: ${describeFilesystemFailure(error, { path: error?.path })}`);
249
+ }
250
+ }
251
+ const failedCount = results.filter(line => line.startsWith('failed')).length;
252
+ return text([`${changed}/${files.length} file(s) ${dryRun ? 'would be patched' : 'patched'}`, ...results].join('\n'), failedCount > 0);
253
+ }
254
+
255
+ export async function setLinuxPermissions(target, mode, uid, gid, recursive, displayTarget, failures) {
256
+ let directory = null;
257
+ let file = null;
258
+ try {
259
+ const info = await lstat(target);
260
+ if (info.isSymbolicLink()) {
261
+ failures.push(`${displayTarget}: symbolic links are not changed`);
262
+ return 0;
263
+ }
264
+ if (info.isDirectory()) {
265
+ directory = await openDirectoryPath(target);
266
+ let changed = 0;
267
+ if (recursive) {
268
+ for (const entry of await readdir(directory.anchor, { withFileTypes:true })) {
269
+ if (entry.isSymbolicLink()) continue;
270
+ changed += await setLinuxPermissions(path.join(directory.anchor, entry.name), mode, uid, gid, true, path.join(displayTarget, entry.name), failures);
271
+ }
272
+ }
273
+ await directory.handle.chmod(mode);
274
+ if (uid !== null || gid !== null) await directory.handle.chown(uid ?? -1, gid ?? -1);
275
+ return changed + 1;
276
+ }
277
+ if (!info.isFile()) {
278
+ failures.push(`${displayTarget}: special files are not changed`);
279
+ return 0;
280
+ }
281
+ file = await openRegularFile(target, READ_NOFOLLOW);
282
+ await file.handle.chmod(mode);
283
+ if (uid !== null || gid !== null) await file.handle.chown(uid ?? -1, gid ?? -1);
284
+ return 1;
285
+ } catch (error) {
286
+ failures.push(`${displayTarget}: ${describeFilesystemFailure(error, { path:error?.path })}`);
287
+ return 0;
288
+ } finally {
289
+ if (file) await file.close().catch(() => {});
290
+ if (directory) await directory.close().catch(() => {});
291
+ }
292
+ }
293
+
294
+ export async function setPermissionsTool(args) {
295
+ const absolute = await resolveSafePath(args.path);
296
+ const raw = typeof args.mode === 'string' ? args.mode.trim() : String(args.mode ?? '');
297
+ if (!/^[0-7]{3,4}$/.test(raw)) fail('mode must be an octal string such as "755" or "0644"');
298
+ const mode = Number.parseInt(raw, 8);
299
+ const recursive = args.recursive === true;
300
+ const uid = Number.isInteger(Number(args.uid)) ? Number(args.uid) : null;
301
+ const gid = Number.isInteger(Number(args.gid)) ? Number(args.gid) : null;
302
+ const failures = [];
303
+ let changed = 0;
304
+ if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
305
+ changed = await setLinuxPermissions(absolute, mode, uid, gid, recursive, displayPath(absolute), failures);
306
+ } else {
307
+ // macOS/Windows fallback: Node exposes no directory-fd-relative chmod/chown there, so this
308
+ // branch re-resolves each path lexically (confineAll) and then acts on the path. Between those
309
+ // two steps another process can swap a component, so confinement is a guardrail on this
310
+ // platform rather than a descriptor-bound guarantee — the same residual the archive/document
311
+ // staging path documents. Linux uses the descriptor-bound path above.
312
+ const info = await stat(absolute).catch(() => fail(`Path not found: ${displayPath(absolute)}`));
313
+ const targets = [absolute];
314
+ if (recursive && info.isDirectory()) {
315
+ const walk = async target => {
316
+ const entries = await readdir(target, { withFileTypes:true }).catch(() => []);
317
+ for (const entry of entries) {
318
+ if (entry.isSymbolicLink()) continue;
319
+ const child = path.join(target, entry.name);
320
+ targets.push(child);
321
+ if (entry.isDirectory()) await walk(child);
322
+ }
323
+ };
324
+ await walk(absolute);
325
+ }
326
+ const confined = await confineAll(targets);
327
+ for (const target of confined) {
328
+ try {
329
+ await chmod(target, mode);
330
+ if (uid !== null || gid !== null) await chown(target, uid ?? -1, gid ?? -1);
331
+ changed += 1;
332
+ } catch (error) {
333
+ failures.push(`${displayPath(target)}: ${describeFilesystemFailure(error, { path:error?.path })}`);
334
+ }
335
+ }
336
+ }
337
+ const summary = `Set mode ${raw}${uid !== null || gid !== null ? ` (uid ${uid ?? '-'} gid ${gid ?? '-'})` : ''} on ${changed} path(s) starting at ${displayPath(absolute)}.`;
338
+ if (!failures.length) return text(summary);
339
+ return text(`${summary}\n${failures.length} path(s) could not be changed:\n${failures.slice(0, 20).join('\n')}`, true);
340
+ }
341
+
342
+ export async function createDirectoryTool(args) {
343
+ const list = Array.isArray(args.paths) ? args.paths : [args.path];
344
+ if (!list.filter(Boolean).length) fail('path (or paths) is required');
345
+ if (list.length > 200) fail('paths accepts at most 200 entries per call');
346
+ const created = [];
347
+ const failed = [];
348
+ for (const entry of list) {
349
+ try {
350
+ const absolute = await resolveSafePath(entry);
351
+ await ensureDirectoryPath(absolute);
352
+ created.push(displayPath(absolute));
353
+ } catch (error) {
354
+ failed.push(`${entry}: ${describeFilesystemFailure(error, { path: error?.path })}`);
355
+ }
356
+ }
357
+ const header = `${created.length} director${created.length === 1 ? 'y' : 'ies'} ready`;
358
+ return text([header, ...created, ...failed.map(line => `failed ${line}`)].join('\n'), failed.length > 0);
359
+ }
360
+
361
+ export async function pathExists(target) {
362
+ try { await access(target, constants.F_OK); return true; } catch { return false; }
363
+ }
364
+
365
+ export async function moveFileTool(args) {
366
+ const source = await resolveSafePath(args.source, 'source');
367
+ const destination = await resolveSafePath(args.destination, 'destination');
368
+ if (source === destination) fail('source and destination are the same path');
369
+ const overwrite = args.overwrite !== false;
370
+ await withExistingParent(source, async anchoredSource => {
371
+ const sourceInfo = await lstat(anchoredSource).catch(() => fail(`Source not found: ${displayPath(source)}`));
372
+ if (sourceInfo.isSymbolicLink()) fail('source cannot be a symbolic link');
373
+ await withWritableParent(destination, async anchoredDestination => {
374
+ const existing = await lstat(anchoredDestination).catch(() => null);
375
+ if (existing?.isSymbolicLink()) fail('destination cannot be a symbolic link');
376
+ if (existing && !overwrite) fail(`Destination already exists: ${displayPath(destination)}. Pass overwrite: true to replace it.`);
377
+ if (existing?.isDirectory()) {
378
+ const entries = await readdir(anchoredDestination).catch(() => []);
379
+ if (entries.length) fail(`Destination is a non-empty directory: ${displayPath(destination)}. Move it aside or pick another name.`);
380
+ }
381
+ try {
382
+ await rename(anchoredSource, anchoredDestination);
383
+ } catch (error) {
384
+ if (error?.code !== 'EXDEV') throw error;
385
+ if (sourceInfo.isDirectory()) fail('Moving a directory across filesystems is not supported; copy it manually or move within one volume');
386
+ await copyTreeContents(anchoredSource, anchoredDestination, { entries:0, bytes:0, maxBytes:Number.MAX_SAFE_INTEGER });
387
+ await rm(anchoredSource, { force:true });
388
+ }
389
+ });
390
+ });
391
+ return text(`Moved ${displayPath(source)} to ${displayPath(destination)}.`);
392
+ }
393
+
394
+ export async function copyFileTool(args) {
395
+ const source = await resolveSafePath(args.source, 'source');
396
+ const destination = await resolveSafePath(args.destination, 'destination');
397
+ if (source === destination) fail('source and destination are the same path');
398
+ const overwrite = args.overwrite !== false;
399
+ let info;
400
+ await withExistingParent(source, async anchoredSource => {
401
+ const sourceInfo = await lstat(anchoredSource).catch(() => fail(`Source not found: ${displayPath(source)}`));
402
+ if (sourceInfo.isSymbolicLink()) fail('source cannot be a symbolic link');
403
+ if (sourceInfo.isDirectory()) fail('copy_file copies single files only; create the directory and copy its files individually');
404
+ info = sourceInfo;
405
+ await withWritableParent(destination, async anchoredDestination => {
406
+ const existing = await lstat(anchoredDestination).catch(() => null);
407
+ if (existing?.isSymbolicLink()) fail('destination cannot be a symbolic link');
408
+ if (existing && !overwrite) fail(`Destination already exists: ${displayPath(destination)}. Pass overwrite: true to replace it.`);
409
+ await copyTreeContents(anchoredSource, anchoredDestination, { entries:0, bytes:0, maxBytes:Number.MAX_SAFE_INTEGER });
410
+ });
411
+ });
412
+ return text(`Copied ${displayPath(source)} to ${displayPath(destination)} (${info.size} bytes).`);
413
+ }
414
+
415
+ // --- archives -------------------------------------------------------------------------