@kb-labs/quality-entry 2.14.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.
- package/README.md +120 -0
- package/dist/cli/commands/build-order.d.ts +16 -0
- package/dist/cli/commands/build-order.js +113 -0
- package/dist/cli/commands/build-order.js.map +1 -0
- package/dist/cli/commands/check-builds.d.ts +10 -0
- package/dist/cli/commands/check-builds.js +93 -0
- package/dist/cli/commands/check-builds.js.map +1 -0
- package/dist/cli/commands/check-tests.d.ts +10 -0
- package/dist/cli/commands/check-tests.js +114 -0
- package/dist/cli/commands/check-tests.js.map +1 -0
- package/dist/cli/commands/check-types.d.ts +10 -0
- package/dist/cli/commands/check-types.js +108 -0
- package/dist/cli/commands/check-types.js.map +1 -0
- package/dist/cli/commands/cycles.d.ts +17 -0
- package/dist/cli/commands/cycles.js +85 -0
- package/dist/cli/commands/cycles.js.map +1 -0
- package/dist/cli/commands/dead-code.d.ts +10 -0
- package/dist/cli/commands/dead-code.js +217 -0
- package/dist/cli/commands/dead-code.js.map +1 -0
- package/dist/cli/commands/fix-deps.d.ts +28 -0
- package/dist/cli/commands/fix-deps.js +389 -0
- package/dist/cli/commands/fix-deps.js.map +1 -0
- package/dist/cli/commands/flags.d.ts +344 -0
- package/dist/cli/commands/flags.js +298 -0
- package/dist/cli/commands/flags.js.map +1 -0
- package/dist/cli/commands/health.d.ts +10 -0
- package/dist/cli/commands/health.js +210 -0
- package/dist/cli/commands/health.js.map +1 -0
- package/dist/cli/commands/index.d.ts +14 -0
- package/dist/cli/commands/index.js +1747 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +10 -0
- package/dist/cli/commands/stats.js +282 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/visualize.d.ts +26 -0
- package/dist/cli/commands/visualize.js +210 -0
- package/dist/cli/commands/visualize.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +666 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +168 -0
- package/dist/manifest.js +666 -0
- package/dist/manifest.js.map +1 -0
- package/dist/rest/handlers/build-order-handler.d.ts +17 -0
- package/dist/rest/handlers/build-order-handler.js +28 -0
- package/dist/rest/handlers/build-order-handler.js.map +1 -0
- package/dist/rest/handlers/builds-handler.d.ts +12 -0
- package/dist/rest/handlers/builds-handler.js +17 -0
- package/dist/rest/handlers/builds-handler.js.map +1 -0
- package/dist/rest/handlers/cycles-handler.d.ts +13 -0
- package/dist/rest/handlers/cycles-handler.js +23 -0
- package/dist/rest/handlers/cycles-handler.js.map +1 -0
- package/dist/rest/handlers/dependencies-handler.d.ts +14 -0
- package/dist/rest/handlers/dependencies-handler.js +19 -0
- package/dist/rest/handlers/dependencies-handler.js.map +1 -0
- package/dist/rest/handlers/graph-handler.d.ts +30 -0
- package/dist/rest/handlers/graph-handler.js +155 -0
- package/dist/rest/handlers/graph-handler.js.map +1 -0
- package/dist/rest/handlers/health-handler.d.ts +15 -0
- package/dist/rest/handlers/health-handler.js +19 -0
- package/dist/rest/handlers/health-handler.js.map +1 -0
- package/dist/rest/handlers/stale-handler.d.ts +30 -0
- package/dist/rest/handlers/stale-handler.js +20 -0
- package/dist/rest/handlers/stale-handler.js.map +1 -0
- package/dist/rest/handlers/stats-handler.d.ts +16 -0
- package/dist/rest/handlers/stats-handler.js +29 -0
- package/dist/rest/handlers/stats-handler.js.map +1 -0
- package/dist/rest/handlers/tests-handler.d.ts +14 -0
- package/dist/rest/handlers/tests-handler.js +19 -0
- package/dist/rest/handlers/tests-handler.js.map +1 -0
- package/dist/rest/handlers/types-handler.d.ts +12 -0
- package/dist/rest/handlers/types-handler.js +17 -0
- package/dist/rest/handlers/types-handler.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1747 @@
|
|
|
1
|
+
import { defineCommand, useLoader } from '@kb-labs/sdk';
|
|
2
|
+
import { buildDependencyGraph, getBuildOrderForPackage, topologicalSort, findCircularDependencies, getReverseDependencies, getImpactAnalysis } from '@kb-labs/quality-core/graph';
|
|
3
|
+
import { CACHE_KEYS } from '@kb-labs/quality-contracts';
|
|
4
|
+
import { checkBuilds } from '@kb-labs/quality-core/builds';
|
|
5
|
+
import { runTests } from '@kb-labs/quality-core/tests';
|
|
6
|
+
import { analyzeTypes } from '@kb-labs/quality-core/types';
|
|
7
|
+
import { listBackups, restoreFromBackup, scanDeadFiles, removeDeadFiles } from '@kb-labs/quality-core/dead-code';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path3 from 'path';
|
|
10
|
+
|
|
11
|
+
// src/cli/commands/build-order.ts
|
|
12
|
+
var build_order_default = defineCommand({
|
|
13
|
+
id: "quality:build-order",
|
|
14
|
+
description: "Calculate build order using topological sort",
|
|
15
|
+
handler: {
|
|
16
|
+
async execute(ctx, input) {
|
|
17
|
+
const { ui } = ctx;
|
|
18
|
+
const flags = input.flags ?? input;
|
|
19
|
+
const graph = buildDependencyGraph(ctx.cwd);
|
|
20
|
+
let result;
|
|
21
|
+
if (flags.package) {
|
|
22
|
+
result = getBuildOrderForPackage(graph, flags.package);
|
|
23
|
+
} else {
|
|
24
|
+
result = topologicalSort(graph);
|
|
25
|
+
}
|
|
26
|
+
if (result.circular.length > 0) {
|
|
27
|
+
ui?.error?.(
|
|
28
|
+
`Found ${result.circular.length} circular dependencies. Build order cannot be determined.`
|
|
29
|
+
);
|
|
30
|
+
outputCircularDependencies(result.circular, ui);
|
|
31
|
+
return { exitCode: 1, result };
|
|
32
|
+
}
|
|
33
|
+
outputBuildOrder(result, flags, ui);
|
|
34
|
+
return { exitCode: 0, result };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
function outputBuildOrder(result, flags, ui) {
|
|
39
|
+
if (flags.json) {
|
|
40
|
+
ui?.json?.(result);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (flags.script) {
|
|
44
|
+
ui?.write?.("#!/bin/bash");
|
|
45
|
+
ui?.write?.("# Generated build script");
|
|
46
|
+
ui?.write?.("set -e");
|
|
47
|
+
ui?.write?.("");
|
|
48
|
+
for (let i = 0; i < result.layers.length; i++) {
|
|
49
|
+
const layer = result.layers[i];
|
|
50
|
+
if (!layer) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
ui?.write?.(`# Layer ${i + 1} (${layer.length} packages)`);
|
|
54
|
+
for (const pkg of layer) {
|
|
55
|
+
ui?.write?.(`pnpm --filter "${pkg}" run build`);
|
|
56
|
+
}
|
|
57
|
+
ui?.write?.("");
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const sections = [];
|
|
62
|
+
if (flags.layers) {
|
|
63
|
+
const layerItems = [];
|
|
64
|
+
for (let i = 0; i < result.layers.length; i++) {
|
|
65
|
+
const layer = result.layers[i];
|
|
66
|
+
if (!layer) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
layerItems.push(`Layer ${i + 1}: ${layer.length} packages (can build in parallel)`);
|
|
70
|
+
for (const pkg of layer) {
|
|
71
|
+
layerItems.push(` \u2022 ${pkg}`);
|
|
72
|
+
}
|
|
73
|
+
layerItems.push("");
|
|
74
|
+
}
|
|
75
|
+
sections.push({ header: "Build Layers", items: layerItems });
|
|
76
|
+
} else {
|
|
77
|
+
const orderItems = result.sorted.map((pkg, idx) => `${idx + 1}. ${pkg}`);
|
|
78
|
+
sections.push({ header: "Build Order", items: orderItems });
|
|
79
|
+
}
|
|
80
|
+
sections.push({
|
|
81
|
+
header: "Summary",
|
|
82
|
+
items: [
|
|
83
|
+
`Total packages: ${result.sorted.length}`,
|
|
84
|
+
`Build layers: ${result.layers.length}`,
|
|
85
|
+
`Circular dependencies: ${result.circular.length}`
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
const title = flags.package ? `\u{1F4E6} Build Order for ${flags.package}` : "\u{1F4E6} Monorepo Build Order";
|
|
89
|
+
ui?.success?.("Build order calculated successfully", {
|
|
90
|
+
title,
|
|
91
|
+
sections
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function outputCircularDependencies(cycles, ui) {
|
|
95
|
+
const sections = [];
|
|
96
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
97
|
+
const cycle = cycles[i];
|
|
98
|
+
if (!cycle) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
sections.push({
|
|
102
|
+
header: `Cycle ${i + 1}`,
|
|
103
|
+
items: cycle.map((pkg, idx) => {
|
|
104
|
+
if (idx === cycle.length - 1) {
|
|
105
|
+
const firstPkg = cycle[0];
|
|
106
|
+
return ` ${pkg} \u2192 ${firstPkg ?? "?"} (circular!)`;
|
|
107
|
+
}
|
|
108
|
+
return ` ${pkg} \u2192`;
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
ui?.error?.("Circular dependencies detected", {
|
|
113
|
+
title: "\u26A0\uFE0F Circular Dependencies",
|
|
114
|
+
sections
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
var check_builds_default = defineCommand({
|
|
118
|
+
id: "quality:check-builds",
|
|
119
|
+
description: "Check build status across monorepo",
|
|
120
|
+
handler: {
|
|
121
|
+
async execute(ctx, input) {
|
|
122
|
+
const { ui, platform } = ctx;
|
|
123
|
+
const flags = input.flags ?? input;
|
|
124
|
+
const cacheKey = `${CACHE_KEYS.BUILDS}:${flags.package || "all"}`;
|
|
125
|
+
if (!flags.refresh) {
|
|
126
|
+
const cached = await platform.cache.get(cacheKey);
|
|
127
|
+
if (cached) {
|
|
128
|
+
outputBuildCheck({ ...cached, cached: true }, flags, ui);
|
|
129
|
+
return { exitCode: cached.failing > 0 ? 1 : 0, result: cached };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const result = await checkBuilds(ctx.cwd, {
|
|
133
|
+
packageFilter: flags.package,
|
|
134
|
+
timeout: flags.timeout ? Number(flags.timeout) : 3e4
|
|
135
|
+
});
|
|
136
|
+
await platform.cache.set(cacheKey, result, 10 * 60 * 1e3);
|
|
137
|
+
await platform.analytics.track("quality:check-builds", {
|
|
138
|
+
totalPackages: result.totalPackages,
|
|
139
|
+
passing: result.passing,
|
|
140
|
+
failing: result.failing,
|
|
141
|
+
staleBuilds: result.staleBuilds.length,
|
|
142
|
+
duration: result.duration,
|
|
143
|
+
packageSpecific: !!flags.package
|
|
144
|
+
});
|
|
145
|
+
outputBuildCheck({ ...result, cached: false }, flags, ui);
|
|
146
|
+
return {
|
|
147
|
+
exitCode: result.failing > 0 ? 1 : 0,
|
|
148
|
+
result
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
function outputBuildCheck(result, flags, ui) {
|
|
154
|
+
if (flags.json) {
|
|
155
|
+
ui?.json?.(result);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const sections = [];
|
|
159
|
+
const statusIcon = result.failing === 0 ? "\u2705" : "\u274C";
|
|
160
|
+
const statusText = result.failing === 0 ? "All builds passing" : "Build failures detected";
|
|
161
|
+
const statusItems = [
|
|
162
|
+
`${statusIcon} ${statusText}`,
|
|
163
|
+
`Passing: ${result.passing} package(s)`,
|
|
164
|
+
result.failing > 0 ? `Failing: ${result.failing} package(s)` : null
|
|
165
|
+
].filter(Boolean);
|
|
166
|
+
sections.push({ header: "Build Status", items: statusItems });
|
|
167
|
+
if (result.failures.length > 0) {
|
|
168
|
+
const failureItems = [];
|
|
169
|
+
for (const failure of result.failures.slice(0, 5)) {
|
|
170
|
+
failureItems.push(`\u2022 ${failure.package}`);
|
|
171
|
+
const firstLine = failure.error.split("\n")[0];
|
|
172
|
+
failureItems.push(` ${firstLine?.substring(0, 80) || "Build failed"}`);
|
|
173
|
+
}
|
|
174
|
+
if (result.failures.length > 5) {
|
|
175
|
+
failureItems.push(`... and ${result.failures.length - 5} more failures`);
|
|
176
|
+
}
|
|
177
|
+
sections.push({ header: "\u274C Failed Packages", items: failureItems });
|
|
178
|
+
}
|
|
179
|
+
if (result.staleBuilds.length > 0) {
|
|
180
|
+
const staleItems = result.staleBuilds.slice(0, 5).map((s) => `\u2022 ${s.package}`);
|
|
181
|
+
if (result.staleBuilds.length > 5) {
|
|
182
|
+
staleItems.push(`... and ${result.staleBuilds.length - 5} more`);
|
|
183
|
+
}
|
|
184
|
+
sections.push({
|
|
185
|
+
header: "\u26A0\uFE0F Stale Builds (dist/ older than src/)",
|
|
186
|
+
items: staleItems
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const summaryItems = [
|
|
190
|
+
`Total packages: ${result.totalPackages}`,
|
|
191
|
+
`Duration: ${(result.duration / 1e3).toFixed(1)}s`,
|
|
192
|
+
result.cached ? "\u{1F4BE} Cached (use --refresh to recheck)" : "\u{1F504} Fresh check"
|
|
193
|
+
];
|
|
194
|
+
sections.push({ header: "Summary", items: summaryItems });
|
|
195
|
+
const title = result.failing === 0 ? "\u2705 All Builds Passing" : `\u274C ${result.failing} Build Failure(s) Detected`;
|
|
196
|
+
ui?.success?.("Build check completed", {
|
|
197
|
+
title,
|
|
198
|
+
sections
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
var check_tests_default = defineCommand({
|
|
202
|
+
id: "quality:check-tests",
|
|
203
|
+
description: "Run tests and track coverage across monorepo",
|
|
204
|
+
handler: {
|
|
205
|
+
async execute(ctx, input) {
|
|
206
|
+
const { ui, platform } = ctx;
|
|
207
|
+
const flags = input.flags ?? input;
|
|
208
|
+
const cacheKey = `${CACHE_KEYS.TESTS}:${flags.package || "all"}`;
|
|
209
|
+
if (!flags.refresh) {
|
|
210
|
+
const cached = await platform.cache.get(cacheKey);
|
|
211
|
+
if (cached) {
|
|
212
|
+
outputTestResults({ ...cached, cached: true }, flags, ui);
|
|
213
|
+
return { exitCode: cached.failing > 0 ? 1 : 0, result: cached };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const result = await runTests(ctx.cwd, {
|
|
217
|
+
packageFilter: flags.package,
|
|
218
|
+
timeout: flags.timeout ? Number(flags.timeout) : 6e4,
|
|
219
|
+
withCoverage: flags["with-coverage"],
|
|
220
|
+
coverageOnly: flags["coverage-only"]
|
|
221
|
+
});
|
|
222
|
+
await platform.cache.set(cacheKey, result, 5 * 60 * 1e3);
|
|
223
|
+
await platform.analytics.track("quality:check-tests", {
|
|
224
|
+
packageFilter: flags.package || "all",
|
|
225
|
+
totalPackages: result.totalPackages,
|
|
226
|
+
passing: result.passing,
|
|
227
|
+
failing: result.failing,
|
|
228
|
+
withCoverage: flags["with-coverage"],
|
|
229
|
+
cached: false
|
|
230
|
+
});
|
|
231
|
+
outputTestResults({ ...result, cached: false }, flags, ui);
|
|
232
|
+
return {
|
|
233
|
+
exitCode: result.failing > 0 ? 1 : 0,
|
|
234
|
+
result,
|
|
235
|
+
meta: { cached: false }
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
function outputTestResults(result, flags, ui) {
|
|
241
|
+
if (flags.json) {
|
|
242
|
+
ui?.json?.(result);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const sections = [];
|
|
246
|
+
const statusIcon = result.failing === 0 ? "\u2705" : "\u274C";
|
|
247
|
+
const statusText = result.failing === 0 ? "All tests passing" : "Test failures detected";
|
|
248
|
+
const statusItems = [
|
|
249
|
+
`${statusIcon} ${statusText}`,
|
|
250
|
+
`Total packages: ${result.totalPackages}`,
|
|
251
|
+
`Passing: ${result.passing}`,
|
|
252
|
+
result.failing > 0 ? `Failing: ${result.failing}` : null,
|
|
253
|
+
result.skipped > 0 ? `Skipped: ${result.skipped} (no test script)` : null
|
|
254
|
+
].filter(Boolean);
|
|
255
|
+
sections.push({ header: "Test Status", items: statusItems });
|
|
256
|
+
if (result.summary.totalTests > 0) {
|
|
257
|
+
const summaryItems2 = [
|
|
258
|
+
`Total tests: ${result.summary.totalTests}`,
|
|
259
|
+
`Passed: ${result.summary.passedTests}`,
|
|
260
|
+
result.summary.failedTests > 0 ? `Failed: ${result.summary.failedTests}` : null
|
|
261
|
+
].filter(Boolean);
|
|
262
|
+
sections.push({ header: "Test Summary", items: summaryItems2 });
|
|
263
|
+
}
|
|
264
|
+
if (result.failures.length > 0) {
|
|
265
|
+
const failureItems = [];
|
|
266
|
+
for (const failure of result.failures.slice(0, 5)) {
|
|
267
|
+
failureItems.push(`\u274C ${failure.package}`);
|
|
268
|
+
if (failure.failedTests && failure.totalTests) {
|
|
269
|
+
failureItems.push(` ${failure.failedTests}/${failure.totalTests} tests failed`);
|
|
270
|
+
}
|
|
271
|
+
const firstLine = failure.error.split("\n")[0];
|
|
272
|
+
failureItems.push(` ${firstLine?.substring(0, 80) || "Test failed"}`);
|
|
273
|
+
}
|
|
274
|
+
if (result.failures.length > 5) {
|
|
275
|
+
failureItems.push(`... and ${result.failures.length - 5} more failures`);
|
|
276
|
+
}
|
|
277
|
+
sections.push({ header: "Failed Packages", items: failureItems });
|
|
278
|
+
}
|
|
279
|
+
if (result.coverage.packages.length > 0) {
|
|
280
|
+
const coverageItems = [];
|
|
281
|
+
coverageItems.push(`Average coverage: ${result.coverage.avgCoverage.toFixed(1)}%`);
|
|
282
|
+
coverageItems.push("");
|
|
283
|
+
const topPackages = result.coverage.packages.sort((a, b) => b.lines - a.lines).slice(0, 5);
|
|
284
|
+
for (const pkg of topPackages) {
|
|
285
|
+
coverageItems.push(`${pkg.name}`);
|
|
286
|
+
coverageItems.push(
|
|
287
|
+
` Lines: ${pkg.lines.toFixed(1)}% | Statements: ${pkg.statements.toFixed(1)}% | Functions: ${pkg.functions.toFixed(1)}% | Branches: ${pkg.branches.toFixed(1)}%`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (result.coverage.packages.length > 5) {
|
|
291
|
+
coverageItems.push(`... and ${result.coverage.packages.length - 5} more packages`);
|
|
292
|
+
}
|
|
293
|
+
sections.push({ header: "Coverage", items: coverageItems });
|
|
294
|
+
}
|
|
295
|
+
const summaryItems = [
|
|
296
|
+
`Duration: ${(result.duration / 1e3).toFixed(1)}s`,
|
|
297
|
+
result.cached ? "\u{1F4BE} Cached (use --refresh to re-run)" : "\u{1F504} Fresh run"
|
|
298
|
+
];
|
|
299
|
+
sections.push({ header: "Summary", items: summaryItems });
|
|
300
|
+
const title = result.failing === 0 ? "\u2705 All Tests Passed" : `\u274C ${result.failing} Package(s) with Test Failures`;
|
|
301
|
+
ui?.success?.("Test run completed", {
|
|
302
|
+
title,
|
|
303
|
+
sections
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
var check_types_default = defineCommand({
|
|
307
|
+
id: "quality:check-types",
|
|
308
|
+
description: "Analyze TypeScript type safety across monorepo",
|
|
309
|
+
handler: {
|
|
310
|
+
async execute(ctx, input) {
|
|
311
|
+
const { ui, platform } = ctx;
|
|
312
|
+
const flags = input.flags ?? input;
|
|
313
|
+
const cacheKey = `${CACHE_KEYS.TYPE_ANALYSIS}:${flags.package || "all"}`;
|
|
314
|
+
if (!flags.refresh) {
|
|
315
|
+
const cached = await platform.cache.get(cacheKey);
|
|
316
|
+
if (cached) {
|
|
317
|
+
outputTypeAnalysis({ ...cached, cached: true }, flags, ui);
|
|
318
|
+
return { exitCode: cached.totalErrors > 0 ? 1 : 0, result: cached };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const result = await analyzeTypes(ctx.cwd, {
|
|
322
|
+
packageFilter: flags.package,
|
|
323
|
+
errorsOnly: flags["errors-only"]
|
|
324
|
+
});
|
|
325
|
+
await platform.cache.set(cacheKey, result, 10 * 60 * 1e3);
|
|
326
|
+
await platform.analytics.track("quality:check-types", {
|
|
327
|
+
packageFilter: flags.package || "all",
|
|
328
|
+
totalPackages: result.totalPackages,
|
|
329
|
+
totalErrors: result.totalErrors,
|
|
330
|
+
avgCoverage: result.avgCoverage,
|
|
331
|
+
cached: false
|
|
332
|
+
});
|
|
333
|
+
outputTypeAnalysis({ ...result, cached: false }, flags, ui);
|
|
334
|
+
return {
|
|
335
|
+
exitCode: result.totalErrors > 0 ? 1 : 0,
|
|
336
|
+
result,
|
|
337
|
+
meta: { cached: false }
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
function outputTypeAnalysis(result, flags, ui) {
|
|
343
|
+
if (flags.json) {
|
|
344
|
+
ui?.json?.(result);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const errorsOnly = flags["errors-only"] || false;
|
|
348
|
+
const sections = [];
|
|
349
|
+
const statusIcon = result.totalErrors === 0 ? "\u2705" : "\u274C";
|
|
350
|
+
const statusText = result.totalErrors === 0 ? "All packages passed type checks" : "Type errors detected";
|
|
351
|
+
const statusItems = [
|
|
352
|
+
`${statusIcon} ${statusText}`,
|
|
353
|
+
`Analyzed: ${result.totalPackages} package(s)`,
|
|
354
|
+
`Errors: ${result.totalErrors}`,
|
|
355
|
+
`Warnings: ${result.totalWarnings}`,
|
|
356
|
+
`Avg Coverage: ${result.avgCoverage.toFixed(1)}%`
|
|
357
|
+
];
|
|
358
|
+
sections.push({ header: "Type Safety Status", items: statusItems });
|
|
359
|
+
if (result.packagesWithErrors > 0) {
|
|
360
|
+
const errorItems = [];
|
|
361
|
+
const packagesToShow = result.packages.filter((pkg) => !errorsOnly || pkg.errors > 0).slice(0, 10);
|
|
362
|
+
for (const pkg of packagesToShow) {
|
|
363
|
+
const status = pkg.errors > 0 ? "\u274C" : "\u2705";
|
|
364
|
+
errorItems.push(`${status} ${pkg.name}`);
|
|
365
|
+
const details = [
|
|
366
|
+
pkg.errors > 0 ? `${pkg.errors} error(s)` : null,
|
|
367
|
+
pkg.warnings > 0 ? `${pkg.warnings} warning(s)` : null,
|
|
368
|
+
`${pkg.coverage.toFixed(1)}% coverage`,
|
|
369
|
+
pkg.anyCount > 0 ? `${pkg.anyCount} any` : null,
|
|
370
|
+
pkg.tsIgnoreCount > 0 ? `${pkg.tsIgnoreCount} @ts-ignore` : null
|
|
371
|
+
].filter(Boolean).join(", ");
|
|
372
|
+
errorItems.push(` ${details}`);
|
|
373
|
+
}
|
|
374
|
+
if (result.packages.length > 10) {
|
|
375
|
+
errorItems.push(`... and ${result.packages.length - 10} more packages`);
|
|
376
|
+
}
|
|
377
|
+
sections.push({
|
|
378
|
+
header: `Packages with Type Errors (${result.packagesWithErrors})`,
|
|
379
|
+
items: errorItems
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const excellent = result.packages.filter((p) => p.coverage >= 90).length;
|
|
383
|
+
const good = result.packages.filter((p) => p.coverage >= 70 && p.coverage < 90).length;
|
|
384
|
+
const poor = result.packages.filter((p) => p.coverage < 70).length;
|
|
385
|
+
if (result.packages.length > 0) {
|
|
386
|
+
const coverageItems = [
|
|
387
|
+
`\u2705 Excellent (\u226590%): ${excellent} package(s)`,
|
|
388
|
+
`\u26A0\uFE0F Good (70-90%): ${good} package(s)`,
|
|
389
|
+
poor > 0 ? `\u274C Poor (<70%): ${poor} package(s)` : null
|
|
390
|
+
].filter(Boolean);
|
|
391
|
+
sections.push({ header: "Type Coverage Distribution", items: coverageItems });
|
|
392
|
+
}
|
|
393
|
+
const summaryItems = [
|
|
394
|
+
`Total packages: ${result.totalPackages}`,
|
|
395
|
+
`Duration: ${(result.duration / 1e3).toFixed(1)}s`,
|
|
396
|
+
result.cached ? "\u{1F4BE} Cached (use --refresh to recheck)" : "\u{1F504} Fresh analysis"
|
|
397
|
+
];
|
|
398
|
+
sections.push({ header: "Summary", items: summaryItems });
|
|
399
|
+
const title = result.totalErrors === 0 ? "\u2705 All Type Checks Passed" : `\u274C ${result.packagesWithErrors} Package(s) with Type Errors`;
|
|
400
|
+
ui?.success?.("Type analysis completed", {
|
|
401
|
+
title,
|
|
402
|
+
sections
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
var cycles_default = defineCommand({
|
|
406
|
+
id: "quality:cycles",
|
|
407
|
+
description: "Detect circular dependencies in monorepo",
|
|
408
|
+
handler: {
|
|
409
|
+
async execute(ctx, input) {
|
|
410
|
+
const { ui } = ctx;
|
|
411
|
+
const flags = input.flags ?? input;
|
|
412
|
+
const graph = buildDependencyGraph(ctx.cwd);
|
|
413
|
+
const cycles = findCircularDependencies(graph);
|
|
414
|
+
outputCycles(cycles, flags, ui);
|
|
415
|
+
return {
|
|
416
|
+
exitCode: cycles.length > 0 ? 1 : 0,
|
|
417
|
+
cycles
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
function outputCycles(cycles, flags, ui) {
|
|
423
|
+
if (flags.json) {
|
|
424
|
+
ui?.json?.({ cycles, count: cycles.length });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (cycles.length === 0) {
|
|
428
|
+
ui?.success?.("No circular dependencies found", {
|
|
429
|
+
title: "\u2705 Circular Dependencies Check",
|
|
430
|
+
sections: [
|
|
431
|
+
{
|
|
432
|
+
header: "Result",
|
|
433
|
+
items: ["\u2705 No circular dependencies detected"]
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const sections = [];
|
|
440
|
+
sections.push({
|
|
441
|
+
header: "Summary",
|
|
442
|
+
items: [
|
|
443
|
+
`\u26A0\uFE0F Found ${cycles.length} circular dependency chain(s)`,
|
|
444
|
+
"These must be broken to enable clean builds"
|
|
445
|
+
]
|
|
446
|
+
});
|
|
447
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
448
|
+
const cycle = cycles[i];
|
|
449
|
+
if (!cycle) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const cycleItems = [];
|
|
453
|
+
for (let j = 0; j < cycle.length; j++) {
|
|
454
|
+
const pkg = cycle[j];
|
|
455
|
+
if (j === cycle.length - 1) {
|
|
456
|
+
cycleItems.push(` ${j + 1}. ${pkg}`);
|
|
457
|
+
const firstPkg = cycle[0];
|
|
458
|
+
cycleItems.push(` ${j + 2}. ${firstPkg ?? "?"} \u2B05 circular!`);
|
|
459
|
+
} else {
|
|
460
|
+
cycleItems.push(` ${j + 1}. ${pkg}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
sections.push({
|
|
464
|
+
header: `Cycle ${i + 1} (${cycle.length} packages)`,
|
|
465
|
+
items: cycleItems
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
sections.push({
|
|
469
|
+
header: "Recommendations",
|
|
470
|
+
items: [
|
|
471
|
+
"1. Extract shared code into a new package",
|
|
472
|
+
"2. Move one dependency to devDependencies",
|
|
473
|
+
"3. Refactor to remove bidirectional dependencies",
|
|
474
|
+
"4. Use dependency injection patterns"
|
|
475
|
+
]
|
|
476
|
+
});
|
|
477
|
+
ui?.error?.("Circular dependencies detected", {
|
|
478
|
+
title: `\u26A0\uFE0F Circular Dependencies Detected (${cycles.length})`,
|
|
479
|
+
sections
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
var dead_code_default = defineCommand({
|
|
483
|
+
id: "quality:dead-code",
|
|
484
|
+
description: "Detect and optionally remove dead (unreachable) source files",
|
|
485
|
+
handler: {
|
|
486
|
+
async execute(ctx, input) {
|
|
487
|
+
const { ui, platform } = ctx;
|
|
488
|
+
const flags = input.flags ?? input;
|
|
489
|
+
if (flags["list-backups"]) {
|
|
490
|
+
const backups = listBackups(ctx.cwd);
|
|
491
|
+
if (flags.json) {
|
|
492
|
+
ui?.json?.(backups);
|
|
493
|
+
} else {
|
|
494
|
+
outputBackupList(backups, ui);
|
|
495
|
+
}
|
|
496
|
+
return { exitCode: 0, result: backups };
|
|
497
|
+
}
|
|
498
|
+
if (flags.restore) {
|
|
499
|
+
try {
|
|
500
|
+
const result2 = await restoreFromBackup(ctx.cwd, flags.restore);
|
|
501
|
+
if (flags.json) {
|
|
502
|
+
ui?.json?.(result2);
|
|
503
|
+
} else {
|
|
504
|
+
ui?.success?.(`Restored ${result2.restoredFiles} file(s) from backup ${flags.restore}`, {});
|
|
505
|
+
if (result2.restoredExports > 0) {
|
|
506
|
+
ui?.info?.(`Note: ${result2.restoredExports} package.json export(s) were cleaned. You may need to rebuild.`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { exitCode: 0 };
|
|
510
|
+
} catch (err) {
|
|
511
|
+
if (flags.json) {
|
|
512
|
+
ui?.json?.({ error: err.message });
|
|
513
|
+
} else {
|
|
514
|
+
ui?.error?.(err.message);
|
|
515
|
+
}
|
|
516
|
+
return { exitCode: 1 };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const cacheKey = `${CACHE_KEYS.DEAD_CODE}:${flags.package || "all"}`;
|
|
520
|
+
if (!flags.refresh && !flags["auto-remove"]) {
|
|
521
|
+
const cached = await platform.cache.get(cacheKey);
|
|
522
|
+
if (cached) {
|
|
523
|
+
outputDeadCodeReport({ ...cached, cached: true }, flags, ui);
|
|
524
|
+
return { exitCode: cached.summary.totalDead > 0 ? 1 : 0, result: cached };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const result = await scanDeadFiles(ctx.cwd, {
|
|
528
|
+
packageFilter: flags.package,
|
|
529
|
+
verbose: flags.verbose
|
|
530
|
+
});
|
|
531
|
+
await platform.cache.set(cacheKey, result, 5 * 60 * 1e3);
|
|
532
|
+
await platform.analytics.track("quality:dead-code", {
|
|
533
|
+
totalPackages: result.summary.totalPackages,
|
|
534
|
+
totalFiles: result.summary.totalFiles,
|
|
535
|
+
totalDead: result.summary.totalDead,
|
|
536
|
+
totalDeadBytes: result.summary.totalDeadBytes,
|
|
537
|
+
duration: result.duration,
|
|
538
|
+
autoRemove: !!flags["auto-remove"],
|
|
539
|
+
dryRun: !!flags["dry-run"]
|
|
540
|
+
});
|
|
541
|
+
if (flags["auto-remove"]) {
|
|
542
|
+
if (result.summary.totalDead === 0) {
|
|
543
|
+
if (flags.json) {
|
|
544
|
+
ui?.json?.({ message: "No dead files found", ...result });
|
|
545
|
+
} else {
|
|
546
|
+
ui?.success?.("No dead files found. Nothing to remove.", {});
|
|
547
|
+
}
|
|
548
|
+
return { exitCode: 0, result };
|
|
549
|
+
}
|
|
550
|
+
const removalResult = await removeDeadFiles(ctx.cwd, result, {
|
|
551
|
+
dryRun: flags["dry-run"]
|
|
552
|
+
});
|
|
553
|
+
if (flags.json) {
|
|
554
|
+
ui?.json?.(removalResult);
|
|
555
|
+
} else {
|
|
556
|
+
outputRemovalReport(removalResult, flags, ui);
|
|
557
|
+
}
|
|
558
|
+
return { exitCode: 0, result: removalResult };
|
|
559
|
+
}
|
|
560
|
+
outputDeadCodeReport({ ...result, cached: false }, flags, ui);
|
|
561
|
+
return {
|
|
562
|
+
exitCode: result.summary.totalDead > 0 ? 1 : 0,
|
|
563
|
+
result
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
function outputDeadCodeReport(result, flags, ui) {
|
|
569
|
+
if (flags.json) {
|
|
570
|
+
ui?.json?.(result);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const sections = [];
|
|
574
|
+
const packagesWithDead = result.packages.filter((p) => p.deadFiles.length > 0);
|
|
575
|
+
if (packagesWithDead.length > 0) {
|
|
576
|
+
const items = [];
|
|
577
|
+
for (const pkg of packagesWithDead) {
|
|
578
|
+
items.push(`${pkg.packageName} (${pkg.deadFiles.length} dead file${pkg.deadFiles.length === 1 ? "" : "s"})`);
|
|
579
|
+
for (const dead of pkg.deadFiles.slice(0, 5)) {
|
|
580
|
+
const sizeKb = (dead.sizeBytes / 1024).toFixed(1);
|
|
581
|
+
items.push(` ${dead.relativePath} (${sizeKb} KB)`);
|
|
582
|
+
}
|
|
583
|
+
if (pkg.deadFiles.length > 5) {
|
|
584
|
+
items.push(` ... and ${pkg.deadFiles.length - 5} more`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
sections.push({ header: "Dead Files", items });
|
|
588
|
+
}
|
|
589
|
+
if (flags.verbose) {
|
|
590
|
+
for (const pkg of result.packages) {
|
|
591
|
+
if (pkg.entryPoints.length > 0) {
|
|
592
|
+
const items = pkg.entryPoints.slice(0, 10).map((ep) => ` ${ep}`);
|
|
593
|
+
if (pkg.entryPoints.length > 10) {
|
|
594
|
+
items.push(` ... and ${pkg.entryPoints.length - 10} more`);
|
|
595
|
+
}
|
|
596
|
+
sections.push({ header: `Entry Points: ${pkg.packageName}`, items });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const allWarnings = result.packages.flatMap((p) => p.warnings);
|
|
601
|
+
if (allWarnings.length > 0) {
|
|
602
|
+
sections.push({
|
|
603
|
+
header: "Warnings",
|
|
604
|
+
items: allWarnings.slice(0, 10)
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const totalDeadKb = (result.summary.totalDeadBytes / 1024).toFixed(1);
|
|
608
|
+
const summaryItems = [
|
|
609
|
+
`Packages scanned: ${result.summary.totalPackages}`,
|
|
610
|
+
`Total source files: ${result.summary.totalFiles}`,
|
|
611
|
+
`Alive files: ${result.summary.totalAlive}`,
|
|
612
|
+
`Dead files: ${result.summary.totalDead}`,
|
|
613
|
+
result.summary.totalDead > 0 ? `Dead code size: ${totalDeadKb} KB` : null,
|
|
614
|
+
result.summary.emptyDirectories.length > 0 ? `Empty directories: ${result.summary.emptyDirectories.length}` : null,
|
|
615
|
+
`Duration: ${(result.duration / 1e3).toFixed(1)}s`,
|
|
616
|
+
result.cached ? "(cached \u2014 use --refresh to rescan)" : null
|
|
617
|
+
].filter(Boolean);
|
|
618
|
+
sections.push({ header: "Summary", items: summaryItems });
|
|
619
|
+
if (result.summary.totalDead > 0) {
|
|
620
|
+
sections.push({
|
|
621
|
+
header: "Next Steps",
|
|
622
|
+
items: [
|
|
623
|
+
"Run with --auto-remove --dry-run to preview removal",
|
|
624
|
+
"Run with --auto-remove to remove and create backup",
|
|
625
|
+
"Run with --verbose to see entry points"
|
|
626
|
+
]
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const title = result.summary.totalDead === 0 ? "No dead files found" : `${result.summary.totalDead} dead file(s) found`;
|
|
630
|
+
ui?.success?.("Dead code analysis completed", { title, sections });
|
|
631
|
+
}
|
|
632
|
+
function outputRemovalReport(result, flags, ui) {
|
|
633
|
+
const isDryRun = flags["dry-run"];
|
|
634
|
+
const prefix = isDryRun ? "[DRY RUN] Would" : "Successfully";
|
|
635
|
+
const sections = [];
|
|
636
|
+
const items = [
|
|
637
|
+
`${prefix} remove ${result.filesRemoved} file(s)`,
|
|
638
|
+
`${prefix} free ${(result.bytesRemoved / 1024).toFixed(1)} KB`
|
|
639
|
+
];
|
|
640
|
+
if (result.emptyDirsRemoved > 0) {
|
|
641
|
+
items.push(`${prefix} remove ${result.emptyDirsRemoved} empty director${result.emptyDirsRemoved === 1 ? "y" : "ies"}`);
|
|
642
|
+
}
|
|
643
|
+
if (result.exportsCleanedUp > 0) {
|
|
644
|
+
items.push(`${prefix} clean ${result.exportsCleanedUp} package.json export(s)`);
|
|
645
|
+
}
|
|
646
|
+
sections.push({ header: isDryRun ? "Preview" : "Removal Complete", items });
|
|
647
|
+
if (!isDryRun) {
|
|
648
|
+
sections.push({
|
|
649
|
+
header: "Backup",
|
|
650
|
+
items: [
|
|
651
|
+
`Backup ID: ${result.backupId}`,
|
|
652
|
+
`To restore: pnpm kb quality:dead-code --restore ${result.backupId}`
|
|
653
|
+
]
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const fileItems = [];
|
|
657
|
+
for (const file of result.manifest.removedFiles.slice(0, 10)) {
|
|
658
|
+
const sizeKb = (file.sizeBytes / 1024).toFixed(1);
|
|
659
|
+
fileItems.push(`${file.backupPath} (${sizeKb} KB)`);
|
|
660
|
+
}
|
|
661
|
+
if (result.manifest.removedFiles.length > 10) {
|
|
662
|
+
fileItems.push(`... and ${result.manifest.removedFiles.length - 10} more`);
|
|
663
|
+
}
|
|
664
|
+
sections.push({ header: "Files", items: fileItems });
|
|
665
|
+
const title = isDryRun ? `[DRY RUN] Would remove ${result.filesRemoved} file(s)` : `Removed ${result.filesRemoved} file(s)`;
|
|
666
|
+
ui?.success?.(title, { title, sections });
|
|
667
|
+
}
|
|
668
|
+
function outputBackupList(backups, ui) {
|
|
669
|
+
if (backups.length === 0) {
|
|
670
|
+
ui?.info?.("No backups found.");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const sections = [];
|
|
674
|
+
for (const backup of backups) {
|
|
675
|
+
const date = new Date(backup.createdAt).toLocaleString();
|
|
676
|
+
const items = [
|
|
677
|
+
`Date: ${date}`,
|
|
678
|
+
`Git: ${backup.gitBranch} @ ${backup.gitSha.slice(0, 8)}`,
|
|
679
|
+
`Files: ${backup.totalFilesRemoved}`,
|
|
680
|
+
`Size: ${(backup.totalBytesRemoved / 1024).toFixed(1)} KB`,
|
|
681
|
+
`Restore: pnpm kb quality:dead-code --restore ${backup.id}`
|
|
682
|
+
];
|
|
683
|
+
sections.push({ header: `Backup: ${backup.id}`, items });
|
|
684
|
+
}
|
|
685
|
+
ui?.success?.(`${backups.length} backup(s) found`, {
|
|
686
|
+
title: "Dead Code Backups",
|
|
687
|
+
sections
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
var fix_deps_default = defineCommand({
|
|
691
|
+
id: "quality:fix-deps",
|
|
692
|
+
description: "Auto-fix dependency issues",
|
|
693
|
+
handler: {
|
|
694
|
+
async execute(ctx, input) {
|
|
695
|
+
const { ui, platform } = ctx;
|
|
696
|
+
const flags = input.flags ?? input;
|
|
697
|
+
const dryRun = flags["dry-run"] ?? false;
|
|
698
|
+
const removeUnused = flags["remove-unused"] || flags.all;
|
|
699
|
+
const addMissing = flags["add-missing"] || flags.all;
|
|
700
|
+
const alignVersions = flags["align-versions"] || flags.all;
|
|
701
|
+
const showStats = flags.stats;
|
|
702
|
+
if (showStats) {
|
|
703
|
+
const stats = await getDependencyStats(ctx.cwd);
|
|
704
|
+
outputStats(stats, flags, ui);
|
|
705
|
+
return { exitCode: 0 };
|
|
706
|
+
}
|
|
707
|
+
if (!removeUnused && !addMissing && !alignVersions) {
|
|
708
|
+
ui?.error?.("No fix options specified. Use --remove-unused, --add-missing, --align-versions, or --all");
|
|
709
|
+
return { exitCode: 1 };
|
|
710
|
+
}
|
|
711
|
+
const loader = useLoader("Scanning packages...");
|
|
712
|
+
loader.start();
|
|
713
|
+
const result = {
|
|
714
|
+
packagesScanned: 0,
|
|
715
|
+
removedDeps: [],
|
|
716
|
+
addedDeps: [],
|
|
717
|
+
alignedDeps: [],
|
|
718
|
+
dryRun
|
|
719
|
+
};
|
|
720
|
+
const packages = findPackages(ctx.cwd);
|
|
721
|
+
result.packagesScanned = packages.length;
|
|
722
|
+
loader.update({ text: `Scanned ${packages.length} packages` });
|
|
723
|
+
if (alignVersions) {
|
|
724
|
+
loader.update({ text: "Analyzing duplicate dependencies..." });
|
|
725
|
+
const aligned = await alignDuplicateVersions(packages, dryRun);
|
|
726
|
+
result.alignedDeps = aligned;
|
|
727
|
+
}
|
|
728
|
+
if (addMissing) {
|
|
729
|
+
loader.update({ text: "Checking for missing workspace dependencies..." });
|
|
730
|
+
const added = await addMissingWorkspaceDeps(packages, dryRun);
|
|
731
|
+
result.addedDeps = added;
|
|
732
|
+
}
|
|
733
|
+
if (removeUnused) {
|
|
734
|
+
loader.update({ text: "Checking for unused dependencies..." });
|
|
735
|
+
const removed = await removeUnusedDeps(packages, dryRun);
|
|
736
|
+
result.removedDeps = removed;
|
|
737
|
+
}
|
|
738
|
+
loader.succeed("Dependency analysis completed");
|
|
739
|
+
await platform.analytics.track("quality:fix-deps", {
|
|
740
|
+
dryRun,
|
|
741
|
+
packagesScanned: result.packagesScanned,
|
|
742
|
+
removedCount: result.removedDeps.length,
|
|
743
|
+
addedCount: result.addedDeps.length,
|
|
744
|
+
alignedCount: result.alignedDeps.length
|
|
745
|
+
});
|
|
746
|
+
outputResults(result, flags, ui);
|
|
747
|
+
return {
|
|
748
|
+
exitCode: 0,
|
|
749
|
+
result
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
function findPackages(rootDir) {
|
|
755
|
+
const packages = [];
|
|
756
|
+
if (!fs.existsSync(rootDir)) {
|
|
757
|
+
return packages;
|
|
758
|
+
}
|
|
759
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
760
|
+
for (const entry of entries) {
|
|
761
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const repoPath = path3.join(rootDir, entry.name);
|
|
765
|
+
const packagesDir = path3.join(repoPath, "packages");
|
|
766
|
+
if (!fs.existsSync(packagesDir)) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
770
|
+
for (const pkgDir of packageDirs) {
|
|
771
|
+
if (!pkgDir.isDirectory()) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const packageJsonPath = path3.join(packagesDir, pkgDir.name, "package.json");
|
|
775
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
776
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
777
|
+
packages.push({
|
|
778
|
+
name: packageJson.name || pkgDir.name,
|
|
779
|
+
path: packageJsonPath,
|
|
780
|
+
json: packageJson
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return packages;
|
|
786
|
+
}
|
|
787
|
+
async function getDependencyStats(rootDir) {
|
|
788
|
+
const packages = findPackages(rootDir);
|
|
789
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const pkg of packages) {
|
|
791
|
+
const deps = {
|
|
792
|
+
...pkg.json.dependencies,
|
|
793
|
+
...pkg.json.devDependencies
|
|
794
|
+
};
|
|
795
|
+
for (const [dep, version] of Object.entries(deps)) {
|
|
796
|
+
if (!allDeps.has(dep)) {
|
|
797
|
+
allDeps.set(dep, /* @__PURE__ */ new Set());
|
|
798
|
+
}
|
|
799
|
+
allDeps.get(dep).add(version);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const duplicates = Array.from(allDeps.entries()).filter(([_, versions]) => versions.size > 1).filter(([dep]) => !dep.startsWith("@kb-labs/"));
|
|
803
|
+
const topUsed = Array.from(allDeps.entries()).map(([name, versions]) => ({ name, count: versions.size > 1 ? versions.size : 1 })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
804
|
+
return {
|
|
805
|
+
totalPackages: packages.length,
|
|
806
|
+
totalDeps: allDeps.size,
|
|
807
|
+
duplicates: duplicates.length,
|
|
808
|
+
topUsed
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
async function alignDuplicateVersions(packages, dryRun) {
|
|
812
|
+
const aligned = [];
|
|
813
|
+
const depVersions = /* @__PURE__ */ new Map();
|
|
814
|
+
for (const pkg of packages) {
|
|
815
|
+
const deps = {
|
|
816
|
+
...pkg.json.dependencies,
|
|
817
|
+
...pkg.json.devDependencies
|
|
818
|
+
};
|
|
819
|
+
for (const [dep, version] of Object.entries(deps)) {
|
|
820
|
+
if (dep.startsWith("@kb-labs/")) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (!depVersions.has(dep)) {
|
|
824
|
+
depVersions.set(dep, /* @__PURE__ */ new Map());
|
|
825
|
+
}
|
|
826
|
+
const versions = depVersions.get(dep);
|
|
827
|
+
if (!versions.has(version)) {
|
|
828
|
+
versions.set(version, /* @__PURE__ */ new Set());
|
|
829
|
+
}
|
|
830
|
+
versions.get(version).add(pkg.name);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const [dep, versions] of depVersions.entries()) {
|
|
834
|
+
if (versions.size <= 1) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const versionCounts = Array.from(versions.entries()).map(([ver, pkgs]) => ({
|
|
838
|
+
version: ver,
|
|
839
|
+
count: pkgs.size,
|
|
840
|
+
packages: Array.from(pkgs)
|
|
841
|
+
}));
|
|
842
|
+
versionCounts.sort((a, b) => b.count - a.count);
|
|
843
|
+
const targetVersion = versionCounts[0]?.version;
|
|
844
|
+
if (!targetVersion) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
for (const { version, packages: pkgNames } of versionCounts.slice(1)) {
|
|
848
|
+
aligned.push({
|
|
849
|
+
dep,
|
|
850
|
+
from: version,
|
|
851
|
+
to: targetVersion,
|
|
852
|
+
packages: pkgNames
|
|
853
|
+
});
|
|
854
|
+
if (!dryRun) {
|
|
855
|
+
for (const pkgName of pkgNames) {
|
|
856
|
+
const pkg = packages.find((p) => p.name === pkgName);
|
|
857
|
+
if (!pkg) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (pkg.json.dependencies?.[dep]) {
|
|
861
|
+
pkg.json.dependencies[dep] = targetVersion;
|
|
862
|
+
}
|
|
863
|
+
if (pkg.json.devDependencies?.[dep]) {
|
|
864
|
+
pkg.json.devDependencies[dep] = targetVersion;
|
|
865
|
+
}
|
|
866
|
+
fs.writeFileSync(pkg.path, JSON.stringify(pkg.json, null, 2) + "\n");
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return aligned;
|
|
872
|
+
}
|
|
873
|
+
async function addMissingWorkspaceDeps(packages, dryRun) {
|
|
874
|
+
const added = [];
|
|
875
|
+
const workspacePackages = /* @__PURE__ */ new Map();
|
|
876
|
+
for (const pkg of packages) {
|
|
877
|
+
if (pkg.name.startsWith("@kb-labs/")) {
|
|
878
|
+
workspacePackages.set(pkg.name, "workspace:*");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
for (const pkg of packages) {
|
|
882
|
+
const packageDir = path3.dirname(pkg.path);
|
|
883
|
+
const srcDir = path3.join(packageDir, "src");
|
|
884
|
+
if (!fs.existsSync(srcDir)) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const imports = scanImports(srcDir);
|
|
888
|
+
const currentDeps = {
|
|
889
|
+
...pkg.json.dependencies,
|
|
890
|
+
...pkg.json.devDependencies
|
|
891
|
+
};
|
|
892
|
+
for (const imp of imports) {
|
|
893
|
+
if (imp.startsWith("@kb-labs/") && workspacePackages.has(imp) && !currentDeps[imp]) {
|
|
894
|
+
added.push({
|
|
895
|
+
package: pkg.name,
|
|
896
|
+
dep: imp,
|
|
897
|
+
version: "workspace:*"
|
|
898
|
+
});
|
|
899
|
+
if (!dryRun) {
|
|
900
|
+
if (!pkg.json.dependencies) {
|
|
901
|
+
pkg.json.dependencies = {};
|
|
902
|
+
}
|
|
903
|
+
pkg.json.dependencies[imp] = "workspace:*";
|
|
904
|
+
fs.writeFileSync(pkg.path, JSON.stringify(pkg.json, null, 2) + "\n");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return added;
|
|
910
|
+
}
|
|
911
|
+
async function removeUnusedDeps(packages, dryRun) {
|
|
912
|
+
const removed = [];
|
|
913
|
+
for (const pkg of packages) {
|
|
914
|
+
const packageDir = path3.dirname(pkg.path);
|
|
915
|
+
const srcDir = path3.join(packageDir, "src");
|
|
916
|
+
if (!fs.existsSync(srcDir)) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
const imports = new Set(scanImports(srcDir));
|
|
920
|
+
const deps = {
|
|
921
|
+
...pkg.json.dependencies,
|
|
922
|
+
...pkg.json.devDependencies
|
|
923
|
+
};
|
|
924
|
+
for (const dep of Object.keys(deps)) {
|
|
925
|
+
if (isProtectedDep(dep)) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (!imports.has(dep)) {
|
|
929
|
+
removed.push({
|
|
930
|
+
package: pkg.name,
|
|
931
|
+
dep
|
|
932
|
+
});
|
|
933
|
+
if (!dryRun) {
|
|
934
|
+
delete pkg.json.dependencies?.[dep];
|
|
935
|
+
delete pkg.json.devDependencies?.[dep];
|
|
936
|
+
fs.writeFileSync(pkg.path, JSON.stringify(pkg.json, null, 2) + "\n");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return removed;
|
|
942
|
+
}
|
|
943
|
+
function scanImports(dir) {
|
|
944
|
+
const imports = [];
|
|
945
|
+
function walk(currentDir) {
|
|
946
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
947
|
+
for (const entry of entries) {
|
|
948
|
+
const fullPath = path3.join(currentDir, entry.name);
|
|
949
|
+
if (entry.isDirectory()) {
|
|
950
|
+
walk(fullPath);
|
|
951
|
+
} else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
952
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
953
|
+
const importRegex = /(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g;
|
|
954
|
+
let match;
|
|
955
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
956
|
+
const imp = match[1];
|
|
957
|
+
if (!imp) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const pkgName = imp.startsWith("@") ? imp.split("/").slice(0, 2).join("/") : imp.split("/")[0];
|
|
961
|
+
if (pkgName) {
|
|
962
|
+
imports.push(pkgName);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
walk(dir);
|
|
969
|
+
return Array.from(new Set(imports));
|
|
970
|
+
}
|
|
971
|
+
function isProtectedDep(dep) {
|
|
972
|
+
const protectedDeps = [
|
|
973
|
+
"typescript",
|
|
974
|
+
"tsup",
|
|
975
|
+
"esbuild",
|
|
976
|
+
"vite",
|
|
977
|
+
"rollup",
|
|
978
|
+
"rimraf",
|
|
979
|
+
"vitest",
|
|
980
|
+
"jest",
|
|
981
|
+
"playwright",
|
|
982
|
+
"eslint",
|
|
983
|
+
"prettier"
|
|
984
|
+
];
|
|
985
|
+
return protectedDeps.some((p) => dep === p || dep.startsWith(`${p}-`) || dep.startsWith(`@${p}/`));
|
|
986
|
+
}
|
|
987
|
+
function outputStats(stats, flags, ui) {
|
|
988
|
+
if (flags.json) {
|
|
989
|
+
ui?.json?.(stats);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const sections = [];
|
|
993
|
+
sections.push({
|
|
994
|
+
header: "Overview",
|
|
995
|
+
items: [
|
|
996
|
+
`Total Packages: ${stats.totalPackages}`,
|
|
997
|
+
`Total Dependencies: ${stats.totalDeps}`,
|
|
998
|
+
`Duplicate Versions: ${stats.duplicates}`
|
|
999
|
+
]
|
|
1000
|
+
});
|
|
1001
|
+
if (stats.topUsed.length > 0) {
|
|
1002
|
+
sections.push({
|
|
1003
|
+
header: "Top 10 Most Used",
|
|
1004
|
+
items: stats.topUsed.map((d) => `${d.name} (${d.count} packages)`)
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
ui?.success?.("Dependency statistics", {
|
|
1008
|
+
title: "\u{1F4CA} Dependency Statistics",
|
|
1009
|
+
sections
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function outputResults(result, flags, ui) {
|
|
1013
|
+
if (flags.json) {
|
|
1014
|
+
ui?.json?.(result);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const sections = [];
|
|
1018
|
+
const summaryItems = [
|
|
1019
|
+
`Packages Scanned: ${result.packagesScanned}`,
|
|
1020
|
+
`Mode: ${result.dryRun ? "\u{1F50D} Dry Run (no changes applied)" : "\u2705 Changes Applied"}`
|
|
1021
|
+
];
|
|
1022
|
+
sections.push({ header: "Summary", items: summaryItems });
|
|
1023
|
+
if (result.alignedDeps.length > 0) {
|
|
1024
|
+
const alignedItems = result.alignedDeps.map(
|
|
1025
|
+
(a) => `${a.dep}: ${a.from} \u2192 ${a.to} (${a.packages.length} packages)`
|
|
1026
|
+
);
|
|
1027
|
+
sections.push({ header: "Aligned Versions", items: alignedItems.slice(0, 10) });
|
|
1028
|
+
if (result.alignedDeps.length > 10) {
|
|
1029
|
+
const lastSection = sections[sections.length - 1];
|
|
1030
|
+
if (lastSection) {
|
|
1031
|
+
lastSection.items.push(`... and ${result.alignedDeps.length - 10} more`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (result.addedDeps.length > 0) {
|
|
1036
|
+
const addedItems = result.addedDeps.map((a) => `${a.package}: +${a.dep}`);
|
|
1037
|
+
sections.push({ header: "Added Dependencies", items: addedItems.slice(0, 10) });
|
|
1038
|
+
if (result.addedDeps.length > 10) {
|
|
1039
|
+
const lastSection = sections[sections.length - 1];
|
|
1040
|
+
if (lastSection) {
|
|
1041
|
+
lastSection.items.push(`... and ${result.addedDeps.length - 10} more`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (result.removedDeps.length > 0) {
|
|
1046
|
+
const removedItems = result.removedDeps.map((r) => `${r.package}: -${r.dep}`);
|
|
1047
|
+
sections.push({ header: "Removed Dependencies", items: removedItems.slice(0, 10) });
|
|
1048
|
+
if (result.removedDeps.length > 10) {
|
|
1049
|
+
const lastSection = sections[sections.length - 1];
|
|
1050
|
+
if (lastSection) {
|
|
1051
|
+
lastSection.items.push(`... and ${result.removedDeps.length - 10} more`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (result.alignedDeps.length === 0 && result.addedDeps.length === 0 && result.removedDeps.length === 0) {
|
|
1056
|
+
sections.push({ header: "Result", items: ["\u2705 No issues found!"] });
|
|
1057
|
+
}
|
|
1058
|
+
if (result.dryRun && (result.alignedDeps.length > 0 || result.addedDeps.length > 0 || result.removedDeps.length > 0)) {
|
|
1059
|
+
sections.push({
|
|
1060
|
+
header: "Next Steps",
|
|
1061
|
+
items: ["Remove --dry-run flag to apply changes", "Run `pnpm install` after applying changes"]
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const title = result.dryRun ? "\u{1F50D} Dependency Fix Preview" : "\u2705 Dependency Fix Complete";
|
|
1065
|
+
ui?.success?.("Dependency fix completed", {
|
|
1066
|
+
title,
|
|
1067
|
+
sections
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
var health_default = defineCommand({
|
|
1071
|
+
id: "quality:health",
|
|
1072
|
+
description: "Check monorepo health score",
|
|
1073
|
+
handler: {
|
|
1074
|
+
async execute(ctx, input) {
|
|
1075
|
+
const { ui, platform } = ctx;
|
|
1076
|
+
const flags = input.flags ?? input;
|
|
1077
|
+
const health = await calculateHealth(ctx.cwd, flags.package);
|
|
1078
|
+
await platform.cache.set(CACHE_KEYS.HEALTH, health, 5 * 60 * 1e3);
|
|
1079
|
+
await platform.analytics.track("quality:health", {
|
|
1080
|
+
score: health.score,
|
|
1081
|
+
grade: health.grade,
|
|
1082
|
+
issueCount: health.issues.length,
|
|
1083
|
+
packageSpecific: !!flags.package
|
|
1084
|
+
});
|
|
1085
|
+
outputHealth(health, flags, ui);
|
|
1086
|
+
return {
|
|
1087
|
+
exitCode: health.score < 60 ? 1 : 0,
|
|
1088
|
+
result: health
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
function findPackages2(rootDir, specificPackage) {
|
|
1094
|
+
const packages = [];
|
|
1095
|
+
if (!fs.existsSync(rootDir)) {
|
|
1096
|
+
return packages;
|
|
1097
|
+
}
|
|
1098
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
1099
|
+
for (const entry of entries) {
|
|
1100
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const repoPath = path3.join(rootDir, entry.name);
|
|
1104
|
+
const packagesDir = path3.join(repoPath, "packages");
|
|
1105
|
+
if (!fs.existsSync(packagesDir)) {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
1109
|
+
for (const pkgDir of packageDirs) {
|
|
1110
|
+
if (!pkgDir.isDirectory()) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const packageJsonPath = path3.join(packagesDir, pkgDir.name, "package.json");
|
|
1114
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1115
|
+
if (specificPackage) {
|
|
1116
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
1117
|
+
if (pkgJson.name === specificPackage || pkgDir.name === specificPackage) {
|
|
1118
|
+
packages.push(packageJsonPath);
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
packages.push(packageJsonPath);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return packages;
|
|
1127
|
+
}
|
|
1128
|
+
async function calculateHealth(rootDir, specificPackage) {
|
|
1129
|
+
const packages = findPackages2(rootDir, specificPackage);
|
|
1130
|
+
let score = 100;
|
|
1131
|
+
const issues = [];
|
|
1132
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
1133
|
+
for (const packagePath of packages) {
|
|
1134
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
1135
|
+
const deps = {
|
|
1136
|
+
...packageJson.dependencies,
|
|
1137
|
+
...packageJson.devDependencies
|
|
1138
|
+
};
|
|
1139
|
+
for (const [dep, version] of Object.entries(deps)) {
|
|
1140
|
+
if (!allDeps.has(dep)) {
|
|
1141
|
+
allDeps.set(dep, /* @__PURE__ */ new Set());
|
|
1142
|
+
}
|
|
1143
|
+
allDeps.get(dep).add(version);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const duplicates = Array.from(allDeps.entries()).filter(([_, versions]) => versions.size > 1).filter(([dep]) => !dep.startsWith("@kb-labs/"));
|
|
1147
|
+
if (duplicates.length > 0) {
|
|
1148
|
+
const penalty = Math.min(20, duplicates.length * 2);
|
|
1149
|
+
score -= penalty;
|
|
1150
|
+
issues.push({
|
|
1151
|
+
type: "duplicates",
|
|
1152
|
+
severity: "warning",
|
|
1153
|
+
message: `${duplicates.length} duplicate dependencies (-${penalty})`
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
let missingReadmes = 0;
|
|
1157
|
+
for (const packagePath of packages) {
|
|
1158
|
+
const packageDir = path3.dirname(packagePath);
|
|
1159
|
+
const readmePath = path3.join(packageDir, "README.md");
|
|
1160
|
+
if (!fs.existsSync(readmePath)) {
|
|
1161
|
+
missingReadmes++;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (missingReadmes > 0) {
|
|
1165
|
+
const penalty = Math.min(15, missingReadmes);
|
|
1166
|
+
score -= penalty;
|
|
1167
|
+
issues.push({
|
|
1168
|
+
type: "missing-docs",
|
|
1169
|
+
severity: "warning",
|
|
1170
|
+
message: `${missingReadmes} packages missing README (-${penalty})`
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
let namingIssues = 0;
|
|
1174
|
+
for (const packagePath of packages) {
|
|
1175
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
1176
|
+
const packageName = packageJson.name;
|
|
1177
|
+
if (!packageName || !packageName.startsWith("@kb-labs/")) {
|
|
1178
|
+
namingIssues++;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (namingIssues > 0) {
|
|
1182
|
+
const penalty = Math.min(10, namingIssues * 2);
|
|
1183
|
+
score -= penalty;
|
|
1184
|
+
issues.push({
|
|
1185
|
+
type: "naming",
|
|
1186
|
+
severity: "critical",
|
|
1187
|
+
message: `${namingIssues} packages with naming issues (-${penalty})`
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
let missingTypes = 0;
|
|
1191
|
+
for (const packagePath of packages) {
|
|
1192
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
1193
|
+
if (!packageJson.types && !packageJson.typings) {
|
|
1194
|
+
missingTypes++;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (missingTypes > 0) {
|
|
1198
|
+
const penalty = Math.min(15, missingTypes);
|
|
1199
|
+
score -= penalty;
|
|
1200
|
+
issues.push({
|
|
1201
|
+
type: "missing-types",
|
|
1202
|
+
severity: "warning",
|
|
1203
|
+
message: `${missingTypes} packages missing types field (-${penalty})`
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
let grade;
|
|
1207
|
+
if (score >= 90) {
|
|
1208
|
+
grade = "A";
|
|
1209
|
+
} else if (score >= 80) {
|
|
1210
|
+
grade = "B";
|
|
1211
|
+
} else if (score >= 70) {
|
|
1212
|
+
grade = "C";
|
|
1213
|
+
} else if (score >= 60) {
|
|
1214
|
+
grade = "D";
|
|
1215
|
+
} else {
|
|
1216
|
+
grade = "F";
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
score: Math.max(0, score),
|
|
1220
|
+
grade,
|
|
1221
|
+
issues
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function outputHealth(health, flags, ui) {
|
|
1225
|
+
if (flags.json) {
|
|
1226
|
+
ui?.json?.(health);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const sections = [];
|
|
1230
|
+
const scoreItems = [
|
|
1231
|
+
`Score: ${health.score}/100`,
|
|
1232
|
+
`Grade: ${health.grade}`,
|
|
1233
|
+
`Status: ${health.score >= 80 ? "\u2705 Healthy" : health.score >= 60 ? "\u26A0\uFE0F Needs Attention" : "\u274C Critical"}`
|
|
1234
|
+
];
|
|
1235
|
+
sections.push({ header: "Health Score", items: scoreItems });
|
|
1236
|
+
if (health.issues.length > 0) {
|
|
1237
|
+
const issueItems = health.issues.map((issue) => {
|
|
1238
|
+
const icon = issue.severity === "critical" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
|
|
1239
|
+
return `${icon} ${issue.message}`;
|
|
1240
|
+
});
|
|
1241
|
+
if (flags.detailed) {
|
|
1242
|
+
sections.push({ header: "Issues (Detailed)", items: issueItems });
|
|
1243
|
+
} else {
|
|
1244
|
+
sections.push({ header: "Issues", items: issueItems });
|
|
1245
|
+
}
|
|
1246
|
+
} else {
|
|
1247
|
+
sections.push({ header: "Issues", items: ["\u2705 No issues found!"] });
|
|
1248
|
+
}
|
|
1249
|
+
if (health.score < 90) {
|
|
1250
|
+
const recommendations = [];
|
|
1251
|
+
if (health.issues.some((i) => i.type === "duplicates")) {
|
|
1252
|
+
recommendations.push("Run `kb quality:fix-deps --align-versions` to fix duplicates");
|
|
1253
|
+
}
|
|
1254
|
+
if (health.issues.some((i) => i.type === "missing-docs")) {
|
|
1255
|
+
recommendations.push("Add README.md files to packages");
|
|
1256
|
+
}
|
|
1257
|
+
if (health.issues.some((i) => i.type === "missing-types")) {
|
|
1258
|
+
recommendations.push('Add "types" field to package.json or enable dts generation');
|
|
1259
|
+
}
|
|
1260
|
+
if (recommendations.length > 0) {
|
|
1261
|
+
sections.push({ header: "Recommendations", items: recommendations });
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const title = health.score >= 80 ? "\u{1F49A} Monorepo Health Check - Healthy" : health.score >= 60 ? "\u{1F49B} Monorepo Health Check - Needs Attention" : "\u2764\uFE0F Monorepo Health Check - Critical";
|
|
1265
|
+
ui?.success?.("Health check completed", {
|
|
1266
|
+
title,
|
|
1267
|
+
sections
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
var stats_default = defineCommand({
|
|
1271
|
+
id: "quality:stats",
|
|
1272
|
+
description: "Show monorepo statistics and health score",
|
|
1273
|
+
handler: {
|
|
1274
|
+
async execute(ctx, input) {
|
|
1275
|
+
const { ui, platform } = ctx;
|
|
1276
|
+
const flags = input.flags ?? input;
|
|
1277
|
+
if (!flags.refresh) {
|
|
1278
|
+
const cached = await platform.cache.get(CACHE_KEYS.STATS);
|
|
1279
|
+
if (cached) {
|
|
1280
|
+
outputStats2(cached, flags, ui);
|
|
1281
|
+
return { exitCode: 0 };
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const stats = await collectStats(ctx.cwd);
|
|
1285
|
+
if (flags.health) {
|
|
1286
|
+
stats.health = calculateHealthScore(stats);
|
|
1287
|
+
}
|
|
1288
|
+
await platform.cache.set(CACHE_KEYS.STATS, stats, 5 * 60 * 1e3);
|
|
1289
|
+
await platform.analytics.track("quality:stats", {
|
|
1290
|
+
packages: stats.overview.totalPackages,
|
|
1291
|
+
repositories: stats.overview.totalRepositories,
|
|
1292
|
+
withHealth: flags.health ?? false
|
|
1293
|
+
});
|
|
1294
|
+
outputStats2(stats, flags, ui);
|
|
1295
|
+
return { exitCode: 0 };
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
function findPackages3(rootDir) {
|
|
1300
|
+
const packages = [];
|
|
1301
|
+
if (!fs.existsSync(rootDir)) {
|
|
1302
|
+
return packages;
|
|
1303
|
+
}
|
|
1304
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
1305
|
+
for (const entry of entries) {
|
|
1306
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
const repoPath = path3.join(rootDir, entry.name);
|
|
1310
|
+
const packagesDir = path3.join(repoPath, "packages");
|
|
1311
|
+
if (!fs.existsSync(packagesDir)) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
1315
|
+
for (const pkgDir of packageDirs) {
|
|
1316
|
+
if (!pkgDir.isDirectory()) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const packageJsonPath = path3.join(packagesDir, pkgDir.name, "package.json");
|
|
1320
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1321
|
+
packages.push(packageJsonPath);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return packages;
|
|
1326
|
+
}
|
|
1327
|
+
function calculateSize(packageDir) {
|
|
1328
|
+
const srcDir = path3.join(packageDir, "src");
|
|
1329
|
+
if (!fs.existsSync(srcDir)) {
|
|
1330
|
+
return { files: 0, lines: 0, bytes: 0 };
|
|
1331
|
+
}
|
|
1332
|
+
let files = 0;
|
|
1333
|
+
let lines = 0;
|
|
1334
|
+
let bytes = 0;
|
|
1335
|
+
function walk(dir) {
|
|
1336
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1337
|
+
for (const entry of entries) {
|
|
1338
|
+
const fullPath = path3.join(dir, entry.name);
|
|
1339
|
+
if (entry.isDirectory()) {
|
|
1340
|
+
walk(fullPath);
|
|
1341
|
+
} else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
1342
|
+
files++;
|
|
1343
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
1344
|
+
lines += content.split("\n").length;
|
|
1345
|
+
bytes += Buffer.byteLength(content, "utf-8");
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
walk(srcDir);
|
|
1350
|
+
return { files, lines, bytes };
|
|
1351
|
+
}
|
|
1352
|
+
async function collectStats(rootDir) {
|
|
1353
|
+
const packages = findPackages3(rootDir);
|
|
1354
|
+
const stats = {
|
|
1355
|
+
overview: {
|
|
1356
|
+
totalPackages: 0,
|
|
1357
|
+
totalRepositories: 0,
|
|
1358
|
+
totalFiles: 0,
|
|
1359
|
+
totalLines: 0,
|
|
1360
|
+
totalBytes: 0
|
|
1361
|
+
},
|
|
1362
|
+
byRepository: {},
|
|
1363
|
+
dependencies: {
|
|
1364
|
+
total: 0,
|
|
1365
|
+
workspace: 0,
|
|
1366
|
+
external: 0,
|
|
1367
|
+
duplicates: 0,
|
|
1368
|
+
topUsed: []
|
|
1369
|
+
},
|
|
1370
|
+
health: {
|
|
1371
|
+
score: 0,
|
|
1372
|
+
grade: "F",
|
|
1373
|
+
issues: []
|
|
1374
|
+
},
|
|
1375
|
+
largestPackages: []
|
|
1376
|
+
};
|
|
1377
|
+
const repos = /* @__PURE__ */ new Set();
|
|
1378
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
1379
|
+
for (const packagePath of packages) {
|
|
1380
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
1381
|
+
const packageName = packageJson.name;
|
|
1382
|
+
if (!packageName || !packageName.startsWith("@kb-labs/")) {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
stats.overview.totalPackages++;
|
|
1386
|
+
const packageDir = path3.dirname(packagePath);
|
|
1387
|
+
const repoName = path3.basename(path3.dirname(path3.dirname(packageDir)));
|
|
1388
|
+
repos.add(repoName);
|
|
1389
|
+
const size = calculateSize(packageDir);
|
|
1390
|
+
stats.overview.totalFiles += size.files;
|
|
1391
|
+
stats.overview.totalLines += size.lines;
|
|
1392
|
+
stats.overview.totalBytes += size.bytes;
|
|
1393
|
+
if (!stats.byRepository[repoName]) {
|
|
1394
|
+
stats.byRepository[repoName] = {
|
|
1395
|
+
name: repoName,
|
|
1396
|
+
packages: 0,
|
|
1397
|
+
files: 0,
|
|
1398
|
+
lines: 0,
|
|
1399
|
+
bytes: 0
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
stats.byRepository[repoName].packages++;
|
|
1403
|
+
stats.byRepository[repoName].files += size.files;
|
|
1404
|
+
stats.byRepository[repoName].lines += size.lines;
|
|
1405
|
+
stats.byRepository[repoName].bytes += size.bytes;
|
|
1406
|
+
const deps = {
|
|
1407
|
+
...packageJson.dependencies,
|
|
1408
|
+
...packageJson.devDependencies
|
|
1409
|
+
};
|
|
1410
|
+
for (const dep of Object.keys(deps)) {
|
|
1411
|
+
stats.dependencies.total++;
|
|
1412
|
+
if (dep.startsWith("@kb-labs/")) {
|
|
1413
|
+
stats.dependencies.workspace++;
|
|
1414
|
+
} else {
|
|
1415
|
+
stats.dependencies.external++;
|
|
1416
|
+
}
|
|
1417
|
+
allDeps.set(dep, (allDeps.get(dep) || 0) + 1);
|
|
1418
|
+
}
|
|
1419
|
+
stats.largestPackages.push({
|
|
1420
|
+
name: packageName,
|
|
1421
|
+
repository: repoName,
|
|
1422
|
+
files: size.files,
|
|
1423
|
+
lines: size.lines,
|
|
1424
|
+
bytes: size.bytes
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
stats.overview.totalRepositories = repos.size;
|
|
1428
|
+
stats.dependencies.duplicates = Array.from(allDeps.values()).filter((count) => count > 1).length;
|
|
1429
|
+
stats.dependencies.topUsed = Array.from(allDeps.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, count]) => ({ name, count }));
|
|
1430
|
+
stats.largestPackages.sort((a, b) => b.lines - a.lines);
|
|
1431
|
+
stats.largestPackages = stats.largestPackages.slice(0, 10);
|
|
1432
|
+
return stats;
|
|
1433
|
+
}
|
|
1434
|
+
function calculateHealthScore(stats) {
|
|
1435
|
+
let score = 100;
|
|
1436
|
+
const issues = [];
|
|
1437
|
+
if (stats.dependencies.duplicates > 0) {
|
|
1438
|
+
const penalty = Math.min(20, stats.dependencies.duplicates * 2);
|
|
1439
|
+
score -= penalty;
|
|
1440
|
+
issues.push({
|
|
1441
|
+
type: "duplicates",
|
|
1442
|
+
severity: "warning",
|
|
1443
|
+
message: `${stats.dependencies.duplicates} duplicate dependencies (-${penalty})`
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
let grade;
|
|
1447
|
+
if (score >= 90) {
|
|
1448
|
+
grade = "A";
|
|
1449
|
+
} else if (score >= 80) {
|
|
1450
|
+
grade = "B";
|
|
1451
|
+
} else if (score >= 70) {
|
|
1452
|
+
grade = "C";
|
|
1453
|
+
} else if (score >= 60) {
|
|
1454
|
+
grade = "D";
|
|
1455
|
+
} else {
|
|
1456
|
+
grade = "F";
|
|
1457
|
+
}
|
|
1458
|
+
return {
|
|
1459
|
+
score,
|
|
1460
|
+
grade,
|
|
1461
|
+
issues
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function outputStats2(stats, flags, ui) {
|
|
1465
|
+
if (flags.json) {
|
|
1466
|
+
ui?.json?.(stats);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (flags.md) {
|
|
1470
|
+
outputMarkdown(stats);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const overviewItems = [
|
|
1474
|
+
`Packages: ${stats.overview.totalPackages}`,
|
|
1475
|
+
`Repositories: ${stats.overview.totalRepositories}`,
|
|
1476
|
+
`Lines of Code: ${stats.overview.totalLines.toLocaleString()}`,
|
|
1477
|
+
`Total Size: ${formatBytes(stats.overview.totalBytes)}`
|
|
1478
|
+
];
|
|
1479
|
+
const sections = [
|
|
1480
|
+
{ header: "Overview", items: overviewItems }
|
|
1481
|
+
];
|
|
1482
|
+
if (flags.health && stats.health) {
|
|
1483
|
+
const healthItems = [
|
|
1484
|
+
`Score: ${stats.health.score}/100 (Grade ${stats.health.grade})`
|
|
1485
|
+
];
|
|
1486
|
+
if (stats.health.issues.length > 0) {
|
|
1487
|
+
healthItems.push("");
|
|
1488
|
+
healthItems.push("Issues:");
|
|
1489
|
+
stats.health.issues.forEach((issue) => {
|
|
1490
|
+
healthItems.push(` ${issue.message}`);
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
sections.push({ header: "Health Score", items: healthItems });
|
|
1494
|
+
}
|
|
1495
|
+
const topRepos = Object.values(stats.byRepository).sort((a, b) => b.lines - a.lines).slice(0, 5).map((repo) => `${repo.name}: ${repo.lines.toLocaleString()} lines, ${repo.packages} pkg(s)`);
|
|
1496
|
+
if (topRepos.length > 0) {
|
|
1497
|
+
sections.push({ header: "Top Repositories", items: topRepos });
|
|
1498
|
+
}
|
|
1499
|
+
ui?.success?.("Statistics collected successfully", {
|
|
1500
|
+
title: "\u{1F4CA} KB Labs Monorepo Statistics",
|
|
1501
|
+
sections
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
function outputMarkdown(stats) {
|
|
1505
|
+
console.log("# KB Labs Monorepo Statistics\n");
|
|
1506
|
+
console.log("## Overview\n");
|
|
1507
|
+
console.log("| Metric | Value |");
|
|
1508
|
+
console.log("|--------|-------|");
|
|
1509
|
+
console.log(`| Packages | ${stats.overview.totalPackages} |`);
|
|
1510
|
+
console.log(`| Repositories | ${stats.overview.totalRepositories} |`);
|
|
1511
|
+
console.log(`| Lines of Code | ${stats.overview.totalLines.toLocaleString()} |`);
|
|
1512
|
+
console.log(`| Total Size | ${formatBytes(stats.overview.totalBytes)} |
|
|
1513
|
+
`);
|
|
1514
|
+
if (stats.health) {
|
|
1515
|
+
console.log("## Health Score\n");
|
|
1516
|
+
console.log(`**${stats.health.score}/100** (Grade ${stats.health.grade})
|
|
1517
|
+
`);
|
|
1518
|
+
if (stats.health.issues.length > 0) {
|
|
1519
|
+
console.log("### Issues\n");
|
|
1520
|
+
for (const issue of stats.health.issues) {
|
|
1521
|
+
console.log(`- ${issue.message}`);
|
|
1522
|
+
}
|
|
1523
|
+
console.log("");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
console.log("## By Repository\n");
|
|
1527
|
+
console.log("| Repository | Packages | Lines | Size |");
|
|
1528
|
+
console.log("|------------|----------|-------|------|");
|
|
1529
|
+
for (const [name, repo] of Object.entries(stats.byRepository)) {
|
|
1530
|
+
console.log(`| ${name} | ${repo.packages} | ${repo.lines.toLocaleString()} | ${formatBytes(repo.bytes)} |`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function formatBytes(bytes) {
|
|
1534
|
+
if (bytes === 0) {
|
|
1535
|
+
return "0 B";
|
|
1536
|
+
}
|
|
1537
|
+
const k = 1024;
|
|
1538
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
1539
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1540
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
1541
|
+
}
|
|
1542
|
+
var visualize_default = defineCommand({
|
|
1543
|
+
id: "quality:visualize",
|
|
1544
|
+
description: "Visualize dependency graph",
|
|
1545
|
+
handler: {
|
|
1546
|
+
async execute(ctx, input) {
|
|
1547
|
+
const { ui } = ctx;
|
|
1548
|
+
const flags = input.flags ?? input;
|
|
1549
|
+
const graph = buildDependencyGraph(ctx.cwd);
|
|
1550
|
+
if (flags.dot) {
|
|
1551
|
+
outputDot(graph, flags, ui);
|
|
1552
|
+
} else if (flags.tree && flags.package) {
|
|
1553
|
+
outputTree(graph, flags.package, flags, ui);
|
|
1554
|
+
} else if (flags.reverse && flags.package) {
|
|
1555
|
+
outputReverse(graph, flags.package, flags, ui);
|
|
1556
|
+
} else if (flags.impact && flags.package) {
|
|
1557
|
+
outputImpact(graph, flags.package, flags, ui);
|
|
1558
|
+
} else if (flags.stats) {
|
|
1559
|
+
outputStats3(graph, flags, ui);
|
|
1560
|
+
} else {
|
|
1561
|
+
outputStats3(graph, flags, ui);
|
|
1562
|
+
}
|
|
1563
|
+
return { exitCode: 0 };
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
function outputTree(graph, packageName, flags, ui) {
|
|
1568
|
+
const { nodes } = graph;
|
|
1569
|
+
const node = nodes.get(packageName);
|
|
1570
|
+
if (!node) {
|
|
1571
|
+
ui?.fatal?.(`Package not found: ${packageName}`);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const sections = [];
|
|
1575
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1576
|
+
const treeItems = [];
|
|
1577
|
+
function buildTree(pkg, depth, prefix) {
|
|
1578
|
+
if (visited.has(pkg)) {
|
|
1579
|
+
treeItems.push(`${prefix}${pkg} (circular)`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
visited.add(pkg);
|
|
1583
|
+
const pkgNode = nodes.get(pkg);
|
|
1584
|
+
if (!pkgNode) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
treeItems.push(`${prefix}${pkg}`);
|
|
1588
|
+
const deps = Array.from(pkgNode.deps);
|
|
1589
|
+
for (let i = 0; i < deps.length; i++) {
|
|
1590
|
+
const dep = deps[i];
|
|
1591
|
+
if (!dep) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const isLast = i === deps.length - 1;
|
|
1595
|
+
const newPrefix = prefix + (isLast ? " \u2514\u2500 " : " \u251C\u2500 ");
|
|
1596
|
+
buildTree(dep, depth + 1, newPrefix);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
buildTree(packageName, 0, "");
|
|
1600
|
+
sections.push({
|
|
1601
|
+
header: "Dependency Tree",
|
|
1602
|
+
items: treeItems
|
|
1603
|
+
});
|
|
1604
|
+
ui?.success?.("Dependency tree generated", {
|
|
1605
|
+
title: `\u{1F4E6} Dependency Tree for ${packageName}`,
|
|
1606
|
+
sections
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
function outputReverse(graph, packageName, flags, ui) {
|
|
1610
|
+
const reverseDeps = getReverseDependencies(graph, packageName);
|
|
1611
|
+
if (flags.json) {
|
|
1612
|
+
ui?.json?.({ package: packageName, reverseDependencies: Array.from(reverseDeps) });
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const sections = [];
|
|
1616
|
+
if (reverseDeps.size === 0) {
|
|
1617
|
+
sections.push({
|
|
1618
|
+
header: "Result",
|
|
1619
|
+
items: ["No packages depend on this package"]
|
|
1620
|
+
});
|
|
1621
|
+
} else {
|
|
1622
|
+
sections.push({
|
|
1623
|
+
header: `Packages that depend on ${packageName}`,
|
|
1624
|
+
items: Array.from(reverseDeps).map((dep) => `\u2022 ${dep}`)
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
ui?.success?.("Reverse dependencies found", {
|
|
1628
|
+
title: `\u2B05\uFE0F Reverse Dependencies for ${packageName}`,
|
|
1629
|
+
sections
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
function outputImpact(graph, packageName, flags, ui) {
|
|
1633
|
+
const affected = getImpactAnalysis(graph, packageName);
|
|
1634
|
+
if (flags.json) {
|
|
1635
|
+
ui?.json?.({ package: packageName, affectedPackages: Array.from(affected) });
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const sections = [];
|
|
1639
|
+
if (affected.size === 0) {
|
|
1640
|
+
sections.push({
|
|
1641
|
+
header: "Result",
|
|
1642
|
+
items: ["No other packages affected by changes to this package"]
|
|
1643
|
+
});
|
|
1644
|
+
} else {
|
|
1645
|
+
sections.push({
|
|
1646
|
+
header: `Impact Analysis for ${packageName}`,
|
|
1647
|
+
items: [
|
|
1648
|
+
`Changes to this package will affect ${affected.size} other packages:`,
|
|
1649
|
+
"",
|
|
1650
|
+
...Array.from(affected).map((dep) => `\u2022 ${dep}`)
|
|
1651
|
+
]
|
|
1652
|
+
});
|
|
1653
|
+
sections.push({
|
|
1654
|
+
header: "Recommendations",
|
|
1655
|
+
items: [
|
|
1656
|
+
"\u{1F9EA} Run tests in affected packages",
|
|
1657
|
+
"\u{1F528} Rebuild affected packages",
|
|
1658
|
+
"\u{1F4DD} Update documentation if API changed"
|
|
1659
|
+
]
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
ui?.success?.("Impact analysis completed", {
|
|
1663
|
+
title: `\u{1F4A5} Impact Analysis for ${packageName}`,
|
|
1664
|
+
sections
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
function outputStats3(graph, flags, ui) {
|
|
1668
|
+
const { nodes } = graph;
|
|
1669
|
+
let totalDeps = 0;
|
|
1670
|
+
let maxDeps = 0;
|
|
1671
|
+
let maxDepsPackage = "";
|
|
1672
|
+
const depCounts = /* @__PURE__ */ new Map();
|
|
1673
|
+
for (const [name, node] of nodes) {
|
|
1674
|
+
const depCount = node.deps.size;
|
|
1675
|
+
totalDeps += depCount;
|
|
1676
|
+
if (depCount > maxDeps) {
|
|
1677
|
+
maxDeps = depCount;
|
|
1678
|
+
maxDepsPackage = name;
|
|
1679
|
+
}
|
|
1680
|
+
depCounts.set(depCount, (depCounts.get(depCount) ?? 0) + 1);
|
|
1681
|
+
}
|
|
1682
|
+
const avgDeps = nodes.size > 0 ? (totalDeps / nodes.size).toFixed(2) : "0";
|
|
1683
|
+
if (flags.json) {
|
|
1684
|
+
ui?.json?.({
|
|
1685
|
+
totalPackages: nodes.size,
|
|
1686
|
+
totalDependencies: totalDeps,
|
|
1687
|
+
averageDependencies: Number.parseFloat(avgDeps),
|
|
1688
|
+
maxDependencies: maxDeps,
|
|
1689
|
+
maxDependenciesPackage: maxDepsPackage
|
|
1690
|
+
});
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const sections = [];
|
|
1694
|
+
sections.push({
|
|
1695
|
+
header: "Graph Statistics",
|
|
1696
|
+
items: [
|
|
1697
|
+
`Total packages: ${nodes.size}`,
|
|
1698
|
+
`Total dependencies: ${totalDeps}`,
|
|
1699
|
+
`Average dependencies per package: ${avgDeps}`,
|
|
1700
|
+
`Max dependencies: ${maxDeps} (${maxDepsPackage})`
|
|
1701
|
+
]
|
|
1702
|
+
});
|
|
1703
|
+
const distributionItems = [];
|
|
1704
|
+
const sortedCounts = Array.from(depCounts.entries()).sort((a, b) => a[0] - b[0]);
|
|
1705
|
+
for (const [count, packages] of sortedCounts) {
|
|
1706
|
+
const bar = "\u2588".repeat(Math.min(packages, 20));
|
|
1707
|
+
distributionItems.push(`${count} deps: ${bar} ${packages} packages`);
|
|
1708
|
+
}
|
|
1709
|
+
sections.push({
|
|
1710
|
+
header: "Dependency Distribution",
|
|
1711
|
+
items: distributionItems
|
|
1712
|
+
});
|
|
1713
|
+
ui?.success?.("Graph statistics calculated", {
|
|
1714
|
+
title: "\u{1F4CA} Dependency Graph Statistics",
|
|
1715
|
+
sections
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
function outputDot(graph, flags, ui) {
|
|
1719
|
+
const { nodes } = graph;
|
|
1720
|
+
const lines = [];
|
|
1721
|
+
lines.push("digraph dependencies {");
|
|
1722
|
+
lines.push(" rankdir=LR;");
|
|
1723
|
+
lines.push(" node [shape=box];");
|
|
1724
|
+
lines.push("");
|
|
1725
|
+
for (const [name, node] of nodes) {
|
|
1726
|
+
for (const dep of node.deps) {
|
|
1727
|
+
const safeName = name.replace(/[@\/]/g, "_");
|
|
1728
|
+
const safeDep = dep.replace(/[@\/]/g, "_");
|
|
1729
|
+
lines.push(` "${safeName}" -> "${safeDep}";`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
lines.push("}");
|
|
1733
|
+
if (flags.json) {
|
|
1734
|
+
ui?.json?.({ dot: lines.join("\n") });
|
|
1735
|
+
} else {
|
|
1736
|
+
for (const line of lines) {
|
|
1737
|
+
ui?.write?.(line);
|
|
1738
|
+
}
|
|
1739
|
+
ui?.write?.("");
|
|
1740
|
+
ui?.write?.("# Save to file and visualize with:");
|
|
1741
|
+
ui?.write?.("# dot -Tpng dependencies.dot -o dependencies.png");
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export { build_order_default as buildOrder, check_builds_default as checkBuilds, check_tests_default as checkTests, check_types_default as checkTypes, cycles_default as cycles, dead_code_default as deadCode, fix_deps_default as fixDeps, health_default as health, stats_default as stats, visualize_default as visualize };
|
|
1746
|
+
//# sourceMappingURL=index.js.map
|
|
1747
|
+
//# sourceMappingURL=index.js.map
|