hillclimb 0.5.4 → 0.5.5
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/dist/cli.js +300 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2419,6 +2419,48 @@ async function readFileContent(file) {
|
|
|
2419
2419
|
}
|
|
2420
2420
|
}
|
|
2421
2421
|
|
|
2422
|
+
// src/middleware/large-file.ts
|
|
2423
|
+
import { constants as bufferConstants } from "buffer";
|
|
2424
|
+
var MAX_STRINGIFIABLE_BYTES = bufferConstants.MAX_STRING_LENGTH;
|
|
2425
|
+
var largeFileThreshold = MAX_STRINGIFIABLE_BYTES;
|
|
2426
|
+
function getLargeFileThreshold() {
|
|
2427
|
+
return largeFileThreshold;
|
|
2428
|
+
}
|
|
2429
|
+
var NEWLINE = 10;
|
|
2430
|
+
var OVERSIZED_LINE_PLACEHOLDER = '{"_hillclimb_omitted":"line exceeded max string length; dropped to allow redaction"}';
|
|
2431
|
+
function redactBufferByLine(buffer, redactLine, options = {}) {
|
|
2432
|
+
const maxLineBytes = options.maxLineBytes ?? MAX_STRINGIFIABLE_BYTES;
|
|
2433
|
+
const oversizedPlaceholder = options.oversizedPlaceholder ?? OVERSIZED_LINE_PLACEHOLDER;
|
|
2434
|
+
const pieces = [];
|
|
2435
|
+
const newlineBuf = Buffer.from([NEWLINE]);
|
|
2436
|
+
let totalCount = 0;
|
|
2437
|
+
let oversizedLines = 0;
|
|
2438
|
+
let start = 0;
|
|
2439
|
+
let first = true;
|
|
2440
|
+
for (; ; ) {
|
|
2441
|
+
const nl = buffer.indexOf(NEWLINE, start);
|
|
2442
|
+
const end = nl === -1 ? buffer.length : nl;
|
|
2443
|
+
const slice = buffer.subarray(start, end);
|
|
2444
|
+
if (!first) pieces.push(newlineBuf);
|
|
2445
|
+
first = false;
|
|
2446
|
+
if (slice.length > maxLineBytes) {
|
|
2447
|
+
oversizedLines++;
|
|
2448
|
+
pieces.push(Buffer.from(oversizedPlaceholder, "utf-8"));
|
|
2449
|
+
} else {
|
|
2450
|
+
const { value, count } = redactLine(slice.toString("utf-8"));
|
|
2451
|
+
totalCount += count;
|
|
2452
|
+
pieces.push(Buffer.from(value, "utf-8"));
|
|
2453
|
+
}
|
|
2454
|
+
if (nl === -1) break;
|
|
2455
|
+
start = nl + 1;
|
|
2456
|
+
}
|
|
2457
|
+
return {
|
|
2458
|
+
content: Buffer.concat(pieces),
|
|
2459
|
+
count: totalCount,
|
|
2460
|
+
oversizedLines
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2422
2464
|
// src/middleware/patterns.json
|
|
2423
2465
|
var patterns_default = {
|
|
2424
2466
|
patterns: [
|
|
@@ -10658,6 +10700,19 @@ function processFiles(files) {
|
|
|
10658
10700
|
}
|
|
10659
10701
|
|
|
10660
10702
|
// src/middleware/pattern-redact.ts
|
|
10703
|
+
function redactPatternLine(line, patterns, memo, isJsonl) {
|
|
10704
|
+
if (isJsonl) {
|
|
10705
|
+
if (!line.trim()) return { value: line, count: 0 };
|
|
10706
|
+
try {
|
|
10707
|
+
const parsed = JSON.parse(line);
|
|
10708
|
+
const walked = walkAndRedactAll(parsed, patterns, memo);
|
|
10709
|
+
return { value: JSON.stringify(walked.value), count: walked.count };
|
|
10710
|
+
} catch {
|
|
10711
|
+
return redactString(line, patterns);
|
|
10712
|
+
}
|
|
10713
|
+
}
|
|
10714
|
+
return redactString(line, patterns);
|
|
10715
|
+
}
|
|
10661
10716
|
var WORKER_COUNT = Math.min(
|
|
10662
10717
|
os3.availableParallelism?.() ?? os3.cpus().length,
|
|
10663
10718
|
4
|
|
@@ -10686,9 +10741,14 @@ var PatternRedactMiddleware = class {
|
|
|
10686
10741
|
const tasks = [];
|
|
10687
10742
|
const fileMap = /* @__PURE__ */ new Map();
|
|
10688
10743
|
const passThrough = [];
|
|
10744
|
+
const largeResults = /* @__PURE__ */ new Map();
|
|
10689
10745
|
for (let i = 0; i < group.files.length; i++) {
|
|
10690
10746
|
const file = group.files[i];
|
|
10691
10747
|
this.stats.filesScanned++;
|
|
10748
|
+
if (file.content && !file.isBinary && file.content.length > getLargeFileThreshold()) {
|
|
10749
|
+
largeResults.set(i, this.redactLargeFile(file));
|
|
10750
|
+
continue;
|
|
10751
|
+
}
|
|
10692
10752
|
const read = await readFileContent(file);
|
|
10693
10753
|
if (read.kind === "binary") {
|
|
10694
10754
|
this.stats.binaryFiles++;
|
|
@@ -10718,6 +10778,11 @@ var PatternRedactMiddleware = class {
|
|
|
10718
10778
|
}
|
|
10719
10779
|
const newFiles = [];
|
|
10720
10780
|
for (let i = 0; i < group.files.length; i++) {
|
|
10781
|
+
const large = largeResults.get(i);
|
|
10782
|
+
if (large) {
|
|
10783
|
+
newFiles.push(large);
|
|
10784
|
+
continue;
|
|
10785
|
+
}
|
|
10721
10786
|
const pt = passThrough.find((p7) => p7.index === i);
|
|
10722
10787
|
if (pt) {
|
|
10723
10788
|
newFiles.push(pt.file);
|
|
@@ -10739,6 +10804,33 @@ var PatternRedactMiddleware = class {
|
|
|
10739
10804
|
}
|
|
10740
10805
|
return { ...group, files: newFiles };
|
|
10741
10806
|
}
|
|
10807
|
+
// Stream-redact an oversized file straight from its buffer, one line at a time,
|
|
10808
|
+
// so we never build a string larger than a single line. Single-threaded (no
|
|
10809
|
+
// worker pool) — it trades parallelism for bounded memory on the one giant file.
|
|
10810
|
+
redactLargeFile(file) {
|
|
10811
|
+
const buffer = file.content;
|
|
10812
|
+
const isJsonl = file.absolutePath.endsWith(".jsonl");
|
|
10813
|
+
const patterns = compilePatterns();
|
|
10814
|
+
const memo = /* @__PURE__ */ new Map();
|
|
10815
|
+
const { content, count, oversizedLines } = redactBufferByLine(
|
|
10816
|
+
buffer,
|
|
10817
|
+
(line) => redactPatternLine(line, patterns, memo, isJsonl)
|
|
10818
|
+
);
|
|
10819
|
+
if (oversizedLines > 0) {
|
|
10820
|
+
appendLog(
|
|
10821
|
+
"warn",
|
|
10822
|
+
`pattern-redact: dropped ${oversizedLines} oversized line(s) in ${file.absolutePath} (exceeded max string length)`
|
|
10823
|
+
);
|
|
10824
|
+
}
|
|
10825
|
+
if (count > 0) {
|
|
10826
|
+
this.stats.filesRedacted++;
|
|
10827
|
+
this.stats.totalRedactions += count;
|
|
10828
|
+
}
|
|
10829
|
+
if (count > 0 || oversizedLines > 0) {
|
|
10830
|
+
return { ...file, content };
|
|
10831
|
+
}
|
|
10832
|
+
return file;
|
|
10833
|
+
}
|
|
10742
10834
|
async processWithWorkers(tasks) {
|
|
10743
10835
|
const workerPath = resolveWorkerPath();
|
|
10744
10836
|
const sorted = [...tasks].sort(
|
|
@@ -10814,6 +10906,10 @@ var RedactMiddleware = class {
|
|
|
10814
10906
|
for (const file of group.files) {
|
|
10815
10907
|
this.stats.filesScanned++;
|
|
10816
10908
|
regex.lastIndex = 0;
|
|
10909
|
+
if (file.content && !file.isBinary && file.content.length > getLargeFileThreshold()) {
|
|
10910
|
+
newFiles.push(this.redactLargeFile(file, regex));
|
|
10911
|
+
continue;
|
|
10912
|
+
}
|
|
10817
10913
|
const read = await readFileContent(file);
|
|
10818
10914
|
if (read.kind === "binary") {
|
|
10819
10915
|
this.stats.binaryFiles++;
|
|
@@ -10849,6 +10945,47 @@ var RedactMiddleware = class {
|
|
|
10849
10945
|
}
|
|
10850
10946
|
return { ...group, files: newFiles };
|
|
10851
10947
|
}
|
|
10948
|
+
// Stream-redact an oversized file straight from its buffer. Each line is
|
|
10949
|
+
// redacted with the same JSON-aware (or raw) logic as redactJsonl, so the
|
|
10950
|
+
// output matches the in-memory path exactly for files whose lines all fit in a
|
|
10951
|
+
// string — which holds for real transcripts (largest line is a few MiB).
|
|
10952
|
+
redactLargeFile(file, regex) {
|
|
10953
|
+
const buffer = file.content;
|
|
10954
|
+
const isJsonl = file.absolutePath.endsWith(".jsonl");
|
|
10955
|
+
const { content, count, oversizedLines } = redactBufferByLine(
|
|
10956
|
+
buffer,
|
|
10957
|
+
(line) => this.redactLine(line, regex, isJsonl)
|
|
10958
|
+
);
|
|
10959
|
+
if (oversizedLines > 0) {
|
|
10960
|
+
appendLog(
|
|
10961
|
+
"warn",
|
|
10962
|
+
`redact: dropped ${oversizedLines} oversized line(s) in ${file.absolutePath} (exceeded max string length)`
|
|
10963
|
+
);
|
|
10964
|
+
}
|
|
10965
|
+
if (count > 0) {
|
|
10966
|
+
this.stats.filesRedacted++;
|
|
10967
|
+
this.stats.totalRedactions += count;
|
|
10968
|
+
}
|
|
10969
|
+
if (count > 0 || oversizedLines > 0) {
|
|
10970
|
+
return { ...file, content };
|
|
10971
|
+
}
|
|
10972
|
+
return file;
|
|
10973
|
+
}
|
|
10974
|
+
redactLine(line, regex, isJsonl) {
|
|
10975
|
+
if (isJsonl) {
|
|
10976
|
+
if (!line.trim()) return { value: line, count: 0 };
|
|
10977
|
+
try {
|
|
10978
|
+
const parsed = JSON.parse(line);
|
|
10979
|
+
const walked = walkAndRedact(parsed, regex);
|
|
10980
|
+
return { value: JSON.stringify(walked.value), count: walked.count };
|
|
10981
|
+
} catch {
|
|
10982
|
+
const { result: result2, count: count2 } = this.redactString(line, regex);
|
|
10983
|
+
return { value: result2, count: count2 };
|
|
10984
|
+
}
|
|
10985
|
+
}
|
|
10986
|
+
const { result, count } = this.redactString(line, regex);
|
|
10987
|
+
return { value: result, count };
|
|
10988
|
+
}
|
|
10852
10989
|
redactJsonl(content, regex) {
|
|
10853
10990
|
let totalCount = 0;
|
|
10854
10991
|
const lines = content.split("\n");
|
|
@@ -13351,10 +13488,10 @@ var NormalizeMiddleware = class {
|
|
|
13351
13488
|
file.sourceName
|
|
13352
13489
|
))
|
|
13353
13490
|
continue;
|
|
13354
|
-
const content = file.content ? file.content.toString("utf-8") : null;
|
|
13355
|
-
if (!content) continue;
|
|
13356
13491
|
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
|
|
13357
13492
|
try {
|
|
13493
|
+
const content = file.content ? file.content.toString("utf-8") : null;
|
|
13494
|
+
if (!content) continue;
|
|
13358
13495
|
const trajectory = normalizeContent(
|
|
13359
13496
|
file.sourceName,
|
|
13360
13497
|
content,
|
|
@@ -13383,7 +13520,7 @@ var NormalizeMiddleware = class {
|
|
|
13383
13520
|
} catch (err) {
|
|
13384
13521
|
appendLog(
|
|
13385
13522
|
"warn",
|
|
13386
|
-
`normalize:
|
|
13523
|
+
`normalize: skipped ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
13387
13524
|
);
|
|
13388
13525
|
}
|
|
13389
13526
|
}
|
|
@@ -14085,7 +14222,7 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
14085
14222
|
import path18 from "path";
|
|
14086
14223
|
|
|
14087
14224
|
// src/git-traces/git-ops.ts
|
|
14088
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
14225
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
14089
14226
|
import fs12 from "fs";
|
|
14090
14227
|
import os7 from "os";
|
|
14091
14228
|
import path16 from "path";
|
|
@@ -14097,11 +14234,155 @@ var EXEC_OPTS = {
|
|
|
14097
14234
|
};
|
|
14098
14235
|
var MAX_ERROR_OUTPUT_CHARS = 2e3;
|
|
14099
14236
|
var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
|
|
14100
|
-
var
|
|
14237
|
+
var MAX_BINARY_SNAPSHOT_FILE_BYTES = 1024 * 1024;
|
|
14238
|
+
var BINARY_SNIFF_BYTES = 8e3;
|
|
14239
|
+
var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
14240
|
+
// Documents
|
|
14241
|
+
".pdf",
|
|
14242
|
+
// Raster images
|
|
14243
|
+
".png",
|
|
14244
|
+
".jpg",
|
|
14245
|
+
".jpeg",
|
|
14246
|
+
".gif",
|
|
14247
|
+
".bmp",
|
|
14248
|
+
".tiff",
|
|
14249
|
+
".tif",
|
|
14250
|
+
".webp",
|
|
14251
|
+
".ico",
|
|
14252
|
+
".heic",
|
|
14253
|
+
".heif",
|
|
14254
|
+
".avif",
|
|
14255
|
+
// Video
|
|
14256
|
+
".mp4",
|
|
14257
|
+
".mov",
|
|
14258
|
+
".avi",
|
|
14259
|
+
".mkv",
|
|
14260
|
+
".webm",
|
|
14261
|
+
".m4v",
|
|
14262
|
+
".mpg",
|
|
14263
|
+
".mpeg",
|
|
14264
|
+
".wmv",
|
|
14265
|
+
".flv",
|
|
14266
|
+
// Audio
|
|
14267
|
+
".mp3",
|
|
14268
|
+
".wav",
|
|
14269
|
+
".flac",
|
|
14270
|
+
".ogg",
|
|
14271
|
+
".m4a",
|
|
14272
|
+
".aac",
|
|
14273
|
+
".aiff",
|
|
14274
|
+
// Archives / compressed
|
|
14275
|
+
".zip",
|
|
14276
|
+
".tar",
|
|
14277
|
+
".gz",
|
|
14278
|
+
".tgz",
|
|
14279
|
+
".bz2",
|
|
14280
|
+
".tbz2",
|
|
14281
|
+
".xz",
|
|
14282
|
+
".7z",
|
|
14283
|
+
".rar",
|
|
14284
|
+
".zst",
|
|
14285
|
+
".lz4",
|
|
14286
|
+
// Fonts
|
|
14287
|
+
".woff",
|
|
14288
|
+
".woff2",
|
|
14289
|
+
".ttf",
|
|
14290
|
+
".otf",
|
|
14291
|
+
".eot",
|
|
14292
|
+
// Compiled / native artifacts
|
|
14293
|
+
".so",
|
|
14294
|
+
".dylib",
|
|
14295
|
+
".dll",
|
|
14296
|
+
".a",
|
|
14297
|
+
".o",
|
|
14298
|
+
".lib",
|
|
14299
|
+
".class",
|
|
14300
|
+
".jar",
|
|
14301
|
+
".wasm",
|
|
14302
|
+
".exe",
|
|
14303
|
+
".pyc",
|
|
14304
|
+
".pyd",
|
|
14305
|
+
".node",
|
|
14306
|
+
// Databases
|
|
14307
|
+
".sqlite",
|
|
14308
|
+
".sqlite3",
|
|
14309
|
+
".db",
|
|
14310
|
+
".mdb",
|
|
14311
|
+
// Office / design binaries
|
|
14312
|
+
".doc",
|
|
14313
|
+
".docx",
|
|
14314
|
+
".xls",
|
|
14315
|
+
".xlsx",
|
|
14316
|
+
".ppt",
|
|
14317
|
+
".pptx",
|
|
14318
|
+
".odt",
|
|
14319
|
+
".ods",
|
|
14320
|
+
".odp",
|
|
14321
|
+
".psd",
|
|
14322
|
+
".ai",
|
|
14323
|
+
".sketch",
|
|
14324
|
+
".fig",
|
|
14325
|
+
".blend"
|
|
14326
|
+
]);
|
|
14327
|
+
var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
|
|
14328
|
+
".DS_Store",
|
|
14329
|
+
"Thumbs.db",
|
|
14330
|
+
"desktop.ini"
|
|
14331
|
+
]);
|
|
14101
14332
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
14102
|
-
function
|
|
14333
|
+
function isExcludedSnapshotPath(filePath) {
|
|
14334
|
+
if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
|
|
14103
14335
|
return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
|
|
14104
14336
|
}
|
|
14337
|
+
function isBinaryBuffer(buffer) {
|
|
14338
|
+
return buffer.includes(0);
|
|
14339
|
+
}
|
|
14340
|
+
function readTreeBlobHead(repoRoot, sha) {
|
|
14341
|
+
try {
|
|
14342
|
+
const { stdout } = spawnSync("git", ["cat-file", "blob", sha], {
|
|
14343
|
+
cwd: repoRoot,
|
|
14344
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
14345
|
+
maxBuffer: BINARY_SNIFF_BYTES
|
|
14346
|
+
});
|
|
14347
|
+
return stdout?.length ? stdout.subarray(0, BINARY_SNIFF_BYTES) : null;
|
|
14348
|
+
} catch {
|
|
14349
|
+
return null;
|
|
14350
|
+
}
|
|
14351
|
+
}
|
|
14352
|
+
function readWorkingFileHead(absPath) {
|
|
14353
|
+
let fd = null;
|
|
14354
|
+
try {
|
|
14355
|
+
fd = fs12.openSync(absPath, "r");
|
|
14356
|
+
const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
14357
|
+
const bytesRead = fs12.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
|
|
14358
|
+
return buffer.subarray(0, bytesRead);
|
|
14359
|
+
} catch {
|
|
14360
|
+
return null;
|
|
14361
|
+
} finally {
|
|
14362
|
+
if (fd !== null) {
|
|
14363
|
+
try {
|
|
14364
|
+
fs12.closeSync(fd);
|
|
14365
|
+
} catch {
|
|
14366
|
+
}
|
|
14367
|
+
}
|
|
14368
|
+
}
|
|
14369
|
+
}
|
|
14370
|
+
function classifyOmission(filePath, sizeBytes, readHead) {
|
|
14371
|
+
if (isExcludedSnapshotPath(filePath)) return "excluded-extension";
|
|
14372
|
+
if (sizeBytes > MAX_SNAPSHOT_FILE_BYTES) return "file-over-limit";
|
|
14373
|
+
if (sizeBytes > MAX_BINARY_SNAPSHOT_FILE_BYTES) {
|
|
14374
|
+
const head = readHead();
|
|
14375
|
+
if (!head) {
|
|
14376
|
+
appendLog(
|
|
14377
|
+
"warn",
|
|
14378
|
+
`git-traces: binary sniff failed for ${filePath} (${sizeBytes} bytes); keeping it in the snapshot`
|
|
14379
|
+
);
|
|
14380
|
+
return null;
|
|
14381
|
+
}
|
|
14382
|
+
if (isBinaryBuffer(head)) return "binary-over-limit";
|
|
14383
|
+
}
|
|
14384
|
+
return null;
|
|
14385
|
+
}
|
|
14105
14386
|
function quoteGitArg(arg) {
|
|
14106
14387
|
if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
|
|
14107
14388
|
return JSON.stringify(arg);
|
|
@@ -14205,7 +14486,7 @@ function recordOmittedSnapshotFile(omittedFiles, file, options = {}) {
|
|
|
14205
14486
|
omittedFiles?.push(file);
|
|
14206
14487
|
if (options.log === false) return;
|
|
14207
14488
|
const action = file.tracked ? "omitting tracked" : "skipping untracked";
|
|
14208
|
-
const reason = file.reason === "file-over-limit" ? `${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit` : "excluded extension";
|
|
14489
|
+
const reason = file.reason === "file-over-limit" ? `${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit` : file.reason === "binary-over-limit" ? `binary ${file.sizeBytes} bytes > ${MAX_BINARY_SNAPSHOT_FILE_BYTES} binary limit` : "excluded extension";
|
|
14209
14490
|
appendLog("warn", `git-traces: ${action} ${file.path} (${reason})`);
|
|
14210
14491
|
}
|
|
14211
14492
|
function parseLsTreeLongZ(output) {
|
|
@@ -14228,7 +14509,11 @@ function listOmittedTreeFiles(repoRoot, treeSha, options = {}) {
|
|
|
14228
14509
|
const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
|
|
14229
14510
|
const omitted = [];
|
|
14230
14511
|
for (const entry of parseLsTreeLongZ(output)) {
|
|
14231
|
-
const reason =
|
|
14512
|
+
const reason = classifyOmission(
|
|
14513
|
+
entry.path,
|
|
14514
|
+
entry.sizeBytes,
|
|
14515
|
+
() => readTreeBlobHead(repoRoot, entry.sha)
|
|
14516
|
+
);
|
|
14232
14517
|
if (!reason) continue;
|
|
14233
14518
|
const file = {
|
|
14234
14519
|
path: entry.path,
|
|
@@ -14286,8 +14571,13 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
14286
14571
|
for (const relPath of list.split("\0")) {
|
|
14287
14572
|
if (!relPath) continue;
|
|
14288
14573
|
try {
|
|
14289
|
-
const
|
|
14290
|
-
const
|
|
14574
|
+
const absPath = path16.join(repoRoot, relPath);
|
|
14575
|
+
const stat = fs12.lstatSync(absPath);
|
|
14576
|
+
const reason = classifyOmission(
|
|
14577
|
+
relPath,
|
|
14578
|
+
stat.size,
|
|
14579
|
+
() => readWorkingFileHead(absPath)
|
|
14580
|
+
);
|
|
14291
14581
|
if (reason) {
|
|
14292
14582
|
recordOmittedSnapshotFile(omittedFiles, {
|
|
14293
14583
|
path: relPath,
|