@kb-labs/quality-entry 2.93.0 → 2.96.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/dist/cli/commands/build-order.d.ts +5 -12
- package/dist/cli/commands/build-order.js +45 -97
- package/dist/cli/commands/build-order.js.map +1 -1
- package/dist/cli/commands/check-layers.d.ts +9 -0
- package/dist/cli/commands/check-layers.js +48 -0
- package/dist/cli/commands/check-layers.js.map +1 -0
- package/dist/cli/commands/context.d.ts +9 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/coupling.d.ts +9 -0
- package/dist/cli/commands/coupling.js +35 -0
- package/dist/cli/commands/coupling.js.map +1 -0
- package/dist/cli/commands/dead-code.d.ts +4 -5
- package/dist/cli/commands/dead-code.js +41 -197
- package/dist/cli/commands/dead-code.js.map +1 -1
- package/dist/cli/commands/fix-deps.d.ts +2 -5
- package/dist/cli/commands/fix-deps.js +74 -83
- package/dist/cli/commands/fix-deps.js.map +1 -1
- package/dist/cli/commands/flags.d.ts +115 -1
- package/dist/cli/commands/flags.js +46 -1
- package/dist/cli/commands/flags.js.map +1 -1
- package/dist/cli/commands/gate.d.ts +9 -0
- package/dist/cli/commands/gate.js +59 -0
- package/dist/cli/commands/gate.js.map +1 -0
- package/dist/cli/commands/health.d.ts +4 -5
- package/dist/cli/commands/health.js +42 -191
- package/dist/cli/commands/health.js.map +1 -1
- package/dist/cli/commands/history.d.ts +9 -0
- package/dist/cli/commands/history.js +55 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/index.d.ts +6 -2
- package/dist/cli/commands/index.js +411 -828
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/snapshot.d.ts +9 -0
- package/dist/cli/commands/snapshot.js +91 -0
- package/dist/cli/commands/snapshot.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +4 -5
- package/dist/cli/commands/stats.js +17 -265
- package/dist/cli/commands/stats.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +198 -306
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +95 -82
- package/dist/manifest.js +198 -306
- package/dist/manifest.js.map +1 -1
- package/dist/rest/handlers/build-order-handler.d.ts +5 -13
- package/dist/rest/handlers/build-order-handler.js +14 -17
- package/dist/rest/handlers/build-order-handler.js.map +1 -1
- package/dist/rest/handlers/builds-handler.js +10 -6
- package/dist/rest/handlers/builds-handler.js.map +1 -1
- package/dist/rest/handlers/coupling-handler.d.ts +8 -0
- package/dist/rest/handlers/coupling-handler.js +28 -0
- package/dist/rest/handlers/coupling-handler.js.map +1 -0
- package/dist/rest/handlers/cycles-handler.js +16 -12
- package/dist/rest/handlers/cycles-handler.js.map +1 -1
- package/dist/rest/handlers/dependencies-handler.js +12 -8
- package/dist/rest/handlers/dependencies-handler.js.map +1 -1
- package/dist/rest/handlers/graph-handler.js +77 -73
- package/dist/rest/handlers/graph-handler.js.map +1 -1
- package/dist/rest/handlers/health-handler.d.ts +4 -11
- package/dist/rest/handlers/health-handler.js +35 -9
- package/dist/rest/handlers/health-handler.js.map +1 -1
- package/dist/rest/handlers/history-handler.d.ts +7 -0
- package/dist/rest/handlers/history-handler.js +21 -0
- package/dist/rest/handlers/history-handler.js.map +1 -0
- package/dist/rest/handlers/knip-handler.d.ts +8 -0
- package/dist/rest/handlers/knip-handler.js +31 -0
- package/dist/rest/handlers/knip-handler.js.map +1 -0
- package/dist/rest/handlers/layers-handler.d.ts +8 -0
- package/dist/rest/handlers/layers-handler.js +28 -0
- package/dist/rest/handlers/layers-handler.js.map +1 -0
- package/dist/rest/handlers/snapshot-handler.d.ts +7 -0
- package/dist/rest/handlers/snapshot-handler.js +67 -0
- package/dist/rest/handlers/snapshot-handler.js.map +1 -0
- package/dist/rest/handlers/stale-handler.js +13 -9
- package/dist/rest/handlers/stale-handler.js.map +1 -1
- package/dist/rest/handlers/stats-handler.d.ts +4 -9
- package/dist/rest/handlers/stats-handler.js +22 -18
- package/dist/rest/handlers/stats-handler.js.map +1 -1
- package/dist/rest/handlers/tests-handler.js +12 -8
- package/dist/rest/handlers/tests-handler.js.map +1 -1
- package/dist/rest/handlers/types-handler.js +10 -6
- package/dist/rest/handlers/types-handler.js.map +1 -1
- package/dist/widgets/117.js +24 -1
- package/dist/widgets/117.js.map +1 -1
- package/dist/widgets/370.js +1 -1
- package/dist/widgets/370.js.map +1 -1
- package/dist/widgets/62.js +97 -0
- package/dist/widgets/62.js.map +1 -0
- package/dist/widgets/__federation_expose_QualityOverview.js +1 -1
- package/dist/widgets/__federation_expose_QualityOverview.js.map +1 -1
- package/dist/widgets/mf-manifest.json +27 -27
- package/dist/widgets/mf-stats.json +27 -27
- package/dist/widgets/remoteEntry.js +2 -2
- package/dist/widgets/remoteEntry.js.map +1 -1
- package/package.json +9 -8
- package/dist/widgets/418.js +0 -39
- package/dist/widgets/418.js.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { defineCommand, useLoader } from '@kb-labs/sdk';
|
|
2
|
-
import {
|
|
3
|
-
import { CACHE_KEYS } from '@kb-labs/quality-contracts';
|
|
1
|
+
import { defineCommand, useConfig, validationError, useLoader } from '@kb-labs/sdk';
|
|
2
|
+
import { analyzeBuildOrder, analyzeLayering, analyzeCoupling, runKnip, analyzeTypes as analyzeTypes$1, calculateHealth, QualitySnapshotStore, calculateStats } from '@kb-labs/quality-core';
|
|
3
|
+
import { defaultQualityConfig, CACHE_KEYS, CACHE_TTLS } from '@kb-labs/quality-contracts';
|
|
4
4
|
import { checkBuilds } from '@kb-labs/quality-core/builds';
|
|
5
5
|
import { runTests } from '@kb-labs/quality-core/tests';
|
|
6
6
|
import { analyzeTypes } from '@kb-labs/quality-core/types';
|
|
7
|
-
import {
|
|
7
|
+
import { buildDependencyGraph, findCircularDependencies, getReverseDependencies, getImpactAnalysis } from '@kb-labs/quality-core/graph';
|
|
8
8
|
import fs from 'fs';
|
|
9
|
-
import
|
|
9
|
+
import path from 'path';
|
|
10
10
|
|
|
11
11
|
// src/cli/commands/build-order.ts
|
|
12
12
|
var build_order_default = defineCommand({
|
|
@@ -14,106 +14,53 @@ var build_order_default = defineCommand({
|
|
|
14
14
|
description: "Calculate build order using topological sort",
|
|
15
15
|
handler: {
|
|
16
16
|
async execute(ctx, input) {
|
|
17
|
-
const {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (result.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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`);
|
|
17
|
+
const { flags } = input;
|
|
18
|
+
const config = await useConfig();
|
|
19
|
+
const cfg = config ?? defaultQualityConfig;
|
|
20
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
21
|
+
const result = analyzeBuildOrder({
|
|
22
|
+
rootDir: cwd,
|
|
23
|
+
layerMap: cfg.layers,
|
|
24
|
+
filterPackage: flags.package
|
|
25
|
+
});
|
|
26
|
+
if (result.hasCircular) {
|
|
27
|
+
if (flags.json) {
|
|
28
|
+
ctx.ui?.json?.({ ...result, ok: false });
|
|
29
|
+
} else {
|
|
30
|
+
ctx.ui?.error?.(`Circular dependencies detected \u2014 build order cannot be fully determined`, {
|
|
31
|
+
sections: result.circular.map((cycle, i) => ({
|
|
32
|
+
header: `Cycle ${i + 1}`,
|
|
33
|
+
items: cycle
|
|
34
|
+
}))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { exitCode: 1 };
|
|
56
38
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
39
|
+
if (flags.json) {
|
|
40
|
+
ctx.ui?.json?.(result);
|
|
41
|
+
return { exitCode: 0 };
|
|
68
42
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
43
|
+
if (flags.script) {
|
|
44
|
+
for (let i = 0; i < result.layers.length; i++) {
|
|
45
|
+
const layer = result.layers[i];
|
|
46
|
+
if (!layer) continue;
|
|
47
|
+
ctx.ui?.success?.(`# Layer ${i + 1}`, { sections: [{ header: "", items: layer.map((p) => `pnpm --filter "${p}" run build`) }] });
|
|
48
|
+
}
|
|
49
|
+
return { exitCode: 0 };
|
|
72
50
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
51
|
+
const sections = flags.layers ? result.layers.map((layer, i) => ({
|
|
52
|
+
header: `Layer ${i + 1} \u2014 ${layer.length} package(s) (parallel)`,
|
|
53
|
+
items: layer
|
|
54
|
+
})) : [{ header: "Build order", items: result.sorted.map((p, i) => `${i + 1}. ${p}`) }];
|
|
55
|
+
sections.push({
|
|
56
|
+
header: "Summary",
|
|
57
|
+
items: [`${result.packageCount} packages in ${result.layerCount} layers`]
|
|
58
|
+
});
|
|
59
|
+
ctx.ui?.success?.(flags.package ? `Build order for ${flags.package}` : "Monorepo build order", { sections });
|
|
60
|
+
return { exitCode: 0 };
|
|
100
61
|
}
|
|
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
62
|
}
|
|
112
|
-
|
|
113
|
-
title: "\u26A0\uFE0F Circular Dependencies",
|
|
114
|
-
sections
|
|
115
|
-
});
|
|
116
|
-
}
|
|
63
|
+
});
|
|
117
64
|
var check_builds_default = defineCommand({
|
|
118
65
|
id: "quality:check-builds",
|
|
119
66
|
description: "Check build status across monorepo",
|
|
@@ -402,6 +349,73 @@ function outputTypeAnalysis(result, flags, ui) {
|
|
|
402
349
|
sections
|
|
403
350
|
});
|
|
404
351
|
}
|
|
352
|
+
var check_layers_default = defineCommand({
|
|
353
|
+
id: "quality:check-layers",
|
|
354
|
+
description: "Detect layering violations (lower layer importing higher layer)",
|
|
355
|
+
handler: {
|
|
356
|
+
async execute(ctx, input) {
|
|
357
|
+
const { flags } = input;
|
|
358
|
+
const config = await useConfig();
|
|
359
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
360
|
+
const report = await analyzeLayering({
|
|
361
|
+
rootDir: cwd,
|
|
362
|
+
layerMap: config?.layers
|
|
363
|
+
});
|
|
364
|
+
const filtered = flags.package ? {
|
|
365
|
+
...report,
|
|
366
|
+
violations: report.violations.filter((v) => v.fromPackage.includes(flags.package)),
|
|
367
|
+
totalViolations: report.violations.filter((v) => v.fromPackage.includes(flags.package)).length
|
|
368
|
+
} : report;
|
|
369
|
+
if (flags.json) {
|
|
370
|
+
ctx.ui?.json?.(filtered);
|
|
371
|
+
return { exitCode: filtered.totalViolations > 0 ? 1 : 0 };
|
|
372
|
+
}
|
|
373
|
+
if (filtered.totalViolations === 0) {
|
|
374
|
+
ctx.ui?.success?.("No layering violations found");
|
|
375
|
+
return { exitCode: 0 };
|
|
376
|
+
}
|
|
377
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const v of filtered.violations) {
|
|
379
|
+
const list = byPackage.get(v.fromPackage) ?? [];
|
|
380
|
+
list.push(v);
|
|
381
|
+
byPackage.set(v.fromPackage, list);
|
|
382
|
+
}
|
|
383
|
+
const sections = [...byPackage.entries()].map(([pkg, vs]) => ({
|
|
384
|
+
header: `${pkg} (Layer ${vs[0]?.fromLayer})`,
|
|
385
|
+
items: vs.map((v) => `\u2192 ${v.toPackage} (Layer ${v.toLayer}) ${v.importSpecifier}`)
|
|
386
|
+
}));
|
|
387
|
+
ctx.ui?.error?.(`${filtered.totalViolations} layering violation(s) in ${filtered.affectedPackages.length} package(s)`, { sections });
|
|
388
|
+
return { exitCode: 1 };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
var coupling_default = defineCommand({
|
|
393
|
+
id: "quality:coupling",
|
|
394
|
+
description: "Show coupling metrics per package (afferent/efferent/instability)",
|
|
395
|
+
handler: {
|
|
396
|
+
async execute(ctx, input) {
|
|
397
|
+
const { flags } = input;
|
|
398
|
+
const config = await useConfig();
|
|
399
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
400
|
+
const report = analyzeCoupling({ rootDir: cwd, layerMap: config?.layers, topN: flags.top });
|
|
401
|
+
if (flags.json) {
|
|
402
|
+
ctx.ui?.json?.(report);
|
|
403
|
+
return { exitCode: 0 };
|
|
404
|
+
}
|
|
405
|
+
const sorted = flags.sort === "coupled" ? [...report.packages].sort((a, b) => b.afferent + b.efferent - (a.afferent + a.efferent)) : [...report.packages].sort((a, b) => b.instability - a.instability);
|
|
406
|
+
const top = sorted.slice(0, flags.top ?? 10);
|
|
407
|
+
const threshold = config?.thresholds?.instability ?? 0.85;
|
|
408
|
+
const items = top.map((p) => {
|
|
409
|
+
const warn = p.instability > threshold ? " \u26A0 unstable" : "";
|
|
410
|
+
return `${p.name} Ca: ${p.afferent} Ce: ${p.efferent} I: ${p.instability.toFixed(2)}${warn}`;
|
|
411
|
+
});
|
|
412
|
+
ctx.ui?.success?.(`Coupling \u2014 avg instability: ${report.avgInstability.toFixed(2)}`, {
|
|
413
|
+
sections: [{ header: `Top ${top.length} by instability`, items }]
|
|
414
|
+
});
|
|
415
|
+
return { exitCode: 0 };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
405
419
|
var cycles_default = defineCommand({
|
|
406
420
|
id: "quality:cycles",
|
|
407
421
|
description: "Detect circular dependencies in monorepo",
|
|
@@ -481,220 +495,93 @@ function outputCycles(cycles, flags, ui) {
|
|
|
481
495
|
}
|
|
482
496
|
var dead_code_default = defineCommand({
|
|
483
497
|
id: "quality:dead-code",
|
|
484
|
-
description: "Detect
|
|
498
|
+
description: "Detect unused files, exports, and deps via knip",
|
|
485
499
|
handler: {
|
|
486
500
|
async execute(ctx, input) {
|
|
487
|
-
const {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
outputBackupList(backups, ui);
|
|
495
|
-
}
|
|
496
|
-
return { exitCode: 0, result: backups };
|
|
501
|
+
const { flags } = input;
|
|
502
|
+
const config = await useConfig();
|
|
503
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
504
|
+
const cfg = config ?? defaultQualityConfig;
|
|
505
|
+
if (!cfg.knip.enabled) {
|
|
506
|
+
ctx.ui?.success?.("knip is disabled in config (knip.enabled = false)");
|
|
507
|
+
return { exitCode: 0 };
|
|
497
508
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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 instanceof Error ? err.message : String(err) });
|
|
513
|
-
} else {
|
|
514
|
-
ui?.error?.(err instanceof Error ? err.message : String(err));
|
|
515
|
-
}
|
|
516
|
-
return { exitCode: 1 };
|
|
517
|
-
}
|
|
509
|
+
const report = await runKnip({ shell: ctx.api.shell, rootDir: cwd });
|
|
510
|
+
if (flags.json) {
|
|
511
|
+
ctx.ui?.json?.(report);
|
|
512
|
+
return { exitCode: report.totalIssues > 0 ? 1 : 0 };
|
|
518
513
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (cached) {
|
|
523
|
-
outputDeadCodeReport({ ...cached, cached: true }, flags, ui);
|
|
524
|
-
return { exitCode: cached.summary.totalDead > 0 ? 1 : 0, result: cached };
|
|
525
|
-
}
|
|
514
|
+
if (report.totalIssues === 0) {
|
|
515
|
+
ctx.ui?.success?.("No dead code detected");
|
|
516
|
+
return { exitCode: 0 };
|
|
526
517
|
}
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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"]
|
|
518
|
+
const sections = [];
|
|
519
|
+
if (report.unusedFiles.length > 0) {
|
|
520
|
+
sections.push({
|
|
521
|
+
header: `Unused files (${report.unusedFiles.length})`,
|
|
522
|
+
items: report.unusedFiles
|
|
552
523
|
});
|
|
553
|
-
if (flags.json) {
|
|
554
|
-
ui?.json?.(removalResult);
|
|
555
|
-
} else {
|
|
556
|
-
outputRemovalReport(removalResult, flags, ui);
|
|
557
|
-
}
|
|
558
|
-
return { exitCode: 0, result: removalResult };
|
|
559
524
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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)`);
|
|
525
|
+
if (report.unusedExports.length > 0) {
|
|
526
|
+
sections.push({
|
|
527
|
+
header: `Unused exports (${report.unusedExports.length})`,
|
|
528
|
+
items: report.unusedExports.map((e) => `${e.file} ${e.symbol}`)
|
|
529
|
+
});
|
|
582
530
|
}
|
|
583
|
-
if (
|
|
584
|
-
|
|
531
|
+
if (report.unusedDependencies.length > 0) {
|
|
532
|
+
sections.push({
|
|
533
|
+
header: `Unused dependencies (${report.unusedDependencies.length})`,
|
|
534
|
+
items: report.unusedDependencies.map((d) => `${d.package} in ${d.workspace}`)
|
|
535
|
+
});
|
|
585
536
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 });
|
|
537
|
+
if (report.unlistedDependencies.length > 0) {
|
|
538
|
+
sections.push({
|
|
539
|
+
header: `Unlisted dependencies (${report.unlistedDependencies.length})`,
|
|
540
|
+
items: report.unlistedDependencies.map((d) => `${d.package} in ${d.workspace}`)
|
|
541
|
+
});
|
|
597
542
|
}
|
|
543
|
+
ctx.ui?.error?.(`${report.totalIssues} dead code issue(s) found`, { sections });
|
|
544
|
+
return { exitCode: 1 };
|
|
598
545
|
}
|
|
599
546
|
}
|
|
600
|
-
|
|
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
|
-
}
|
|
547
|
+
});
|
|
690
548
|
var fix_deps_default = defineCommand({
|
|
691
549
|
id: "quality:fix-deps",
|
|
692
550
|
description: "Auto-fix dependency issues",
|
|
693
551
|
handler: {
|
|
552
|
+
async intent(_ctx, input) {
|
|
553
|
+
const flags = input.flags;
|
|
554
|
+
const removeUnused = flags["remove-unused"] || flags.all;
|
|
555
|
+
const addMissing = flags["add-missing"] || flags.all;
|
|
556
|
+
const alignVersions = flags["align-versions"] || flags.all;
|
|
557
|
+
const packages = findPackages(_ctx.cwd);
|
|
558
|
+
const preview = await runAnalysis(packages, { removeUnused, addMissing, alignVersions, dryRun: true });
|
|
559
|
+
const operations = [
|
|
560
|
+
...preview.alignedDeps.map((a) => ({
|
|
561
|
+
type: "update",
|
|
562
|
+
resource: "dependency",
|
|
563
|
+
details: { dep: a.dep, from: a.from, to: a.to, packages: a.packages }
|
|
564
|
+
})),
|
|
565
|
+
...preview.addedDeps.map((a) => ({
|
|
566
|
+
type: "create",
|
|
567
|
+
resource: "dependency",
|
|
568
|
+
details: { package: a.package, dep: a.dep, version: a.version }
|
|
569
|
+
})),
|
|
570
|
+
...preview.removedDeps.map((r) => ({
|
|
571
|
+
type: "delete",
|
|
572
|
+
resource: "dependency",
|
|
573
|
+
details: { package: r.package, dep: r.dep }
|
|
574
|
+
}))
|
|
575
|
+
];
|
|
576
|
+
const total = operations.length;
|
|
577
|
+
return {
|
|
578
|
+
summary: total > 0 ? `Fix ${total} dependency issue(s) across ${preview.packagesScanned} packages` : `No dependency issues found in ${preview.packagesScanned} packages`,
|
|
579
|
+
operations
|
|
580
|
+
};
|
|
581
|
+
},
|
|
694
582
|
async execute(ctx, input) {
|
|
695
583
|
const { ui, platform } = ctx;
|
|
696
|
-
const flags =
|
|
697
|
-
const dryRun = flags["dry-run"] ?? false;
|
|
584
|
+
const flags = input.flags;
|
|
698
585
|
const removeUnused = flags["remove-unused"] || flags.all;
|
|
699
586
|
const addMissing = flags["add-missing"] || flags.all;
|
|
700
587
|
const alignVersions = flags["align-versions"] || flags.all;
|
|
@@ -705,52 +592,45 @@ var fix_deps_default = defineCommand({
|
|
|
705
592
|
return { exitCode: 0 };
|
|
706
593
|
}
|
|
707
594
|
if (!removeUnused && !addMissing && !alignVersions) {
|
|
708
|
-
|
|
595
|
+
validationError(ctx, "No fix options specified", "Use --remove-unused, --add-missing, --align-versions, or --all", flags.json);
|
|
709
596
|
return { exitCode: 1 };
|
|
710
597
|
}
|
|
711
598
|
const loader = useLoader("Scanning packages...");
|
|
712
599
|
loader.start();
|
|
713
|
-
const result = {
|
|
714
|
-
packagesScanned: 0,
|
|
715
|
-
removedDeps: [],
|
|
716
|
-
addedDeps: [],
|
|
717
|
-
alignedDeps: [],
|
|
718
|
-
dryRun
|
|
719
|
-
};
|
|
720
600
|
const packages = findPackages(ctx.cwd);
|
|
721
|
-
result.packagesScanned = packages.length;
|
|
722
601
|
loader.update({ text: `Scanned ${packages.length} packages` });
|
|
723
|
-
|
|
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
|
-
}
|
|
602
|
+
const result = await runAnalysis(packages, { removeUnused, addMissing, alignVersions, dryRun: false });
|
|
738
603
|
loader.succeed("Dependency analysis completed");
|
|
739
604
|
await platform.analytics.track("quality:fix-deps", {
|
|
740
|
-
dryRun,
|
|
605
|
+
dryRun: false,
|
|
741
606
|
packagesScanned: result.packagesScanned,
|
|
742
607
|
removedCount: result.removedDeps.length,
|
|
743
608
|
addedCount: result.addedDeps.length,
|
|
744
609
|
alignedCount: result.alignedDeps.length
|
|
745
610
|
});
|
|
746
|
-
outputResults(result, flags, ui);
|
|
747
|
-
return {
|
|
748
|
-
exitCode: 0,
|
|
749
|
-
result
|
|
750
|
-
};
|
|
611
|
+
outputResults({ ...result, dryRun: false }, flags, ui);
|
|
612
|
+
return { exitCode: 0, result };
|
|
751
613
|
}
|
|
752
614
|
}
|
|
753
615
|
});
|
|
616
|
+
async function runAnalysis(packages, opts) {
|
|
617
|
+
const result = {
|
|
618
|
+
packagesScanned: packages.length,
|
|
619
|
+
removedDeps: [],
|
|
620
|
+
addedDeps: [],
|
|
621
|
+
alignedDeps: []
|
|
622
|
+
};
|
|
623
|
+
if (opts.alignVersions) {
|
|
624
|
+
result.alignedDeps = await alignDuplicateVersions(packages, opts.dryRun);
|
|
625
|
+
}
|
|
626
|
+
if (opts.addMissing) {
|
|
627
|
+
result.addedDeps = await addMissingWorkspaceDeps(packages, opts.dryRun);
|
|
628
|
+
}
|
|
629
|
+
if (opts.removeUnused) {
|
|
630
|
+
result.removedDeps = await removeUnusedDeps(packages, opts.dryRun);
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
754
634
|
function findPackages(rootDir) {
|
|
755
635
|
const packages = [];
|
|
756
636
|
if (!fs.existsSync(rootDir)) {
|
|
@@ -761,8 +641,8 @@ function findPackages(rootDir) {
|
|
|
761
641
|
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
762
642
|
continue;
|
|
763
643
|
}
|
|
764
|
-
const repoPath =
|
|
765
|
-
const packagesDir =
|
|
644
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
645
|
+
const packagesDir = path.join(repoPath, "packages");
|
|
766
646
|
if (!fs.existsSync(packagesDir)) {
|
|
767
647
|
continue;
|
|
768
648
|
}
|
|
@@ -771,7 +651,7 @@ function findPackages(rootDir) {
|
|
|
771
651
|
if (!pkgDir.isDirectory()) {
|
|
772
652
|
continue;
|
|
773
653
|
}
|
|
774
|
-
const packageJsonPath =
|
|
654
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
|
|
775
655
|
if (fs.existsSync(packageJsonPath)) {
|
|
776
656
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
777
657
|
packages.push({
|
|
@@ -845,12 +725,7 @@ async function alignDuplicateVersions(packages, dryRun) {
|
|
|
845
725
|
continue;
|
|
846
726
|
}
|
|
847
727
|
for (const { version, packages: pkgNames } of versionCounts.slice(1)) {
|
|
848
|
-
aligned.push({
|
|
849
|
-
dep,
|
|
850
|
-
from: version,
|
|
851
|
-
to: targetVersion,
|
|
852
|
-
packages: pkgNames
|
|
853
|
-
});
|
|
728
|
+
aligned.push({ dep, from: version, to: targetVersion, packages: pkgNames });
|
|
854
729
|
if (!dryRun) {
|
|
855
730
|
for (const pkgName of pkgNames) {
|
|
856
731
|
const pkg = packages.find((p) => p.name === pkgName);
|
|
@@ -879,8 +754,8 @@ async function addMissingWorkspaceDeps(packages, dryRun) {
|
|
|
879
754
|
}
|
|
880
755
|
}
|
|
881
756
|
for (const pkg of packages) {
|
|
882
|
-
const packageDir =
|
|
883
|
-
const srcDir =
|
|
757
|
+
const packageDir = path.dirname(pkg.path);
|
|
758
|
+
const srcDir = path.join(packageDir, "src");
|
|
884
759
|
if (!fs.existsSync(srcDir)) {
|
|
885
760
|
continue;
|
|
886
761
|
}
|
|
@@ -891,11 +766,7 @@ async function addMissingWorkspaceDeps(packages, dryRun) {
|
|
|
891
766
|
};
|
|
892
767
|
for (const imp of imports) {
|
|
893
768
|
if (imp.startsWith("@kb-labs/") && workspacePackages.has(imp) && !currentDeps[imp]) {
|
|
894
|
-
added.push({
|
|
895
|
-
package: pkg.name,
|
|
896
|
-
dep: imp,
|
|
897
|
-
version: "workspace:*"
|
|
898
|
-
});
|
|
769
|
+
added.push({ package: pkg.name, dep: imp, version: "workspace:*" });
|
|
899
770
|
if (!dryRun) {
|
|
900
771
|
if (!pkg.json.dependencies) {
|
|
901
772
|
pkg.json.dependencies = {};
|
|
@@ -911,8 +782,8 @@ async function addMissingWorkspaceDeps(packages, dryRun) {
|
|
|
911
782
|
async function removeUnusedDeps(packages, dryRun) {
|
|
912
783
|
const removed = [];
|
|
913
784
|
for (const pkg of packages) {
|
|
914
|
-
const packageDir =
|
|
915
|
-
const srcDir =
|
|
785
|
+
const packageDir = path.dirname(pkg.path);
|
|
786
|
+
const srcDir = path.join(packageDir, "src");
|
|
916
787
|
if (!fs.existsSync(srcDir)) {
|
|
917
788
|
continue;
|
|
918
789
|
}
|
|
@@ -926,10 +797,7 @@ async function removeUnusedDeps(packages, dryRun) {
|
|
|
926
797
|
continue;
|
|
927
798
|
}
|
|
928
799
|
if (!imports.has(dep)) {
|
|
929
|
-
removed.push({
|
|
930
|
-
package: pkg.name,
|
|
931
|
-
dep
|
|
932
|
-
});
|
|
800
|
+
removed.push({ package: pkg.name, dep });
|
|
933
801
|
if (!dryRun) {
|
|
934
802
|
delete pkg.json.dependencies?.[dep];
|
|
935
803
|
delete pkg.json.devDependencies?.[dep];
|
|
@@ -945,7 +813,7 @@ function scanImports(dir) {
|
|
|
945
813
|
function walk(currentDir) {
|
|
946
814
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
947
815
|
for (const entry of entries) {
|
|
948
|
-
const fullPath =
|
|
816
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
949
817
|
if (entry.isDirectory()) {
|
|
950
818
|
walk(fullPath);
|
|
951
819
|
} else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
@@ -1005,7 +873,7 @@ function outputStats(stats, flags, ui) {
|
|
|
1005
873
|
});
|
|
1006
874
|
}
|
|
1007
875
|
ui?.success?.("Dependency statistics", {
|
|
1008
|
-
title: "
|
|
876
|
+
title: "Dependency Statistics",
|
|
1009
877
|
sections
|
|
1010
878
|
});
|
|
1011
879
|
}
|
|
@@ -1015,530 +883,245 @@ function outputResults(result, flags, ui) {
|
|
|
1015
883
|
return;
|
|
1016
884
|
}
|
|
1017
885
|
const sections = [];
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
`
|
|
1021
|
-
|
|
1022
|
-
sections.push({ header: "Summary", items: summaryItems });
|
|
886
|
+
sections.push({
|
|
887
|
+
header: "Summary",
|
|
888
|
+
items: [`Packages Scanned: ${result.packagesScanned}`]
|
|
889
|
+
});
|
|
1023
890
|
if (result.alignedDeps.length > 0) {
|
|
1024
|
-
const
|
|
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) });
|
|
891
|
+
const items = result.alignedDeps.slice(0, 10).map((a) => `${a.dep}: ${a.from} \u2192 ${a.to} (${a.packages.length} packages)`);
|
|
1028
892
|
if (result.alignedDeps.length > 10) {
|
|
1029
|
-
|
|
1030
|
-
if (lastSection) {
|
|
1031
|
-
lastSection.items.push(`... and ${result.alignedDeps.length - 10} more`);
|
|
1032
|
-
}
|
|
893
|
+
items.push(`... and ${result.alignedDeps.length - 10} more`);
|
|
1033
894
|
}
|
|
895
|
+
sections.push({ header: "Aligned Versions", items });
|
|
1034
896
|
}
|
|
1035
897
|
if (result.addedDeps.length > 0) {
|
|
1036
|
-
const
|
|
1037
|
-
sections.push({ header: "Added Dependencies", items: addedItems.slice(0, 10) });
|
|
898
|
+
const items = result.addedDeps.slice(0, 10).map((a) => `${a.package}: +${a.dep}`);
|
|
1038
899
|
if (result.addedDeps.length > 10) {
|
|
1039
|
-
|
|
1040
|
-
if (lastSection) {
|
|
1041
|
-
lastSection.items.push(`... and ${result.addedDeps.length - 10} more`);
|
|
1042
|
-
}
|
|
900
|
+
items.push(`... and ${result.addedDeps.length - 10} more`);
|
|
1043
901
|
}
|
|
902
|
+
sections.push({ header: "Added Dependencies", items });
|
|
1044
903
|
}
|
|
1045
904
|
if (result.removedDeps.length > 0) {
|
|
1046
|
-
const
|
|
1047
|
-
sections.push({ header: "Removed Dependencies", items: removedItems.slice(0, 10) });
|
|
905
|
+
const items = result.removedDeps.slice(0, 10).map((r) => `${r.package}: -${r.dep}`);
|
|
1048
906
|
if (result.removedDeps.length > 10) {
|
|
1049
|
-
|
|
1050
|
-
if (lastSection) {
|
|
1051
|
-
lastSection.items.push(`... and ${result.removedDeps.length - 10} more`);
|
|
1052
|
-
}
|
|
907
|
+
items.push(`... and ${result.removedDeps.length - 10} more`);
|
|
1053
908
|
}
|
|
909
|
+
sections.push({ header: "Removed Dependencies", items });
|
|
1054
910
|
}
|
|
1055
911
|
if (result.alignedDeps.length === 0 && result.addedDeps.length === 0 && result.removedDeps.length === 0) {
|
|
1056
|
-
sections.push({ header: "Result", items: ["
|
|
912
|
+
sections.push({ header: "Result", items: ["No issues found!"] });
|
|
1057
913
|
}
|
|
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
914
|
ui?.success?.("Dependency fix completed", {
|
|
1066
|
-
title,
|
|
915
|
+
title: "Dependency Fix Complete",
|
|
1067
916
|
sections
|
|
1068
917
|
});
|
|
1069
918
|
}
|
|
1070
919
|
var health_default = defineCommand({
|
|
1071
920
|
id: "quality:health",
|
|
1072
|
-
description: "
|
|
921
|
+
description: "Multidimensional health score: architecture, TypeScript, dead code, dependencies",
|
|
1073
922
|
handler: {
|
|
1074
923
|
async execute(ctx, input) {
|
|
1075
|
-
const {
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
await platform.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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;
|
|
924
|
+
const { flags } = input;
|
|
925
|
+
const config = await useConfig();
|
|
926
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
927
|
+
const cfg = config ?? defaultQualityConfig;
|
|
928
|
+
const cached = await ctx.platform.cache.get(CACHE_KEYS.HEALTH);
|
|
929
|
+
if (cached && !flags.refresh) {
|
|
930
|
+
const health2 = cached;
|
|
931
|
+
outputHealth(health2, flags, ctx);
|
|
932
|
+
return { exitCode: health2.score < cfg.thresholds.health ? 1 : 0 };
|
|
1112
933
|
}
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
packages.push(packageJsonPath);
|
|
1122
|
-
}
|
|
934
|
+
const [layering, types] = await Promise.all([
|
|
935
|
+
analyzeLayering({ rootDir: cwd, layerMap: cfg.layers }),
|
|
936
|
+
analyzeTypes$1(cwd)
|
|
937
|
+
]);
|
|
938
|
+
const coupling = analyzeCoupling({ rootDir: cwd, layerMap: cfg.layers });
|
|
939
|
+
let knip = { unusedFiles: [], unusedExports: [], unusedDependencies: [], unlistedDependencies: [], totalIssues: 0 };
|
|
940
|
+
if (cfg.knip.enabled) {
|
|
941
|
+
knip = await runKnip({ shell: ctx.api.shell, rootDir: cwd });
|
|
1123
942
|
}
|
|
943
|
+
const health = calculateHealth({
|
|
944
|
+
layering,
|
|
945
|
+
coupling,
|
|
946
|
+
types,
|
|
947
|
+
knip,
|
|
948
|
+
totalPackages: coupling.packages.length,
|
|
949
|
+
avgTestCoverage: null,
|
|
950
|
+
thresholds: cfg.thresholds
|
|
951
|
+
});
|
|
952
|
+
await ctx.platform.cache.set(CACHE_KEYS.HEALTH, health, CACHE_TTLS.FAST);
|
|
953
|
+
outputHealth(health, flags, ctx);
|
|
954
|
+
return { exitCode: health.score < cfg.thresholds.health ? 1 : 0 };
|
|
1124
955
|
}
|
|
1125
956
|
}
|
|
1126
|
-
|
|
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) {
|
|
957
|
+
});
|
|
958
|
+
function outputHealth(health, flags, ctx) {
|
|
1225
959
|
if (flags.json) {
|
|
1226
|
-
ui?.json?.(health);
|
|
960
|
+
ctx.ui?.json?.(health);
|
|
1227
961
|
return;
|
|
1228
962
|
}
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
|
963
|
+
const dimItems = Object.entries(health.dimensions).map(([key, dim]) => {
|
|
964
|
+
const details = dim.details.length > 0 ? `: ${dim.details.join(", ")}` : "";
|
|
965
|
+
return `${key} ${dim.score}/100 (${dim.grade})${details}`;
|
|
966
|
+
});
|
|
967
|
+
ctx.ui?.success?.(`Health score: ${health.score}/100 (${health.grade})`, {
|
|
968
|
+
sections: [{ header: "Dimensions", items: dimItems }]
|
|
1268
969
|
});
|
|
1269
970
|
}
|
|
1270
|
-
var
|
|
1271
|
-
id: "quality:
|
|
1272
|
-
description: "Show
|
|
971
|
+
var history_default = defineCommand({
|
|
972
|
+
id: "quality:history",
|
|
973
|
+
description: "Show quality snapshot history and trends",
|
|
1273
974
|
handler: {
|
|
1274
975
|
async execute(ctx, input) {
|
|
1275
|
-
const {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
976
|
+
const { flags } = input;
|
|
977
|
+
const config = await useConfig();
|
|
978
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
979
|
+
const store = new QualitySnapshotStore(cwd, config?.maxSnapshots ?? defaultQualityConfig.maxSnapshots);
|
|
980
|
+
const { snapshots, delta, latest } = store.history();
|
|
981
|
+
if (snapshots.length === 0) {
|
|
982
|
+
ctx.ui?.success?.("No snapshots yet. Run `kb quality snapshot` first.");
|
|
983
|
+
return { exitCode: 0 };
|
|
1283
984
|
}
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
985
|
+
const limit = flags.limit ?? 10;
|
|
986
|
+
const recent = snapshots.slice(-limit).reverse();
|
|
987
|
+
if (flags.json) {
|
|
988
|
+
ctx.ui?.json?.({ snapshots: recent, delta, latest });
|
|
989
|
+
return { exitCode: 0 };
|
|
1287
990
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
withHealth: flags.health ?? false
|
|
991
|
+
const items = recent.map((s) => {
|
|
992
|
+
const date = new Date(s.timestamp).toLocaleString();
|
|
993
|
+
const branch = s.git.branch ? ` [${s.git.branch}]` : "";
|
|
994
|
+
return `${date}${branch} \u2014 ${s.score}/100 (${s.grade}) violations: ${s.counters.layeringViolations} any: ${s.counters.anyCount}`;
|
|
1293
995
|
});
|
|
1294
|
-
|
|
996
|
+
const sections = [
|
|
997
|
+
{ header: `Last ${recent.length} snapshots`, items }
|
|
998
|
+
];
|
|
999
|
+
if (delta) {
|
|
1000
|
+
const sign = (n) => n > 0 ? `+${n}` : `${n}`;
|
|
1001
|
+
sections.push({
|
|
1002
|
+
header: "Delta vs previous snapshot",
|
|
1003
|
+
items: [
|
|
1004
|
+
`Score: ${sign(delta.score)}`,
|
|
1005
|
+
`Layering violations: ${sign(delta.layeringViolations)}`,
|
|
1006
|
+
`Any count: ${sign(delta.anyCount)}`,
|
|
1007
|
+
`Unused files: ${sign(delta.unusedFiles)}`,
|
|
1008
|
+
`Avg instability: ${sign(delta.avgInstability)}`
|
|
1009
|
+
]
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
ctx.ui?.success?.(`Quality history (${recent.length} snapshots)`, { sections });
|
|
1295
1013
|
return { exitCode: 0 };
|
|
1296
1014
|
}
|
|
1297
1015
|
}
|
|
1298
1016
|
});
|
|
1299
|
-
function
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
if (!
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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 };
|
|
1017
|
+
async function captureGit(shell, cwd) {
|
|
1018
|
+
try {
|
|
1019
|
+
const [c, b, m] = await Promise.all([
|
|
1020
|
+
shell.exec("git", ["rev-parse", "--short", "HEAD"], { cwd, throwOnError: false }),
|
|
1021
|
+
shell.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, throwOnError: false }),
|
|
1022
|
+
shell.exec("git", ["log", "-1", "--format=%s"], { cwd, throwOnError: false })
|
|
1023
|
+
]);
|
|
1024
|
+
if (!c.ok) return void 0;
|
|
1025
|
+
return { commit: c.stdout.trim(), branch: b.stdout.trim() || "HEAD", message: m.stdout.trim() };
|
|
1026
|
+
} catch {
|
|
1027
|
+
return void 0;
|
|
1331
1028
|
}
|
|
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
1029
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
|
1030
|
+
var snapshot_default = defineCommand({
|
|
1031
|
+
id: "quality:snapshot",
|
|
1032
|
+
description: "Collect all quality metrics and save a snapshot for trend tracking",
|
|
1033
|
+
handler: {
|
|
1034
|
+
async intent(_ctx, _input) {
|
|
1035
|
+
return {
|
|
1036
|
+
summary: "Collect all quality metrics and save snapshot for trend tracking",
|
|
1037
|
+
operations: [
|
|
1038
|
+
{ type: "create", resource: "file", details: { path: ".kb/quality/snapshots/<timestamp>.json" } }
|
|
1039
|
+
]
|
|
1400
1040
|
};
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1041
|
+
},
|
|
1042
|
+
async execute(ctx, input) {
|
|
1043
|
+
const { flags } = input;
|
|
1044
|
+
const config = await useConfig();
|
|
1045
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1046
|
+
const cfg = config ?? defaultQualityConfig;
|
|
1047
|
+
ctx.ui?.success?.("Collecting quality metrics\u2026");
|
|
1048
|
+
const [layering, coupling, types, git] = await Promise.all([
|
|
1049
|
+
analyzeLayering({ rootDir: cwd, layerMap: cfg.layers }),
|
|
1050
|
+
Promise.resolve(analyzeCoupling({ rootDir: cwd, layerMap: cfg.layers })),
|
|
1051
|
+
analyzeTypes$1(cwd),
|
|
1052
|
+
captureGit(ctx.api.shell, cwd)
|
|
1053
|
+
]);
|
|
1054
|
+
let knip = { unusedFiles: [], unusedExports: [], unusedDependencies: [], unlistedDependencies: [], totalIssues: 0 };
|
|
1055
|
+
if (cfg.knip.enabled) {
|
|
1056
|
+
knip = await runKnip({ shell: ctx.api.shell, rootDir: cwd });
|
|
1416
1057
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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}`);
|
|
1058
|
+
const totalPackages = coupling.packages.length;
|
|
1059
|
+
const health = calculateHealth({
|
|
1060
|
+
layering,
|
|
1061
|
+
coupling,
|
|
1062
|
+
types,
|
|
1063
|
+
knip,
|
|
1064
|
+
totalPackages,
|
|
1065
|
+
avgTestCoverage: null,
|
|
1066
|
+
thresholds: cfg.thresholds
|
|
1491
1067
|
});
|
|
1068
|
+
const store = new QualitySnapshotStore(cwd, cfg.maxSnapshots);
|
|
1069
|
+
const snap = store.save({
|
|
1070
|
+
score: health.score,
|
|
1071
|
+
grade: health.grade,
|
|
1072
|
+
dimensions: health.dimensions,
|
|
1073
|
+
counters: {
|
|
1074
|
+
layeringViolations: layering.totalViolations,
|
|
1075
|
+
avgInstability: coupling.avgInstability,
|
|
1076
|
+
anyCount: types.packages.reduce((s, p) => s + p.anyCount, 0),
|
|
1077
|
+
tsIgnoreCount: types.packages.reduce((s, p) => s + p.tsIgnoreCount, 0),
|
|
1078
|
+
unusedFiles: knip.unusedFiles.length,
|
|
1079
|
+
unusedDeps: knip.unusedDependencies.length
|
|
1080
|
+
},
|
|
1081
|
+
git: git ?? { commit: "", branch: "", message: "" }
|
|
1082
|
+
});
|
|
1083
|
+
if (flags.json) {
|
|
1084
|
+
ctx.ui?.json?.(snap);
|
|
1085
|
+
} else {
|
|
1086
|
+
ctx.ui?.success?.(`Snapshot saved: score ${snap.score}/100 (${snap.grade})`, {
|
|
1087
|
+
sections: [{
|
|
1088
|
+
header: "Dimensions",
|
|
1089
|
+
items: Object.entries(snap.dimensions).map(
|
|
1090
|
+
([k, d]) => `${k} ${d.score}/100 (${d.grade})`
|
|
1091
|
+
)
|
|
1092
|
+
}]
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
return { exitCode: 0 };
|
|
1492
1096
|
}
|
|
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
1097
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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}`);
|
|
1098
|
+
});
|
|
1099
|
+
var stats_default = defineCommand({
|
|
1100
|
+
id: "quality:stats",
|
|
1101
|
+
description: "Monorepo statistics: packages, lines of code, size",
|
|
1102
|
+
handler: {
|
|
1103
|
+
async execute(ctx, input) {
|
|
1104
|
+
const { flags } = input;
|
|
1105
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1106
|
+
const stats = await calculateStats(cwd);
|
|
1107
|
+
if (flags.json) {
|
|
1108
|
+
ctx.ui?.json?.(stats);
|
|
1109
|
+
return { exitCode: 0 };
|
|
1522
1110
|
}
|
|
1523
|
-
|
|
1111
|
+
ctx.ui?.success?.("Monorepo statistics", {
|
|
1112
|
+
sections: [{
|
|
1113
|
+
header: "Overview",
|
|
1114
|
+
items: [
|
|
1115
|
+
`Packages: ${stats.packages}`,
|
|
1116
|
+
`Lines of code: ${stats.loc.toLocaleString()}`,
|
|
1117
|
+
`Size: ${stats.sizeFormatted}`
|
|
1118
|
+
]
|
|
1119
|
+
}]
|
|
1120
|
+
});
|
|
1121
|
+
return { exitCode: 0 };
|
|
1524
1122
|
}
|
|
1525
1123
|
}
|
|
1526
|
-
|
|
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
|
-
}
|
|
1124
|
+
});
|
|
1542
1125
|
var visualize_default = defineCommand({
|
|
1543
1126
|
id: "quality:visualize",
|
|
1544
1127
|
description: "Visualize dependency graph",
|
|
@@ -1556,9 +1139,9 @@ var visualize_default = defineCommand({
|
|
|
1556
1139
|
} else if (flags.impact && flags.package) {
|
|
1557
1140
|
outputImpact(graph, flags.package, flags, ui);
|
|
1558
1141
|
} else if (flags.stats) {
|
|
1559
|
-
|
|
1142
|
+
outputStats2(graph, flags, ui);
|
|
1560
1143
|
} else {
|
|
1561
|
-
|
|
1144
|
+
outputStats2(graph, flags, ui);
|
|
1562
1145
|
}
|
|
1563
1146
|
return { exitCode: 0 };
|
|
1564
1147
|
}
|
|
@@ -1664,7 +1247,7 @@ function outputImpact(graph, packageName, flags, ui) {
|
|
|
1664
1247
|
sections
|
|
1665
1248
|
});
|
|
1666
1249
|
}
|
|
1667
|
-
function
|
|
1250
|
+
function outputStats2(graph, flags, ui) {
|
|
1668
1251
|
const { nodes } = graph;
|
|
1669
1252
|
let totalDeps = 0;
|
|
1670
1253
|
let maxDeps = 0;
|
|
@@ -1742,6 +1325,6 @@ function outputDot(graph, flags, ui) {
|
|
|
1742
1325
|
}
|
|
1743
1326
|
}
|
|
1744
1327
|
|
|
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 };
|
|
1328
|
+
export { build_order_default as buildOrder, check_builds_default as checkBuilds, check_layers_default as checkLayers, check_tests_default as checkTests, check_types_default as checkTypes, coupling_default as coupling, cycles_default as cycles, dead_code_default as deadCode, fix_deps_default as fixDeps, health_default as health, history_default as history, snapshot_default as snapshot, stats_default as stats, visualize_default as visualize };
|
|
1746
1329
|
//# sourceMappingURL=index.js.map
|
|
1747
1330
|
//# sourceMappingURL=index.js.map
|