@lunanoir/dep-lens 0.1.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/utils.js ADDED
@@ -0,0 +1,169 @@
1
+ import { PALETTE } from './ui/theme.js';
2
+ export const SORT_COLUMNS = [
3
+ 'name',
4
+ 'version',
5
+ 'license',
6
+ 'category',
7
+ 'dependencyType',
8
+ 'riskScore',
9
+ 'commercialUse',
10
+ ];
11
+ const CATEGORY_ORDER = {
12
+ Permissive: 0,
13
+ WeakCopyleft: 1,
14
+ StrongCopyleft: 2,
15
+ Unknown: 3,
16
+ };
17
+ const COMMERCIAL_ORDER = {
18
+ yes: 0,
19
+ caution: 1,
20
+ restricted: 2,
21
+ review: 3,
22
+ };
23
+ /**
24
+ * Case-insensitive filter over package name, license, and category, with an
25
+ * optional exact category restriction (used by the TUI quick filters).
26
+ */
27
+ export function filterPackages(packages, query, category = null) {
28
+ const needle = query.trim().toLowerCase();
29
+ return packages.filter((pkg) => {
30
+ if (category !== null && pkg.category !== category) {
31
+ return false;
32
+ }
33
+ if (needle.length === 0) {
34
+ return true;
35
+ }
36
+ return (pkg.name.toLowerCase().includes(needle) ||
37
+ pkg.license.toLowerCase().includes(needle) ||
38
+ pkg.category.toLowerCase().includes(needle));
39
+ });
40
+ }
41
+ /** Stable sort by the given column; ties are broken by package name. */
42
+ export function sortPackages(packages, column, descending) {
43
+ const sorted = [...packages].sort((a, b) => {
44
+ let cmp;
45
+ switch (column) {
46
+ case 'riskScore':
47
+ cmp = a.riskScore - b.riskScore;
48
+ break;
49
+ case 'category':
50
+ cmp = CATEGORY_ORDER[a.category] - CATEGORY_ORDER[b.category];
51
+ break;
52
+ case 'commercialUse':
53
+ cmp = COMMERCIAL_ORDER[a.commercialUse] - COMMERCIAL_ORDER[b.commercialUse];
54
+ break;
55
+ case 'dependencyType':
56
+ cmp = a.dependencyType.localeCompare(b.dependencyType);
57
+ break;
58
+ default:
59
+ cmp = a[column].localeCompare(b[column]);
60
+ break;
61
+ }
62
+ if (descending) {
63
+ cmp = -cmp;
64
+ }
65
+ // Ties always fall back to ascending name order, regardless of direction.
66
+ if (cmp === 0) {
67
+ cmp = a.name.localeCompare(b.name);
68
+ }
69
+ return cmp;
70
+ });
71
+ return sorted;
72
+ }
73
+ /** Percentage with one decimal, e.g. "42.9". Returns "0.0" for empty sets. */
74
+ export function percent(part, total) {
75
+ if (total === 0) {
76
+ return '0.0';
77
+ }
78
+ return ((part / total) * 100).toFixed(1);
79
+ }
80
+ /**
81
+ * Calculate a weighted health score (0-100) for the project.
82
+ * Permissive: 1.0, Weak: 0.5, Unknown: 0.2, Strong: 0.0
83
+ */
84
+ export function calculateHealthScore(summary) {
85
+ if (summary.total === 0)
86
+ return 100;
87
+ const weighted = summary.permissive * 1.0 +
88
+ summary.weakCopyleft * 0.5 +
89
+ summary.unknown * 0.2 +
90
+ summary.strongCopyleft * 0.0;
91
+ return Math.round((weighted / summary.total) * 100);
92
+ }
93
+ /** Truncate to width, marking cut-off text with a two-dot ASCII ellipsis. */
94
+ export function truncate(text, width) {
95
+ if (text.length <= width) {
96
+ return text;
97
+ }
98
+ return width <= 2 ? text.slice(0, width) : `${text.slice(0, width - 2)}..`;
99
+ }
100
+ /** Truncate then right-pad to an exact column width. */
101
+ export function pad(text, width) {
102
+ return truncate(text, width).padEnd(width);
103
+ }
104
+ /** Dim horizontal rule used to separate sections, e.g. "──────────". */
105
+ export function buildSectionDivider(width) {
106
+ return '─'.repeat(Math.max(0, width));
107
+ }
108
+ const SEGMENT_STYLE = [
109
+ { category: 'Permissive', char: '#', color: PALETTE.good },
110
+ { category: 'WeakCopyleft', char: '=', color: PALETTE.ok },
111
+ { category: 'StrongCopyleft', char: '!', color: PALETTE.bad },
112
+ { category: 'Unknown', char: '?', color: PALETTE.unknown },
113
+ ];
114
+ /**
115
+ * Build a stacked horizontal ratio bar for the summary line. `progress`
116
+ * (0..1) scales the filled portion so the bar can grow as an entrance
117
+ * animation. Widths are distributed by largest remainder so they always sum
118
+ * to the filled width. Distinct fill characters keep the bar readable on
119
+ * monochrome terminals.
120
+ */
121
+ export function buildRatioSegments(summary, width, progress = 1) {
122
+ const counts = {
123
+ Permissive: summary.permissive,
124
+ WeakCopyleft: summary.weakCopyleft,
125
+ StrongCopyleft: summary.strongCopyleft,
126
+ Unknown: summary.unknown,
127
+ };
128
+ const filled = Math.round(width * Math.min(Math.max(progress, 0), 1));
129
+ if (summary.total === 0 || filled === 0) {
130
+ return [];
131
+ }
132
+ const exact = SEGMENT_STYLE.map((style) => ({
133
+ style,
134
+ value: (counts[style.category] / summary.total) * filled,
135
+ }));
136
+ const widths = exact.map((entry) => Math.floor(entry.value));
137
+ let remaining = filled - widths.reduce((sum, w) => sum + w, 0);
138
+ const order = exact
139
+ .map((entry, index) => ({ index, frac: entry.value - Math.floor(entry.value) }))
140
+ .sort((a, b) => b.frac - a.frac);
141
+ for (const { index } of order) {
142
+ if (remaining <= 0) {
143
+ break;
144
+ }
145
+ const current = widths[index] ?? 0;
146
+ widths[index] = current + 1;
147
+ remaining -= 1;
148
+ }
149
+ return SEGMENT_STYLE.map((style, index) => ({
150
+ category: style.category,
151
+ char: style.char,
152
+ color: style.color,
153
+ width: widths[index] ?? 0,
154
+ })).filter((segment) => segment.width > 0);
155
+ }
156
+ /**
157
+ * Packages that violate a --fail-on policy. "gpl" matches every strong
158
+ * copyleft license (GPL-2.0, GPL-3.0, AGPL-3.0); "agpl" matches AGPL only.
159
+ * Dual-licensed packages classified as permissive (e.g. "GPL-2.0 OR MIT")
160
+ * do not violate either policy.
161
+ */
162
+ export function violations(report, failOn) {
163
+ return report.packages.filter((pkg) => {
164
+ if (failOn === 'agpl') {
165
+ return pkg.category === 'StrongCopyleft' && pkg.license.toUpperCase().includes('AGPL');
166
+ }
167
+ return pkg.category === 'StrongCopyleft';
168
+ });
169
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@lunanoir/dep-lens",
3
+ "version": "0.1.0",
4
+ "description": "Dependency license scanner across npm, Cargo, Go, Python, Ruby, PHP, Java, Dart, and C/C++ with commercial risk reporting",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lunanoir21/dep-lens.git"
10
+ },
11
+ "bin": {
12
+ "dep-lens": "dist/cli.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "!dist/test"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.18"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "keywords": [
25
+ "license",
26
+ "spdx",
27
+ "compliance",
28
+ "dependencies",
29
+ "npm",
30
+ "cargo",
31
+ "cli"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc",
35
+ "test": "tsc && node --test \"dist/test/**/*.test.js\""
36
+ },
37
+ "dependencies": {
38
+ "ink": "^5.2.1",
39
+ "react": "^18.3.1"
40
+ },
41
+ "optionalDependencies": {
42
+ "@lunanoir/dep-lens-darwin-arm64": "0.1.0",
43
+ "@lunanoir/dep-lens-darwin-x64": "0.1.0",
44
+ "@lunanoir/dep-lens-linux-x64": "0.1.0",
45
+ "@lunanoir/dep-lens-win32-x64": "0.1.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.14.0",
49
+ "@types/react": "^18.3.3",
50
+ "typescript": "^5.5.0"
51
+ }
52
+ }