@ankurhalder/codebase-snapshot 4.0.0 → 4.0.2

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.
@@ -2188,11 +2188,15 @@ function isSensitivePath(relativePath) {
2188
2188
  }
2189
2189
  return matchesSensitiveList(posixPath, base);
2190
2190
  }
2191
- function findSecretInPath(relativePath) {
2191
+ function pathScanRules(extraRules) {
2192
+ const fallback = extraRules ?? [];
2193
+ return [...DEFAULT_SECRET_PATTERNS, ...fallback];
2194
+ }
2195
+ function findSecretInPath(relativePath, extraRules) {
2192
2196
  const posixPath = relativePath.replace(/\\/g, "/");
2193
2197
  const spans = [];
2194
2198
  const rules = [];
2195
- for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
2199
+ for (const { rule, pattern } of pathScanRules(extraRules)) {
2196
2200
  const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2197
2201
  let match;
2198
2202
  while ((match = scan.exec(posixPath)) !== null) {
@@ -2200,24 +2204,39 @@ function findSecretInPath(relativePath) {
2200
2204
  scan.lastIndex += 1;
2201
2205
  continue;
2202
2206
  }
2203
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
2207
+ spans.push({ start: match.index, end: match.index + match[0].length });
2204
2208
  if (!rules.includes(rule)) rules.push(rule);
2205
2209
  }
2206
2210
  }
2207
2211
  if (spans.length === 0) return null;
2208
2212
  spans.sort((a, b) => a.start - b.start || a.end - b.end);
2213
+ const merged = [];
2214
+ for (const span of spans) {
2215
+ const last = merged[merged.length - 1];
2216
+ if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
2217
+ else merged.push({ start: span.start, end: span.end });
2218
+ }
2209
2219
  let out = "";
2210
2220
  let cursor = 0;
2211
- for (const span of spans) {
2212
- if (span.end <= cursor) continue;
2213
- const start = Math.max(span.start, cursor);
2214
- out += posixPath.slice(cursor, start);
2221
+ for (const span of merged) {
2222
+ out += posixPath.slice(cursor, span.start);
2215
2223
  out += REDACTION_MARKER;
2216
- cursor = Math.min(span.end, posixPath.length);
2224
+ cursor = span.end;
2217
2225
  }
2218
2226
  out += posixPath.slice(cursor);
2219
2227
  return { rules, masked: out };
2220
2228
  }
2229
+ function maskSkipPaths(records, extraRules) {
2230
+ return records.map((record) => {
2231
+ const hit = findSecretInPath(record.path, extraRules);
2232
+ const detail = record.detail ? findSecretInPath(record.detail, extraRules) : null;
2233
+ return {
2234
+ ...record,
2235
+ path: hit ? hit.masked : record.path,
2236
+ ...detail ? { detail: detail.masked } : {}
2237
+ };
2238
+ });
2239
+ }
2221
2240
  function globToRegExp(glob) {
2222
2241
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
2223
2242
  const anchored = normalized.startsWith("/");
@@ -2448,6 +2467,7 @@ function collectFiles(cwd, options = {}) {
2448
2467
  const includePatterns = options.includePatterns ?? [];
2449
2468
  const excludePatterns = options.excludePatterns ?? [];
2450
2469
  const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
2470
+ const pathRules = options.secretRules;
2451
2471
  const isToolOwned = makeToolOwnedDecider(cwd);
2452
2472
  let candidates;
2453
2473
  const skipped = [];
@@ -2483,7 +2503,7 @@ function collectFiles(cwd, options = {}) {
2483
2503
  }
2484
2504
  }
2485
2505
  } catch {
2486
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
2506
+ const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
2487
2507
  candidates = walked.files;
2488
2508
  skipped.push(...walked.skipped);
2489
2509
  }
@@ -2521,7 +2541,7 @@ function collectFiles(cwd, options = {}) {
2521
2541
  });
2522
2542
  return false;
2523
2543
  }
2524
- const secret = findSecretInPath(file);
2544
+ const secret = findSecretInPath(file, pathRules);
2525
2545
  if (secret) {
2526
2546
  skipped.push({
2527
2547
  path: secret.masked,
@@ -2532,30 +2552,29 @@ function collectFiles(cwd, options = {}) {
2532
2552
  }
2533
2553
  return true;
2534
2554
  }).sort();
2535
- return { files, skipped };
2555
+ return { files, skipped: maskSkipPaths(skipped, pathRules) };
2536
2556
  }
2537
- function walkDirectory(dir, baseDir, visited, isRoot = false) {
2538
- const results = { files: [], skipped: [] };
2539
- let realDir;
2557
+ function rootRealFor(root) {
2540
2558
  try {
2541
- realDir = fs9.realpathSync(dir);
2559
+ return fs9.realpathSync(root);
2542
2560
  } catch {
2543
- realDir = dir;
2544
- }
2545
- if (visited.has(realDir)) return results;
2546
- visited.add(realDir);
2547
- let rootReal;
2548
- try {
2549
- rootReal = fs9.realpathSync(baseDir);
2550
- } catch {
2551
- rootReal = baseDir;
2561
+ return root;
2552
2562
  }
2563
+ }
2564
+ function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
2565
+ const results = { files: [], skipped: [] };
2566
+ if (visited.has(dir)) return results;
2567
+ visited.add(dir);
2553
2568
  const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
2554
2569
  let entries;
2555
2570
  try {
2556
2571
  entries = fs9.readdirSync(dir, { withFileTypes: true });
2557
2572
  } catch {
2558
- results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
2573
+ results.skipped.push({
2574
+ path: path10.relative(baseDir, dir).replace(/\\/g, "/") || ".",
2575
+ reason: "read-error",
2576
+ detail: "directory unreadable"
2577
+ });
2559
2578
  return results;
2560
2579
  }
2561
2580
  for (const entry of entries) {
@@ -2634,7 +2653,7 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
2634
2653
  isFile = true;
2635
2654
  }
2636
2655
  if (isDirectory) {
2637
- const nested = walkDirectory(fullPath, baseDir, visited);
2656
+ const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
2638
2657
  results.files.push(...nested.files);
2639
2658
  results.skipped.push(...nested.skipped);
2640
2659
  } else if (isFile) {
@@ -2905,6 +2924,9 @@ async function loadConfigFileDetailed(cwd) {
2905
2924
  }
2906
2925
  async function loadConfigFile(cwd) {
2907
2926
  const loaded = await loadConfigFileDetailed(cwd);
2927
+ if (loaded.errors.length > 0) {
2928
+ throw new Error(`The config file could not be read: ${loaded.errors.join("; ")}`);
2929
+ }
2908
2930
  return loaded.config;
2909
2931
  }
2910
2932
  var MIN_MAX_PART_BYTES = 4096;
@@ -3333,7 +3355,8 @@ function defineConfig(config) {
3333
3355
  }
3334
3356
 
3335
3357
  // src/generator/audit.ts
3336
- function generateAuditReport(manifest, secretFindings) {
3358
+ function generateAuditReport(manifest, secretFindings, secretRules) {
3359
+ const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
3337
3360
  const lines = [
3338
3361
  "# Codebase Health & Security Audit",
3339
3362
  "",
@@ -3368,7 +3391,7 @@ function generateAuditReport(manifest, secretFindings) {
3368
3391
  "| :--- | :--- | :--- |"
3369
3392
  );
3370
3393
  for (const finding of secretFindings) {
3371
- lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
3394
+ lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
3372
3395
  }
3373
3396
  lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
3374
3397
  for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
@@ -3390,7 +3413,7 @@ function generateAuditReport(manifest, secretFindings) {
3390
3413
  for (const file of largeFiles) {
3391
3414
  const sizeKb = (file.sizeBytes / 1024).toFixed(1);
3392
3415
  const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
3393
- lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
3416
+ lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
3394
3417
  }
3395
3418
  lines.push("");
3396
3419
  }
@@ -3432,7 +3455,7 @@ function generateAuditReport(manifest, secretFindings) {
3432
3455
  "| :--- | :--- | :--- |"
3433
3456
  );
3434
3457
  for (const skip of manifest.skipped) {
3435
- lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
3458
+ lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
3436
3459
  }
3437
3460
  lines.push("");
3438
3461
  }
@@ -4094,6 +4117,9 @@ function detectLanguage(filePath) {
4094
4117
  const ext = path10.posix.extname(base).toLowerCase();
4095
4118
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
4096
4119
  }
4120
+ function maskedPath(value, secretRules) {
4121
+ return findSecretInPath(value, secretRules)?.masked ?? value;
4122
+ }
4097
4123
  function emptyDrift() {
4098
4124
  return {
4099
4125
  missingInSnapshot: [],
@@ -4164,15 +4190,20 @@ function validateSnapshot(configInput) {
4164
4190
  const skippedSet = new Set(
4165
4191
  manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
4166
4192
  );
4193
+ const isSkippedOnDisk = (diskFile) => {
4194
+ if (skippedSet.has(diskFile)) return true;
4195
+ return skippedSet.has(maskedPath(diskFile, config.secretRules));
4196
+ };
4167
4197
  const collection = collectFiles(config.cwd, {
4168
4198
  includePatterns: config.include,
4169
4199
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
4170
- includeSensitiveFiles: config.includeSensitiveFiles
4200
+ includeSensitiveFiles: config.includeSensitiveFiles,
4201
+ secretRules: config.secretRules
4171
4202
  });
4172
4203
  for (const diskFile of collection.files) {
4173
4204
  const snapshotHash = manifestFileMap.get(diskFile);
4174
4205
  if (!snapshotHash) {
4175
- if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
4206
+ if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
4176
4207
  drift.missingInSnapshot.push(diskFile);
4177
4208
  continue;
4178
4209
  }
@@ -4441,10 +4472,19 @@ function validateSnapshot(configInput) {
4441
4472
  `${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
4442
4473
  );
4443
4474
  }
4475
+ const maskedDrift = {
4476
+ ...drift,
4477
+ missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
4478
+ missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
4479
+ hashMismatches: drift.hashMismatches.map((mismatch) => ({
4480
+ ...mismatch,
4481
+ path: maskedPath(mismatch.path, config.secretRules)
4482
+ }))
4483
+ };
4444
4484
  return {
4445
4485
  valid: errors.length === 0,
4446
4486
  manifestPresent: true,
4447
- drift,
4487
+ drift: maskedDrift,
4448
4488
  errors,
4449
4489
  warnings
4450
4490
  };
@@ -4479,18 +4519,22 @@ function readManifest2(config) {
4479
4519
  function explainPath(config, target) {
4480
4520
  const relative4 = normalizeTarget(config.cwd, target);
4481
4521
  const absolute = path10.join(config.cwd, relative4);
4522
+ const maskedPath2 = findSecretInPath(relative4, config.secretRules)?.masked ?? relative4;
4482
4523
  const result = {
4483
- path: relative4,
4524
+ path: maskedPath2,
4484
4525
  exists: fs9.existsSync(absolute),
4485
4526
  included: false
4486
4527
  };
4487
4528
  const collection = collectFiles(config.cwd, {
4488
4529
  includePatterns: config.include,
4489
4530
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
4490
- includeSensitiveFiles: config.includeSensitiveFiles
4531
+ includeSensitiveFiles: config.includeSensitiveFiles,
4532
+ secretRules: config.secretRules
4491
4533
  });
4492
4534
  if (!collection.files.includes(relative4)) {
4493
- const record = collection.skipped.find((entry2) => entry2.path === relative4);
4535
+ const record = collection.skipped.find(
4536
+ (entry2) => entry2.path === relative4 || entry2.path === maskedPath2
4537
+ );
4494
4538
  if (record) {
4495
4539
  result.reason = record.reason;
4496
4540
  result.detail = record.detail;
@@ -4527,7 +4571,7 @@ function explainPath(config, target) {
4527
4571
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
4528
4572
  }
4529
4573
  const manifest = readManifest2(config);
4530
- const entry = manifest?.files.find((file) => file.relativePath === relative4);
4574
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative4) : void 0;
4531
4575
  result.part = entry?.part ?? null;
4532
4576
  result.inclusionMode = entry?.inclusionMode;
4533
4577
  return result;
@@ -4632,12 +4676,13 @@ function compareManifests(before, after) {
4632
4676
  if (!right.has(filePath)) removed.push(filePath);
4633
4677
  }
4634
4678
  const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
4679
+ const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
4635
4680
  return {
4636
4681
  before: side(before),
4637
4682
  after: side(after),
4638
- added: sort(added),
4639
- removed: sort(removed),
4640
- changed: sort(changed),
4683
+ added: mask(sort(added)),
4684
+ removed: mask(sort(removed)),
4685
+ changed: mask(sort(changed)),
4641
4686
  unchanged
4642
4687
  };
4643
4688
  }
@@ -5362,7 +5407,7 @@ async function runInit(options) {
5362
5407
 
5363
5408
  // src/index.ts
5364
5409
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
5365
- var GENERATOR_VERSION = "4.0.0";
5410
+ var GENERATOR_VERSION = "4.0.2";
5366
5411
  var SCHEMA_VERSION = "3.0.0";
5367
5412
  function sha256Hex2(input) {
5368
5413
  return crypto.createHash("sha256").update(input).digest("hex");
@@ -5399,7 +5444,6 @@ function bump(counts, key) {
5399
5444
  }
5400
5445
  var RELATIVE_SPECIFIER = /^\.{1,2}\//;
5401
5446
  var IMPORT_CANDIDATE_SUFFIXES = [
5402
- "",
5403
5447
  ".ts",
5404
5448
  ".tsx",
5405
5449
  ".mts",
@@ -5413,23 +5457,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
5413
5457
  "/index.js",
5414
5458
  "/index.mjs"
5415
5459
  ];
5416
- function resolveSpecifier(fromFile, specifier, knownFiles) {
5460
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5461
+ ".ts",
5462
+ ".tsx",
5463
+ ".mts",
5464
+ ".cts",
5465
+ ".js",
5466
+ ".jsx",
5467
+ ".mjs",
5468
+ ".cjs"
5469
+ ]);
5470
+ function buildImportResolver(files) {
5471
+ const byPath = /* @__PURE__ */ new Map();
5472
+ const byPathFolded = /* @__PURE__ */ new Map();
5473
+ const byStem = /* @__PURE__ */ new Map();
5474
+ const byStemFolded = /* @__PURE__ */ new Map();
5475
+ const remember = (map, key, value) => {
5476
+ if (!map.has(key)) map.set(key, value);
5477
+ };
5478
+ for (const file of files) {
5479
+ const relativePath = file.relativePath;
5480
+ remember(byPath, relativePath, relativePath);
5481
+ remember(byPathFolded, relativePath.toLowerCase(), relativePath);
5482
+ const extension = path10.posix.extname(relativePath);
5483
+ if (!SOURCE_EXTENSIONS.has(extension)) continue;
5484
+ const stem = relativePath.slice(0, -extension.length);
5485
+ remember(byStem, stem, relativePath);
5486
+ remember(byStemFolded, stem.toLowerCase(), relativePath);
5487
+ if (path10.posix.basename(stem) === "index") {
5488
+ const directory = path10.posix.dirname(stem);
5489
+ remember(byStem, directory, relativePath);
5490
+ remember(byStemFolded, directory.toLowerCase(), relativePath);
5491
+ }
5492
+ }
5493
+ return { byPath, byPathFolded, byStem, byStemFolded };
5494
+ }
5495
+ function resolveSpecifier(fromFile, specifier, resolver) {
5417
5496
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5418
- const baseDir = path10.posix.dirname(fromFile);
5419
- const joined = path10.posix.normalize(path10.posix.join(baseDir, specifier));
5420
- const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5497
+ const joined = path10.posix.normalize(
5498
+ path10.posix.join(path10.posix.dirname(fromFile), specifier)
5499
+ );
5500
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5501
+ const direct = exact(joined);
5502
+ if (direct) return direct;
5421
5503
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
5422
- const withSuffix = `${joined}${suffix}`;
5423
- if (knownFiles.has(withSuffix)) return withSuffix;
5504
+ const hit = exact(`${joined}${suffix}`);
5505
+ if (hit) return hit;
5424
5506
  }
5425
- const extensionSwap = new RegExp(`${candidate}\\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$`);
5426
- for (const file of knownFiles) {
5427
- if (extensionSwap.test(file)) return file;
5428
- }
5429
- return null;
5507
+ const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5508
+ const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
5509
+ return byStem(joined) ?? byStem(stem);
5430
5510
  }
5431
5511
  function buildIndexes(files) {
5432
- const knownFiles = new Set(files.map((file) => file.relativePath));
5512
+ const resolver = buildImportResolver(files);
5433
5513
  const importerCounts = /* @__PURE__ */ new Map();
5434
5514
  const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
5435
5515
  const summary = file.summary;
@@ -5441,7 +5521,7 @@ function buildIndexes(files) {
5441
5521
  });
5442
5522
  for (const entry of byFile) {
5443
5523
  for (const specifier of entry.imports) {
5444
- const resolved = resolveSpecifier(entry.path, specifier, knownFiles);
5524
+ const resolved = resolveSpecifier(entry.path, specifier, resolver);
5445
5525
  if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
5446
5526
  }
5447
5527
  }
@@ -5532,15 +5612,22 @@ function resolveSafeOutputDirs(config) {
5532
5612
  return { outDir, tmpDir };
5533
5613
  }
5534
5614
  var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
5535
- var STAGING_ENTRIES = /* @__PURE__ */ new Set([
5536
- ".codebase-snapshot",
5537
- "manifest.json",
5538
- "README.md",
5539
- "parts",
5540
- "reports",
5541
- "full-codebase.json",
5542
- "full-codebase.md"
5543
- ]);
5615
+ var RUN_DIR_PREFIX = "run-";
5616
+ var STALE_RUN_MS = 60 * 60 * 1e3;
5617
+ function maskPathText(value, secretRules) {
5618
+ return findSecretInPath(value, secretRules)?.masked ?? value;
5619
+ }
5620
+ function maskSkipRecords(records, secretRules) {
5621
+ return maskSkipPaths(records, secretRules);
5622
+ }
5623
+ function isProcessAlive(pid) {
5624
+ try {
5625
+ process.kill(pid, 0);
5626
+ return true;
5627
+ } catch (err) {
5628
+ return err.code !== "ESRCH";
5629
+ }
5630
+ }
5544
5631
  var OWNERSHIP_MARKER = ".codebase-snapshot";
5545
5632
  var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
5546
5633
  var SNAPSHOT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -5570,6 +5657,7 @@ function markerPayload(dir, state, id, createdAt) {
5570
5657
  createdAt,
5571
5658
  state,
5572
5659
  id,
5660
+ pid: process.pid,
5573
5661
  entries: listEntries(dir)
5574
5662
  };
5575
5663
  const manifestPath = path10.join(dir, "manifest.json");
@@ -5633,6 +5721,7 @@ function readOwnershipMarker(dir) {
5633
5721
  createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
5634
5722
  state: candidate.state,
5635
5723
  id: candidate.id,
5724
+ pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
5636
5725
  manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
5637
5726
  entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
5638
5727
  },
@@ -5740,7 +5829,7 @@ function describeEntries(entries, limit = 5) {
5740
5829
  const shown = entries.slice(0, limit).join(", ");
5741
5830
  return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
5742
5831
  }
5743
- function assertReplaceable(target, label) {
5832
+ function assertReplaceable(target, label, secretRules) {
5744
5833
  if (!fs9.existsSync(target)) return;
5745
5834
  if (!fs9.statSync(target).isDirectory()) {
5746
5835
  throw new Error(
@@ -5749,34 +5838,89 @@ function assertReplaceable(target, label) {
5749
5838
  }
5750
5839
  const entries = fs9.readdirSync(target);
5751
5840
  if (entries.length === 0) return;
5752
- if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5753
- const { marker: marker2, problem } = readOwnershipMarker(target);
5754
- if (label === "tmpDir") {
5755
- if (!marker2) {
5756
- throw new Error(
5757
- `Refusing to replace tmpDir ${target}: ${problem ?? "it does not carry this generator's marker"}. This generator only clears a staging directory it wrote. Point tmpDir at a new or empty directory, or remove ${target} yourself.`
5758
- );
5759
- }
5760
- const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
5761
- if (unexpected.length > 0) {
5762
- throw new Error(
5763
- `Refusing to replace tmpDir ${target}: it holds ${describeEntries(unexpected)}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
5764
- );
5765
- }
5766
- return;
5767
- }
5841
+ if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5842
+ const { problem } = readOwnershipMarker(target);
5768
5843
  const verdict = assessSnapshotDirectory(target);
5769
5844
  if (verdict.owned) return;
5845
+ const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
5846
+ const shownEntries = entries.map((name) => maskPathText(name, secretRules));
5770
5847
  if (verdict.foreignEntries.length > 0) {
5771
5848
  throw new Error(
5772
- `Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(verdict.foreignEntries)}. Remove them yourself, or point ${label} at a new or empty directory.`
5849
+ `Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(shownForeign)}. Remove them yourself, or point ${label} at a new or empty directory.`
5773
5850
  );
5774
5851
  }
5775
5852
  const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
5776
5853
  throw new Error(
5777
- `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5854
+ `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5778
5855
  );
5779
5856
  }
5857
+ function assertStagingRootUsable(tmpDir, secretRules) {
5858
+ if (!fs9.existsSync(tmpDir)) return;
5859
+ if (!fs9.statSync(tmpDir).isDirectory()) {
5860
+ throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
5861
+ }
5862
+ const entries = fs9.readdirSync(tmpDir);
5863
+ if (entries.length === 0) return;
5864
+ const foreign = entries.filter((name) => {
5865
+ const full = path10.join(tmpDir, name);
5866
+ if (!name.startsWith(RUN_DIR_PREFIX)) return true;
5867
+ let stats;
5868
+ try {
5869
+ stats = fs9.statSync(full);
5870
+ } catch {
5871
+ return true;
5872
+ }
5873
+ if (!stats.isDirectory()) return true;
5874
+ const { marker: marker2 } = readOwnershipMarker(full);
5875
+ if (marker2) return false;
5876
+ try {
5877
+ return fs9.readdirSync(full).length > 0;
5878
+ } catch {
5879
+ return true;
5880
+ }
5881
+ });
5882
+ if (foreign.length > 0) {
5883
+ const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
5884
+ throw new Error(
5885
+ `Refusing to replace tmpDir ${tmpDir}: it holds ${shown}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
5886
+ );
5887
+ }
5888
+ }
5889
+ function cleanStaleRunDirs(tmpDir) {
5890
+ let entries;
5891
+ try {
5892
+ entries = fs9.readdirSync(tmpDir);
5893
+ } catch {
5894
+ return;
5895
+ }
5896
+ for (const name of entries) {
5897
+ if (!name.startsWith(RUN_DIR_PREFIX)) continue;
5898
+ const full = path10.join(tmpDir, name);
5899
+ const { marker: marker2 } = readOwnershipMarker(full);
5900
+ if (marker2) {
5901
+ const created = Date.parse(marker2.createdAt);
5902
+ const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
5903
+ const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
5904
+ if (aged || abandoned) {
5905
+ try {
5906
+ fs9.rmSync(full, { recursive: true, force: true });
5907
+ } catch {
5908
+ }
5909
+ }
5910
+ continue;
5911
+ }
5912
+ try {
5913
+ if (fs9.readdirSync(full).length === 0) fs9.rmSync(full, { recursive: true, force: true });
5914
+ } catch {
5915
+ }
5916
+ }
5917
+ }
5918
+ function removeIfEmpty(dir) {
5919
+ try {
5920
+ if (fs9.readdirSync(dir).length === 0) fs9.rmSync(dir, { recursive: true, force: true });
5921
+ } catch {
5922
+ }
5923
+ }
5780
5924
  function ensureParentDirectory(target, label) {
5781
5925
  const parent = path10.dirname(target);
5782
5926
  if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
@@ -5792,14 +5936,12 @@ function ensureParentDirectory(target, label) {
5792
5936
  );
5793
5937
  }
5794
5938
  }
5795
- function acquireRunLock(outDir) {
5796
- const lockPath = path10.join(
5797
- os.tmpdir(),
5798
- `codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
5799
- );
5939
+ function acquireRunLock(stagingRoot) {
5940
+ const key = lockKeyFor(stagingRoot);
5941
+ const lockPath = path10.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
5800
5942
  const payload = `${JSON.stringify({
5801
5943
  pid: process.pid,
5802
- outDir,
5944
+ stagingRoot,
5803
5945
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
5804
5946
  })}
5805
5947
  `;
@@ -5824,11 +5966,20 @@ function acquireRunLock(outDir) {
5824
5966
  continue;
5825
5967
  }
5826
5968
  throw new Error(
5827
- `Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5969
+ `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5828
5970
  );
5829
5971
  }
5830
5972
  }
5831
- throw new Error(`Could not acquire the generate lock for ${outDir}.`);
5973
+ throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
5974
+ }
5975
+ function lockKeyFor(stagingRoot) {
5976
+ let key = stagingRoot;
5977
+ try {
5978
+ key = fs9.realpathSync(stagingRoot);
5979
+ } catch {
5980
+ }
5981
+ if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
5982
+ return sha256Hex2(key).slice(0, 16);
5832
5983
  }
5833
5984
  function lockOwnerIsGone(lockPath) {
5834
5985
  let raw;
@@ -5864,18 +6015,32 @@ function writeManifestAtomically(manifestPath, manifest) {
5864
6015
  throw err;
5865
6016
  }
5866
6017
  }
5867
- function swapIntoPlace(tmpDir, outDir) {
6018
+ function swapIntoPlace(stagingDir, outDir) {
5868
6019
  ensureParentDirectory(outDir, "outDir");
6020
+ const deadline = Date.now() + 3e3;
6021
+ for (let attempt = 0; ; attempt += 1) {
6022
+ try {
6023
+ swapOnce(stagingDir, outDir);
6024
+ return;
6025
+ } catch (err) {
6026
+ const code = err.code;
6027
+ const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
6028
+ if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
6029
+ sleepSync(50);
6030
+ }
6031
+ }
6032
+ }
6033
+ function swapOnce(stagingDir, outDir) {
5869
6034
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
5870
6035
  const hadPrevious = fs9.existsSync(outDir);
5871
6036
  if (hadPrevious) fs9.renameSync(outDir, backupDir);
5872
6037
  try {
5873
6038
  try {
5874
- fs9.renameSync(tmpDir, outDir);
6039
+ fs9.renameSync(stagingDir, outDir);
5875
6040
  } catch (err) {
5876
6041
  if (err.code === "EXDEV") {
5877
- fs9.cpSync(tmpDir, outDir, { recursive: true });
5878
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6042
+ fs9.cpSync(stagingDir, outDir, { recursive: true });
6043
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
5879
6044
  } else {
5880
6045
  throw err;
5881
6046
  }
@@ -5889,6 +6054,10 @@ function swapIntoPlace(tmpDir, outDir) {
5889
6054
  throw err;
5890
6055
  }
5891
6056
  }
6057
+ function sleepSync(milliseconds) {
6058
+ const buffer = new Int32Array(new SharedArrayBuffer(4));
6059
+ Atomics.wait(buffer, 0, 0, milliseconds);
6060
+ }
5892
6061
  function artifactWarnings(artifacts) {
5893
6062
  const messages = [];
5894
6063
  if (!artifacts.manifest) {
@@ -5951,11 +6120,12 @@ function generateSnapshot(configInput, options) {
5951
6120
  const collection = collectFiles(config.cwd, {
5952
6121
  includePatterns: config.include,
5953
6122
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5954
- includeSensitiveFiles: config.includeSensitiveFiles
6123
+ includeSensitiveFiles: config.includeSensitiveFiles,
6124
+ secretRules: config.secretRules
5955
6125
  });
5956
6126
  const files = [];
5957
6127
  const monolithBodies = /* @__PURE__ */ new Map();
5958
- const skipped = [...collection.skipped];
6128
+ const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
5959
6129
  const secretFindings = [];
5960
6130
  const warnings = [];
5961
6131
  const layerCounts = {};
@@ -6029,12 +6199,12 @@ function generateSnapshot(configInput, options) {
6029
6199
  summaryQuality = outcome?.quality;
6030
6200
  if (isSourceSummary(summary) && summary.parseErrors) {
6031
6201
  warnings.push(
6032
- `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
6202
+ `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
6033
6203
  );
6034
6204
  }
6035
6205
  } catch (err) {
6036
6206
  warnings.push(
6037
- `Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
6207
+ `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
6038
6208
  );
6039
6209
  }
6040
6210
  files.push({
@@ -6078,6 +6248,7 @@ function generateSnapshot(configInput, options) {
6078
6248
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
6079
6249
  const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
6080
6250
  const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
6251
+ const maskName = (value) => maskPathText(value, config.secretRules);
6081
6252
  const manifest = {
6082
6253
  schemaVersion: SCHEMA_VERSION,
6083
6254
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6098,13 +6269,13 @@ function generateSnapshot(configInput, options) {
6098
6269
  includeMonolith: config.includeMonolith,
6099
6270
  includeSensitiveFiles: config.includeSensitiveFiles,
6100
6271
  artifacts: { ...config.artifacts },
6101
- include: [...config.include],
6272
+ include: [...config.include].map(maskName),
6102
6273
  preset: config.preset,
6103
6274
  layersMode: config.layersMode,
6104
6275
  budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
6105
- excludePatterns: [...config.excludePatterns],
6106
- layers: config.layers.map((layer2) => layer2.name),
6107
- kinds: config.kinds.map((kind) => kind.kind)
6276
+ excludePatterns: [...config.excludePatterns].map(maskName),
6277
+ layers: config.layers.map((layer2) => maskName(layer2.name)),
6278
+ kinds: config.kinds.map((kind) => maskName(kind.kind))
6108
6279
  },
6109
6280
  counts: {
6110
6281
  totalFiles: files.length,
@@ -6143,12 +6314,12 @@ function generateSnapshot(configInput, options) {
6143
6314
  const { content: _content, ...rest } = file;
6144
6315
  return rest;
6145
6316
  }),
6146
- skipped,
6317
+ skipped: maskSkipRecords(skipped, config.secretRules),
6147
6318
  warnings
6148
6319
  };
6149
6320
  manifest.warnings = [
6150
6321
  ...warnings,
6151
- ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
6322
+ ...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
6152
6323
  ];
6153
6324
  manifest.totals.warningCount = manifest.warnings.length;
6154
6325
  let monolithJson = null;
@@ -6194,31 +6365,33 @@ function generateSnapshot(configInput, options) {
6194
6365
  manifest.totals.warningCount = manifest.warnings.length;
6195
6366
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6196
6367
  const { artifacts } = config;
6197
- assertReplaceable(tmpDir, "tmpDir");
6198
- assertReplaceable(outDir, "outDir");
6368
+ assertReplaceable(outDir, "outDir", config.secretRules);
6369
+ assertStagingRootUsable(tmpDir, config.secretRules);
6199
6370
  if (dryRun) {
6200
6371
  runAfterGenerate(config, manifest);
6201
6372
  return manifest;
6202
6373
  }
6203
- const releaseLock = acquireRunLock(outDir);
6204
- let stagingStarted = false;
6374
+ const releaseLock = acquireRunLock(tmpDir);
6375
+ let stagingDir = null;
6205
6376
  try {
6206
6377
  ensureParentDirectory(outDir, "outDir");
6207
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6208
6378
  fs9.mkdirSync(tmpDir, { recursive: true });
6209
- stagingStarted = true;
6379
+ cleanStaleRunDirs(tmpDir);
6210
6380
  const snapshotId = crypto.randomUUID();
6211
6381
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6212
- writeOwnershipMarker(tmpDir, {
6382
+ stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
6383
+ fs9.mkdirSync(stagingDir);
6384
+ writeOwnershipMarker(stagingDir, {
6213
6385
  generator: GENERATOR_NAME,
6214
6386
  version: GENERATOR_VERSION,
6215
6387
  schemaVersion: SCHEMA_VERSION,
6216
6388
  createdAt,
6217
6389
  state: "staging",
6218
- id: snapshotId
6390
+ id: snapshotId,
6391
+ pid: process.pid
6219
6392
  });
6220
- const partsDir = path10.join(tmpDir, "parts");
6221
- const reportsDir = path10.join(tmpDir, "reports");
6393
+ const partsDir = path10.join(stagingDir, "parts");
6394
+ const reportsDir = path10.join(stagingDir, "reports");
6222
6395
  if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
6223
6396
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6224
6397
  fs9.mkdirSync(reportsDir, { recursive: true });
@@ -6238,25 +6411,25 @@ function generateSnapshot(configInput, options) {
6238
6411
  if (artifacts.audit) {
6239
6412
  fs9.writeFileSync(
6240
6413
  path10.join(reportsDir, "audit.md"),
6241
- generateAuditReport(manifest, secretFindings),
6414
+ generateAuditReport(manifest, secretFindings, config.secretRules),
6242
6415
  "utf8"
6243
6416
  );
6244
6417
  }
6245
6418
  if (artifacts.readme) {
6246
- fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
6419
+ fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
6247
6420
  }
6248
6421
  if (artifacts.manifest) {
6249
- fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6422
+ fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6250
6423
  }
6251
6424
  if (monolithJson !== null) {
6252
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
6425
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
6253
6426
  }
6254
6427
  if (monolithMarkdown !== null) {
6255
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
6428
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
6256
6429
  }
6257
- writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
6258
- swapIntoPlace(tmpDir, outDir);
6259
- stagingStarted = false;
6430
+ writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
6431
+ swapIntoPlace(stagingDir, outDir);
6432
+ stagingDir = null;
6260
6433
  const warningsBeforeHook = manifest.warnings.length;
6261
6434
  runAfterGenerate(config, manifest);
6262
6435
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
@@ -6265,14 +6438,15 @@ function generateSnapshot(configInput, options) {
6265
6438
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6266
6439
  return manifest;
6267
6440
  } catch (err) {
6268
- if (stagingStarted) {
6441
+ if (stagingDir !== null) {
6269
6442
  try {
6270
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6443
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
6271
6444
  } catch {
6272
6445
  }
6273
6446
  }
6274
6447
  throw err;
6275
6448
  } finally {
6449
+ removeIfEmpty(tmpDir);
6276
6450
  releaseLock();
6277
6451
  }
6278
6452
  }
@@ -6281,5 +6455,5 @@ async function generateSnapshotAsync(configInput, options) {
6281
6455
  }
6282
6456
 
6283
6457
  export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, fenceForText, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
6284
- //# sourceMappingURL=chunk-FM4OOQPF.js.map
6285
- //# sourceMappingURL=chunk-FM4OOQPF.js.map
6458
+ //# sourceMappingURL=chunk-CY3XU5TS.js.map
6459
+ //# sourceMappingURL=chunk-CY3XU5TS.js.map