@kb-labs/quality-core 0.6.0

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,837 @@
1
+ import fs4 from 'fs';
2
+ import path4 from 'path';
3
+ import globby from 'globby';
4
+ import { execSync } from 'child_process';
5
+
6
+ // src/dead-code/scan-dead-files.ts
7
+ var CONFIG_FILE_PATTERNS = [
8
+ "tsup.config.ts",
9
+ "tsup.config.mts",
10
+ "tsup.config.js",
11
+ "tsup.config.mjs",
12
+ "tsup.bin.config.ts",
13
+ "tsup.lib.config.ts",
14
+ "vitest.config.ts",
15
+ "vitest.config.mts",
16
+ "vitest.config.js",
17
+ "jest.config.ts",
18
+ "jest.config.js",
19
+ "eslint.config.js",
20
+ "eslint.config.mjs",
21
+ "vitest.setup.ts",
22
+ "vitest.setup.js"
23
+ ];
24
+ async function collectEntryPoints(packageDir, packageJson) {
25
+ const entryFiles = /* @__PURE__ */ new Set();
26
+ const aliveByConvention = /* @__PURE__ */ new Set();
27
+ const warnings = [];
28
+ let failOpen = false;
29
+ collectPackageJsonEntries(packageDir, packageJson, entryFiles);
30
+ const tsupOk = await collectTsupEntries(packageDir, entryFiles, warnings);
31
+ if (!tsupOk) {
32
+ failOpen = true;
33
+ }
34
+ collectManifestHandlers(packageDir, entryFiles, warnings);
35
+ await collectDynamicImportTargets(packageDir, entryFiles);
36
+ await collectTestFiles(packageDir, aliveByConvention);
37
+ collectConfigFiles(packageDir, aliveByConvention);
38
+ if (entryFiles.size === 0) {
39
+ const defaultEntry = path4.join(packageDir, "src", "index.ts");
40
+ if (fs4.existsSync(defaultEntry)) {
41
+ entryFiles.add(defaultEntry);
42
+ warnings.push("No entry points found, using src/index.ts as default");
43
+ }
44
+ }
45
+ return { entryFiles, aliveByConvention, warnings, failOpen };
46
+ }
47
+ function distPathToSrcPath(distPath, packageDir) {
48
+ let normalized = distPath.replace(/^\.?\/?/, "");
49
+ if (normalized.startsWith("dist/") || normalized.startsWith("dist\\")) {
50
+ normalized = "src/" + normalized.slice(5);
51
+ }
52
+ normalized = normalized.replace(/\.d\.ts$/, ".ts").replace(/\.js$/, ".ts").replace(/\.mjs$/, ".ts").replace(/\.cjs$/, ".ts");
53
+ const absolute = path4.resolve(packageDir, normalized);
54
+ if (fs4.existsSync(absolute)) {
55
+ return absolute;
56
+ }
57
+ const indexPath = path4.join(absolute.replace(/\.ts$/, ""), "index.ts");
58
+ if (fs4.existsSync(indexPath)) {
59
+ return indexPath;
60
+ }
61
+ return null;
62
+ }
63
+ function collectPackageJsonEntries(packageDir, pkgJson, entryFiles, warnings) {
64
+ for (const field of ["main", "module", "types", "typings"]) {
65
+ const value = pkgJson[field];
66
+ if (typeof value === "string") {
67
+ const srcPath = distPathToSrcPath(value, packageDir);
68
+ if (srcPath) {
69
+ entryFiles.add(srcPath);
70
+ }
71
+ }
72
+ }
73
+ const bin = pkgJson["bin"];
74
+ if (typeof bin === "string") {
75
+ const srcPath = distPathToSrcPath(bin, packageDir);
76
+ if (srcPath) {
77
+ entryFiles.add(srcPath);
78
+ }
79
+ } else if (bin && typeof bin === "object") {
80
+ for (const value of Object.values(bin)) {
81
+ if (typeof value === "string") {
82
+ const srcPath = distPathToSrcPath(value, packageDir);
83
+ if (srcPath) {
84
+ entryFiles.add(srcPath);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ const exports$1 = pkgJson["exports"];
90
+ if (exports$1 && typeof exports$1 === "object") {
91
+ collectExportsEntries(
92
+ packageDir,
93
+ exports$1,
94
+ entryFiles);
95
+ }
96
+ }
97
+ function collectExportsEntries(packageDir, exports$1, entryFiles, _warnings) {
98
+ for (const [key, value] of Object.entries(exports$1)) {
99
+ if (key.includes("*")) {
100
+ continue;
101
+ }
102
+ const importPath = extractImportPath(value);
103
+ if (importPath) {
104
+ const srcPath = distPathToSrcPath(importPath, packageDir);
105
+ if (srcPath) {
106
+ entryFiles.add(srcPath);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ function extractImportPath(value) {
112
+ if (typeof value === "string") {
113
+ return value;
114
+ }
115
+ if (value && typeof value === "object") {
116
+ const obj = value;
117
+ for (const key of ["import", "default", "require"]) {
118
+ if (typeof obj[key] === "string") {
119
+ return obj[key];
120
+ }
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ async function collectTsupEntries(packageDir, entryFiles, warnings) {
126
+ const tsupConfigs = [
127
+ "tsup.config.ts",
128
+ "tsup.config.mts",
129
+ "tsup.config.js",
130
+ "tsup.config.mjs",
131
+ "tsup.bin.config.ts",
132
+ "tsup.lib.config.ts"
133
+ ];
134
+ let foundAny = false;
135
+ for (const configName of tsupConfigs) {
136
+ const configPath = path4.join(packageDir, configName);
137
+ if (!fs4.existsSync(configPath)) {
138
+ continue;
139
+ }
140
+ foundAny = true;
141
+ try {
142
+ const content = fs4.readFileSync(configPath, "utf-8");
143
+ const entries = parseTsupEntries(content);
144
+ for (const entry of entries) {
145
+ if (entry.includes("*") || entry.includes("{")) {
146
+ const expanded = await globby(entry, {
147
+ cwd: packageDir,
148
+ absolute: true
149
+ });
150
+ for (const file of expanded) {
151
+ entryFiles.add(file);
152
+ }
153
+ } else {
154
+ const absolute = path4.resolve(packageDir, entry);
155
+ if (fs4.existsSync(absolute)) {
156
+ entryFiles.add(absolute);
157
+ }
158
+ }
159
+ }
160
+ } catch {
161
+ warnings.push(`Failed to parse ${configName}`);
162
+ return false;
163
+ }
164
+ }
165
+ if (!foundAny) {
166
+ return true;
167
+ }
168
+ return true;
169
+ }
170
+ function parseTsupEntries(content) {
171
+ const entries = [];
172
+ const singleMatch = content.match(/entry\s*:\s*['"]([^'"]+)['"]/);
173
+ if (singleMatch && singleMatch[1]) {
174
+ entries.push(singleMatch[1]);
175
+ return entries;
176
+ }
177
+ const entryBlockMatch = content.match(
178
+ /entry\s*:\s*[\[{]([\s\S]*?)[\]}]/
179
+ );
180
+ if (entryBlockMatch && entryBlockMatch[1]) {
181
+ const block = entryBlockMatch[1];
182
+ const pathPattern = /['"]([^'"]*?src\/[^'"]+)['"]/g;
183
+ let match;
184
+ while ((match = pathPattern.exec(block)) !== null) {
185
+ if (match[1]) {
186
+ entries.push(match[1]);
187
+ }
188
+ }
189
+ return entries;
190
+ }
191
+ return entries;
192
+ }
193
+ function collectManifestHandlers(packageDir, entryFiles, warnings) {
194
+ const manifestPath = path4.join(packageDir, "src", "manifest.ts");
195
+ if (!fs4.existsSync(manifestPath)) {
196
+ return;
197
+ }
198
+ try {
199
+ const content = fs4.readFileSync(manifestPath, "utf-8");
200
+ const handlers = parseManifestHandlers(content);
201
+ for (const handlerPath of handlers) {
202
+ const pathOnly = handlerPath.split("#")[0] ?? handlerPath;
203
+ const srcRelative = "src/" + pathOnly.replace(/^\.\//, "").replace(/\.js$/, ".ts");
204
+ const absolute = path4.resolve(packageDir, srcRelative);
205
+ if (fs4.existsSync(absolute)) {
206
+ entryFiles.add(absolute);
207
+ }
208
+ }
209
+ entryFiles.add(manifestPath);
210
+ } catch {
211
+ warnings.push("Failed to parse manifest.ts");
212
+ }
213
+ }
214
+ function parseManifestHandlers(content) {
215
+ const handlers = [];
216
+ const seen = /* @__PURE__ */ new Set();
217
+ const pattern = /(?:handler|handlerPath)\s*:\s*['"]([^'"]+)['"]/g;
218
+ let match;
219
+ while ((match = pattern.exec(content)) !== null) {
220
+ const matchValue = match[1];
221
+ if (!matchValue) {
222
+ continue;
223
+ }
224
+ const raw = matchValue.split("#")[0] ?? matchValue;
225
+ if (!seen.has(raw)) {
226
+ seen.add(raw);
227
+ handlers.push(raw);
228
+ }
229
+ }
230
+ return handlers;
231
+ }
232
+ async function collectDynamicImportTargets(packageDir, entryFiles, _warnings) {
233
+ const srcDir = path4.join(packageDir, "src");
234
+ if (!fs4.existsSync(srcDir)) {
235
+ return;
236
+ }
237
+ const sourceFiles = await globby("**/*.{ts,tsx}", {
238
+ cwd: srcDir,
239
+ absolute: true,
240
+ ignore: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"]
241
+ });
242
+ const dynamicImportPattern = /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
243
+ for (const file of sourceFiles) {
244
+ try {
245
+ const content = fs4.readFileSync(file, "utf-8");
246
+ let match;
247
+ while ((match = dynamicImportPattern.exec(content)) !== null) {
248
+ const importPath = match[1];
249
+ if (!importPath) {
250
+ continue;
251
+ }
252
+ const resolved = resolveFilePath(importPath, file);
253
+ if (resolved) {
254
+ entryFiles.add(resolved);
255
+ }
256
+ }
257
+ } catch {
258
+ }
259
+ }
260
+ }
261
+ async function collectTestFiles(packageDir, aliveByConvention) {
262
+ const srcDir = path4.join(packageDir, "src");
263
+ if (!fs4.existsSync(srcDir)) {
264
+ return;
265
+ }
266
+ const testFiles = await globby(
267
+ ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**/*.ts"],
268
+ { cwd: srcDir, absolute: true }
269
+ );
270
+ for (const file of testFiles) {
271
+ aliveByConvention.add(file);
272
+ }
273
+ }
274
+ function collectConfigFiles(packageDir, aliveByConvention) {
275
+ for (const pattern of CONFIG_FILE_PATTERNS) {
276
+ const configPath = path4.join(packageDir, pattern);
277
+ if (fs4.existsSync(configPath)) {
278
+ aliveByConvention.add(configPath);
279
+ }
280
+ }
281
+ }
282
+ function resolveFilePath(importPath, fromFile) {
283
+ const dir = path4.dirname(fromFile);
284
+ const base = path4.resolve(dir, importPath);
285
+ const tsPath = base.replace(/\.js$/, ".ts");
286
+ if (fs4.existsSync(tsPath)) {
287
+ return tsPath;
288
+ }
289
+ if (fs4.existsSync(base)) {
290
+ return base;
291
+ }
292
+ if (fs4.existsSync(base + ".ts")) {
293
+ return base + ".ts";
294
+ }
295
+ if (fs4.existsSync(base + ".tsx")) {
296
+ return base + ".tsx";
297
+ }
298
+ if (fs4.existsSync(path4.join(base, "index.ts"))) {
299
+ return path4.join(base, "index.ts");
300
+ }
301
+ return null;
302
+ }
303
+ var IMPORT_PATTERNS = [
304
+ // Static imports: import X from 'module', import { X } from 'module', import 'module'
305
+ /import\s+(?:[\w*{}\n\r\t, ]+\s+from\s+)?['"]([^'"]+)['"]/g,
306
+ // Dynamic imports (string literal): import('./module'), import("./module")
307
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
308
+ // CommonJS require: require('./module'), require("./module")
309
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
310
+ // Re-exports: export { X } from 'module', export * from 'module'
311
+ /export\s+(?:[\w*{}\n\r\t, ]+\s+)?from\s+['"]([^'"]+)['"]/g
312
+ ];
313
+ var RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
314
+ function extractFileImports(content) {
315
+ const imports = [];
316
+ for (const pattern of IMPORT_PATTERNS) {
317
+ pattern.lastIndex = 0;
318
+ let match;
319
+ while ((match = pattern.exec(content)) !== null) {
320
+ const specifier = match[1];
321
+ if (specifier) {
322
+ imports.push(specifier);
323
+ }
324
+ }
325
+ }
326
+ return imports;
327
+ }
328
+ function isRelativeImport(specifier) {
329
+ return specifier.startsWith("./") || specifier.startsWith("../");
330
+ }
331
+ function resolveRelativeImport(specifier, sourceFile) {
332
+ const dir = path4.dirname(sourceFile);
333
+ const basePath = path4.resolve(dir, specifier);
334
+ if (specifier.endsWith(".js")) {
335
+ const tsPath = basePath.slice(0, -3) + ".ts";
336
+ if (fs4.existsSync(tsPath)) {
337
+ return tsPath;
338
+ }
339
+ const tsxPath = basePath.slice(0, -3) + ".tsx";
340
+ if (fs4.existsSync(tsxPath)) {
341
+ return tsxPath;
342
+ }
343
+ }
344
+ if (fs4.existsSync(basePath) && fs4.statSync(basePath).isFile()) {
345
+ return basePath;
346
+ }
347
+ for (const ext of RESOLVE_EXTENSIONS) {
348
+ const withExt = basePath + ext;
349
+ if (fs4.existsSync(withExt)) {
350
+ return withExt;
351
+ }
352
+ }
353
+ if (fs4.existsSync(basePath) && fs4.statSync(basePath).isDirectory()) {
354
+ for (const ext of RESOLVE_EXTENSIONS) {
355
+ const indexPath = path4.join(basePath, "index" + ext);
356
+ if (fs4.existsSync(indexPath)) {
357
+ return indexPath;
358
+ }
359
+ }
360
+ }
361
+ for (const ext of RESOLVE_EXTENSIONS) {
362
+ const indexPath = path4.join(basePath, "index" + ext);
363
+ if (fs4.existsSync(indexPath)) {
364
+ return indexPath;
365
+ }
366
+ }
367
+ return null;
368
+ }
369
+ function buildFileImportGraph(sourceFiles) {
370
+ const graph = /* @__PURE__ */ new Map();
371
+ for (const file of sourceFiles) {
372
+ const deps = /* @__PURE__ */ new Set();
373
+ graph.set(file, deps);
374
+ try {
375
+ const content = fs4.readFileSync(file, "utf-8");
376
+ const imports = extractFileImports(content);
377
+ for (const specifier of imports) {
378
+ if (!isRelativeImport(specifier)) {
379
+ continue;
380
+ }
381
+ const resolved = resolveRelativeImport(specifier, file);
382
+ if (resolved) {
383
+ deps.add(resolved);
384
+ }
385
+ }
386
+ } catch {
387
+ }
388
+ }
389
+ return graph;
390
+ }
391
+ function findReachableFiles(entryPoints, importGraph) {
392
+ const visited = /* @__PURE__ */ new Set();
393
+ const queue = [...entryPoints];
394
+ while (queue.length > 0) {
395
+ const current = queue.shift();
396
+ if (visited.has(current)) {
397
+ continue;
398
+ }
399
+ visited.add(current);
400
+ const deps = importGraph.get(current);
401
+ if (deps) {
402
+ for (const dep of deps) {
403
+ if (!visited.has(dep)) {
404
+ queue.push(dep);
405
+ }
406
+ }
407
+ }
408
+ }
409
+ return visited;
410
+ }
411
+ function countGraphEdges(graph) {
412
+ let count = 0;
413
+ for (const deps of graph.values()) {
414
+ count += deps.size;
415
+ }
416
+ return count;
417
+ }
418
+
419
+ // src/dead-code/scan-dead-files.ts
420
+ async function scanDeadFiles(rootDir, options) {
421
+ const startTime = Date.now();
422
+ const packages = findPackagesInMonorepo(rootDir, options?.packageFilter);
423
+ const results = [];
424
+ for (const pkg of packages) {
425
+ const result = await analyzePackage(pkg);
426
+ if (result) {
427
+ results.push(result);
428
+ }
429
+ }
430
+ const summary = calculateSummary(results);
431
+ return {
432
+ packages: results,
433
+ summary,
434
+ duration: Date.now() - startTime
435
+ };
436
+ }
437
+ function findPackagesInMonorepo(rootDir, filter) {
438
+ const packages = [];
439
+ if (!fs4.existsSync(rootDir)) {
440
+ return packages;
441
+ }
442
+ const entries = fs4.readdirSync(rootDir, { withFileTypes: true });
443
+ for (const entry of entries) {
444
+ if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
445
+ continue;
446
+ }
447
+ const repoPath = path4.join(rootDir, entry.name);
448
+ const packagesDir = path4.join(repoPath, "packages");
449
+ if (!fs4.existsSync(packagesDir)) {
450
+ continue;
451
+ }
452
+ const packageDirs = fs4.readdirSync(packagesDir, { withFileTypes: true });
453
+ for (const pkgDir of packageDirs) {
454
+ if (!pkgDir.isDirectory()) {
455
+ continue;
456
+ }
457
+ const packageJsonPath = path4.join(packagesDir, pkgDir.name, "package.json");
458
+ if (!fs4.existsSync(packageJsonPath)) {
459
+ continue;
460
+ }
461
+ try {
462
+ const pkgJson = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
463
+ const pkgName = pkgJson.name || pkgDir.name;
464
+ const packageDir = path4.join(packagesDir, pkgDir.name);
465
+ const srcDir = path4.join(packageDir, "src");
466
+ if (!fs4.existsSync(srcDir)) {
467
+ continue;
468
+ }
469
+ if (filter && !pkgName.includes(filter) && !pkgDir.name.includes(filter)) {
470
+ continue;
471
+ }
472
+ packages.push({ packageDir, packageJson: pkgJson, packageName: pkgName });
473
+ } catch {
474
+ }
475
+ }
476
+ }
477
+ return packages;
478
+ }
479
+ async function analyzePackage(pkg) {
480
+ const { packageDir, packageJson, packageName } = pkg;
481
+ const srcDir = path4.join(packageDir, "src");
482
+ const allSourceFiles = await globby("**/*.{ts,tsx}", {
483
+ cwd: srcDir,
484
+ absolute: true,
485
+ ignore: ["**/*.d.ts"]
486
+ });
487
+ if (allSourceFiles.length === 0) {
488
+ return null;
489
+ }
490
+ const {
491
+ entryFiles,
492
+ aliveByConvention,
493
+ warnings,
494
+ failOpen
495
+ } = await collectEntryPoints(packageDir, packageJson);
496
+ if (failOpen) {
497
+ return {
498
+ packageName,
499
+ packageDir,
500
+ totalFiles: allSourceFiles.length,
501
+ aliveFiles: allSourceFiles.length,
502
+ deadFiles: [],
503
+ entryPoints: [...entryFiles].map((f) => path4.relative(packageDir, f)),
504
+ graphEdgeCount: 0,
505
+ warnings: [...warnings, "FAIL-OPEN: All files treated as alive due to config parse errors"]
506
+ };
507
+ }
508
+ const importGraph = buildFileImportGraph(allSourceFiles);
509
+ const graphEdgeCount = countGraphEdges(importGraph);
510
+ const seeds = /* @__PURE__ */ new Set([...entryFiles, ...aliveByConvention]);
511
+ const reachable = findReachableFiles(seeds, importGraph);
512
+ const allAlive = /* @__PURE__ */ new Set([...reachable, ...aliveByConvention]);
513
+ const deadFiles = [];
514
+ for (const file of allSourceFiles) {
515
+ if (!allAlive.has(file)) {
516
+ const stats = safeFileStat(file);
517
+ deadFiles.push({
518
+ absolutePath: file,
519
+ relativePath: path4.relative(packageDir, file),
520
+ packageName,
521
+ packageDir,
522
+ sizeBytes: stats?.size ?? 0
523
+ });
524
+ }
525
+ }
526
+ deadFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
527
+ return {
528
+ packageName,
529
+ packageDir,
530
+ totalFiles: allSourceFiles.length,
531
+ aliveFiles: allSourceFiles.length - deadFiles.length,
532
+ deadFiles,
533
+ entryPoints: [...entryFiles].map((f) => path4.relative(packageDir, f)),
534
+ graphEdgeCount,
535
+ warnings
536
+ };
537
+ }
538
+ function calculateSummary(results, rootDir) {
539
+ let totalFiles = 0;
540
+ let totalDead = 0;
541
+ let totalDeadBytes = 0;
542
+ for (const pkg of results) {
543
+ totalFiles += pkg.totalFiles;
544
+ totalDead += pkg.deadFiles.length;
545
+ for (const deadFile of pkg.deadFiles) {
546
+ totalDeadBytes += deadFile.sizeBytes;
547
+ }
548
+ }
549
+ const emptyDirectories = findPotentialEmptyDirs(results);
550
+ return {
551
+ totalPackages: results.length,
552
+ totalFiles,
553
+ totalAlive: totalFiles - totalDead,
554
+ totalDead,
555
+ totalDeadBytes,
556
+ emptyDirectories
557
+ };
558
+ }
559
+ function findPotentialEmptyDirs(results, _rootDir) {
560
+ const emptyDirs = [];
561
+ for (const pkg of results) {
562
+ if (pkg.deadFiles.length === 0) {
563
+ continue;
564
+ }
565
+ const deadByDir = /* @__PURE__ */ new Map();
566
+ for (const deadFile of pkg.deadFiles) {
567
+ const dir = path4.dirname(deadFile.absolutePath);
568
+ deadByDir.set(dir, (deadByDir.get(dir) ?? 0) + 1);
569
+ }
570
+ for (const [dir, deadCount] of deadByDir) {
571
+ try {
572
+ const allFiles = fs4.readdirSync(dir);
573
+ if (allFiles.length === deadCount) {
574
+ emptyDirs.push(path4.relative(pkg.packageDir, dir));
575
+ }
576
+ } catch {
577
+ }
578
+ }
579
+ }
580
+ return emptyDirs;
581
+ }
582
+ function safeFileStat(filePath) {
583
+ try {
584
+ return fs4.statSync(filePath);
585
+ } catch {
586
+ return null;
587
+ }
588
+ }
589
+ var BACKUP_DIR = ".dead-code-backup";
590
+ async function removeDeadFiles(rootDir, scanResult, options) {
591
+ const dryRun = options?.dryRun ?? false;
592
+ const backupId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
593
+ const backupPath = path4.join(rootDir, BACKUP_DIR, backupId);
594
+ const gitSha = safeExec("git rev-parse HEAD", rootDir) ?? "unknown";
595
+ const gitBranch = safeExec("git rev-parse --abbrev-ref HEAD", rootDir) ?? "unknown";
596
+ const allDeadFiles = [];
597
+ for (const pkg of scanResult.packages) {
598
+ allDeadFiles.push(...pkg.deadFiles);
599
+ }
600
+ if (allDeadFiles.length === 0) {
601
+ return {
602
+ backupId,
603
+ backupPath,
604
+ filesRemoved: 0,
605
+ bytesRemoved: 0,
606
+ emptyDirsRemoved: 0,
607
+ exportsCleanedUp: 0,
608
+ manifest: {
609
+ id: backupId,
610
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
611
+ gitSha,
612
+ gitBranch,
613
+ removedFiles: [],
614
+ removedEmptyDirs: [],
615
+ cleanedExports: [],
616
+ totalFilesRemoved: 0,
617
+ totalBytesRemoved: 0
618
+ }
619
+ };
620
+ }
621
+ const manifest = {
622
+ id: backupId,
623
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
624
+ gitSha,
625
+ gitBranch,
626
+ removedFiles: allDeadFiles.map((f) => ({
627
+ originalPath: f.absolutePath,
628
+ backupPath: path4.relative(rootDir, f.absolutePath),
629
+ packageName: f.packageName,
630
+ sizeBytes: f.sizeBytes
631
+ })),
632
+ removedEmptyDirs: [],
633
+ cleanedExports: [],
634
+ totalFilesRemoved: allDeadFiles.length,
635
+ totalBytesRemoved: allDeadFiles.reduce((sum, f) => sum + f.sizeBytes, 0)
636
+ };
637
+ if (dryRun) {
638
+ return {
639
+ backupId,
640
+ backupPath,
641
+ filesRemoved: allDeadFiles.length,
642
+ bytesRemoved: manifest.totalBytesRemoved,
643
+ emptyDirsRemoved: scanResult.summary.emptyDirectories.length,
644
+ exportsCleanedUp: 0,
645
+ manifest
646
+ };
647
+ }
648
+ const filesDir = path4.join(backupPath, "files");
649
+ for (const deadFile of allDeadFiles) {
650
+ const relPath = path4.relative(rootDir, deadFile.absolutePath);
651
+ const destPath = path4.join(filesDir, relPath);
652
+ const destDir = path4.dirname(destPath);
653
+ fs4.mkdirSync(destDir, { recursive: true });
654
+ fs4.copyFileSync(deadFile.absolutePath, destPath);
655
+ }
656
+ fs4.writeFileSync(
657
+ path4.join(backupPath, "manifest.json"),
658
+ JSON.stringify(manifest, null, 2) + "\n"
659
+ );
660
+ for (const deadFile of allDeadFiles) {
661
+ fs4.unlinkSync(deadFile.absolutePath);
662
+ }
663
+ const removedDirs = [];
664
+ const deadFileDirs = new Set(allDeadFiles.map((f) => path4.dirname(f.absolutePath)));
665
+ for (const dir of deadFileDirs) {
666
+ removeEmptyDirsUpward(dir, rootDir, removedDirs);
667
+ }
668
+ manifest.removedEmptyDirs = removedDirs.map((d) => path4.relative(rootDir, d));
669
+ const deletedPaths = new Set(allDeadFiles.map((f) => f.absolutePath));
670
+ let exportsCleanedUp = 0;
671
+ for (const pkg of scanResult.packages) {
672
+ if (pkg.deadFiles.length === 0) {
673
+ continue;
674
+ }
675
+ const pkgJsonPath = path4.join(pkg.packageDir, "package.json");
676
+ const cleaned = cleanPackageJsonExports(pkgJsonPath, deletedPaths, pkg.packageDir);
677
+ if (cleaned.length > 0) {
678
+ manifest.cleanedExports.push({
679
+ packageJsonPath: path4.relative(rootDir, pkgJsonPath),
680
+ removedExportKeys: cleaned
681
+ });
682
+ exportsCleanedUp += cleaned.length;
683
+ }
684
+ }
685
+ fs4.writeFileSync(
686
+ path4.join(backupPath, "manifest.json"),
687
+ JSON.stringify(manifest, null, 2) + "\n"
688
+ );
689
+ return {
690
+ backupId,
691
+ backupPath,
692
+ filesRemoved: allDeadFiles.length,
693
+ bytesRemoved: manifest.totalBytesRemoved,
694
+ emptyDirsRemoved: removedDirs.length,
695
+ exportsCleanedUp,
696
+ manifest
697
+ };
698
+ }
699
+ async function restoreFromBackup(rootDir, backupId) {
700
+ const backupPath = path4.join(rootDir, BACKUP_DIR, backupId);
701
+ const manifestPath = path4.join(backupPath, "manifest.json");
702
+ if (!fs4.existsSync(manifestPath)) {
703
+ throw new Error(`Backup not found: ${backupId}`);
704
+ }
705
+ const manifest = JSON.parse(
706
+ fs4.readFileSync(manifestPath, "utf-8")
707
+ );
708
+ let restoredFiles = 0;
709
+ for (const entry of manifest.removedFiles) {
710
+ const backupFilePath = path4.join(backupPath, "files", entry.backupPath);
711
+ if (!fs4.existsSync(backupFilePath)) {
712
+ continue;
713
+ }
714
+ const parentDir = path4.dirname(entry.originalPath);
715
+ fs4.mkdirSync(parentDir, { recursive: true });
716
+ fs4.copyFileSync(backupFilePath, entry.originalPath);
717
+ restoredFiles++;
718
+ }
719
+ let restoredExports = 0;
720
+ for (const exportEntry of manifest.cleanedExports) {
721
+ const pkgJsonPath = path4.resolve(rootDir, exportEntry.packageJsonPath);
722
+ if (!fs4.existsSync(pkgJsonPath)) {
723
+ continue;
724
+ }
725
+ restoredExports += exportEntry.removedExportKeys.length;
726
+ }
727
+ return { restoredFiles, restoredExports };
728
+ }
729
+ function listBackups(rootDir) {
730
+ const backupDir = path4.join(rootDir, BACKUP_DIR);
731
+ if (!fs4.existsSync(backupDir)) {
732
+ return [];
733
+ }
734
+ const backups = [];
735
+ const entries = fs4.readdirSync(backupDir, { withFileTypes: true });
736
+ for (const entry of entries) {
737
+ if (!entry.isDirectory()) {
738
+ continue;
739
+ }
740
+ const manifestPath = path4.join(backupDir, entry.name, "manifest.json");
741
+ if (!fs4.existsSync(manifestPath)) {
742
+ continue;
743
+ }
744
+ try {
745
+ const manifest = JSON.parse(
746
+ fs4.readFileSync(manifestPath, "utf-8")
747
+ );
748
+ backups.push(manifest);
749
+ } catch {
750
+ }
751
+ }
752
+ backups.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
753
+ return backups;
754
+ }
755
+ function safeExec(cmd, cwd) {
756
+ try {
757
+ return execSync(cmd, { cwd, encoding: "utf-8", timeout: 5e3 }).trim();
758
+ } catch {
759
+ return null;
760
+ }
761
+ }
762
+ function removeEmptyDirsUpward(dir, rootBoundary, removed) {
763
+ let current = dir;
764
+ while (current !== rootBoundary && current.startsWith(rootBoundary)) {
765
+ try {
766
+ const entries = fs4.readdirSync(current);
767
+ if (entries.length > 0) {
768
+ break;
769
+ }
770
+ fs4.rmdirSync(current);
771
+ removed.push(current);
772
+ current = path4.dirname(current);
773
+ } catch {
774
+ break;
775
+ }
776
+ }
777
+ }
778
+ function cleanPackageJsonExports(packageJsonPath, deletedPaths, packageDir) {
779
+ if (!fs4.existsSync(packageJsonPath)) {
780
+ return [];
781
+ }
782
+ try {
783
+ const content = fs4.readFileSync(packageJsonPath, "utf-8");
784
+ const pkgJson = JSON.parse(content);
785
+ const removedKeys = [];
786
+ if (!pkgJson.exports || typeof pkgJson.exports !== "object") {
787
+ return [];
788
+ }
789
+ for (const [key, value] of Object.entries(pkgJson.exports)) {
790
+ if (key.includes("*")) {
791
+ continue;
792
+ }
793
+ const exportPath = extractExportImportPath(value);
794
+ if (!exportPath) {
795
+ continue;
796
+ }
797
+ const srcPath = distToSrcForExportCheck(exportPath, packageDir);
798
+ if (srcPath && deletedPaths.has(srcPath)) {
799
+ delete pkgJson.exports[key];
800
+ removedKeys.push(key);
801
+ }
802
+ }
803
+ if (removedKeys.length > 0) {
804
+ fs4.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
805
+ }
806
+ return removedKeys;
807
+ } catch {
808
+ return [];
809
+ }
810
+ }
811
+ function extractExportImportPath(value) {
812
+ if (typeof value === "string") {
813
+ return value;
814
+ }
815
+ if (value && typeof value === "object") {
816
+ const obj = value;
817
+ for (const key of ["import", "default", "require"]) {
818
+ if (typeof obj[key] === "string") {
819
+ return obj[key];
820
+ }
821
+ }
822
+ }
823
+ return null;
824
+ }
825
+ function distToSrcForExportCheck(distPath, packageDir) {
826
+ let normalized = distPath.replace(/^\.?\/?/, "");
827
+ if (!normalized.startsWith("dist/") && !normalized.startsWith("dist\\")) {
828
+ return null;
829
+ }
830
+ normalized = "src/" + normalized.slice(5);
831
+ normalized = normalized.replace(/\.d\.ts$/, ".ts").replace(/\.js$/, ".ts").replace(/\.mjs$/, ".ts").replace(/\.cjs$/, ".ts");
832
+ return path4.resolve(packageDir, normalized);
833
+ }
834
+
835
+ export { buildFileImportGraph, collectEntryPoints, distPathToSrcPath, extractFileImports, findReachableFiles, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveRelativeImport, restoreFromBackup, scanDeadFiles };
836
+ //# sourceMappingURL=index.js.map
837
+ //# sourceMappingURL=index.js.map