@lunanoir/dep-lens 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/ansi.js +33 -0
- package/dist/args.js +14 -0
- package/dist/cli.js +17 -0
- package/dist/config.js +35 -0
- package/dist/detect.js +26 -0
- package/dist/postinstall.js +146 -0
- package/dist/selftest.js +106 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# dep-lens
|
|
2
2
|
|
|
3
|
-
Scan
|
|
4
|
-
|
|
3
|
+
Scan your project's third-party dependencies across **9 ecosystems** (npm,
|
|
4
|
+
Cargo, Go, Python, Ruby, PHP, Java, Dart/Flutter, C/C++), classify every
|
|
5
|
+
license (permissive / weak copyleft / strong copyleft / unknown), score
|
|
6
|
+
commercial-use risk, and browse the results in a fast, colorful terminal UI
|
|
7
|
+
or as JSON/CSV/Markdown/HTML for CI.
|
|
5
8
|
|
|
6
9
|
```sh
|
|
7
|
-
npm install -g dep-lens
|
|
10
|
+
npm install -g @lunanoir/dep-lens
|
|
8
11
|
|
|
9
12
|
dep-lens # interactive TUI
|
|
13
|
+
dep-lens --tr # Turkish UI (Turkce arayuz)
|
|
10
14
|
dep-lens --json # raw JSON to stdout
|
|
11
15
|
dep-lens --html report.html # standalone HTML report
|
|
16
|
+
dep-lens --test # self-check: verify the scanner on this project
|
|
12
17
|
dep-lens --fail-on gpl # exit 1 when strong copyleft is found (CI gate)
|
|
13
18
|
dep-lens --path ../my-app --ignore left-pad
|
|
14
19
|
```
|
|
15
20
|
|
|
16
|
-
Full documentation: https://github.com/
|
|
21
|
+
Full documentation: https://github.com/lunanoir21/dep-lens
|
|
17
22
|
|
|
18
23
|
This package contains the CLI and TUI; the native scanner binary is delivered
|
|
19
24
|
through a platform-specific optional dependency (Linux x64, macOS x64/arm64,
|
package/dist/ansi.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal truecolor ANSI helpers for plain-console output (postinstall
|
|
3
|
+
* wizard, --test report) where rendering a full Ink tree would be overkill.
|
|
4
|
+
* Colors mirror `ui/theme.ts` PALETTE. No-ops when stdout is not a TTY.
|
|
5
|
+
*/
|
|
6
|
+
function hexToRgb(hex) {
|
|
7
|
+
const value = hex.replace('#', '');
|
|
8
|
+
return [
|
|
9
|
+
parseInt(value.slice(0, 2), 16),
|
|
10
|
+
parseInt(value.slice(2, 4), 16),
|
|
11
|
+
parseInt(value.slice(4, 6), 16),
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
function colorize(text, hex) {
|
|
15
|
+
if (!process.stdout.isTTY) {
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
const [r, g, b] = hexToRgb(hex);
|
|
19
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
20
|
+
}
|
|
21
|
+
export const good = (text) => colorize(text, '#4ade80');
|
|
22
|
+
export const ok = (text) => colorize(text, '#fbbf24');
|
|
23
|
+
export const bad = (text) => colorize(text, '#fb7185');
|
|
24
|
+
export const unknown = (text) => colorize(text, '#94a3b8');
|
|
25
|
+
export const brand = (text) => colorize(text, '#38bdf8');
|
|
26
|
+
export const accent = (text) => colorize(text, '#a78bfa');
|
|
27
|
+
export const dim = (text) => colorize(text, '#64748b');
|
|
28
|
+
export function bold(text) {
|
|
29
|
+
if (!process.stdout.isTTY) {
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
return `\x1b[1m${text}\x1b[22m`;
|
|
33
|
+
}
|
package/dist/args.js
CHANGED
|
@@ -14,6 +14,10 @@ OPTIONS:
|
|
|
14
14
|
--path <DIR> Project directory to scan (default: current directory)
|
|
15
15
|
--ignore <NAMES> Comma-separated package names to exclude (repeatable)
|
|
16
16
|
--tr Turkish UI (Turkce arayuz)
|
|
17
|
+
--test Run a self-check: verify the scanner binary and
|
|
18
|
+
report which ecosystems it detects in --path
|
|
19
|
+
--setup Re-run the interactive setup wizard (language,
|
|
20
|
+
PATH check) even outside of npm install
|
|
17
21
|
--help Show this help
|
|
18
22
|
--version Show version
|
|
19
23
|
|
|
@@ -43,8 +47,11 @@ export function parseArgs(argv) {
|
|
|
43
47
|
path: '.',
|
|
44
48
|
ignore: [],
|
|
45
49
|
locale: 'en',
|
|
50
|
+
localeExplicit: false,
|
|
46
51
|
help: false,
|
|
47
52
|
version: false,
|
|
53
|
+
test: false,
|
|
54
|
+
setup: false,
|
|
48
55
|
};
|
|
49
56
|
for (let i = 0; i < argv.length; i += 1) {
|
|
50
57
|
const arg = argv[i];
|
|
@@ -89,6 +96,13 @@ export function parseArgs(argv) {
|
|
|
89
96
|
}
|
|
90
97
|
case '--tr':
|
|
91
98
|
options.locale = 'tr';
|
|
99
|
+
options.localeExplicit = true;
|
|
100
|
+
break;
|
|
101
|
+
case '--test':
|
|
102
|
+
options.test = true;
|
|
103
|
+
break;
|
|
104
|
+
case '--setup':
|
|
105
|
+
options.setup = true;
|
|
92
106
|
break;
|
|
93
107
|
case '--help':
|
|
94
108
|
case '-h':
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,9 @@ import React from 'react';
|
|
|
5
5
|
import { render } from 'ink';
|
|
6
6
|
import { parseArgs, USAGE } from './args.js';
|
|
7
7
|
import { renderCsv, renderHtml, renderMarkdown, runScan } from './bridge.js';
|
|
8
|
+
import { readConfig } from './config.js';
|
|
9
|
+
import { main as runSetupWizard } from './postinstall.js';
|
|
10
|
+
import { runSelfTest } from './selftest.js';
|
|
8
11
|
import { violations } from './utils.js';
|
|
9
12
|
import { Root } from './ui/Root.js';
|
|
10
13
|
function packageVersion() {
|
|
@@ -44,7 +47,21 @@ async function main() {
|
|
|
44
47
|
process.stdout.write(`dep-lens ${packageVersion()}\n`);
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
50
|
+
if (!options.localeExplicit) {
|
|
51
|
+
const config = await readConfig();
|
|
52
|
+
if (config.locale !== undefined) {
|
|
53
|
+
options.locale = config.locale;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (options.setup) {
|
|
57
|
+
await runSetupWizard(true);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
47
60
|
const scanOptions = { path: options.path, ignore: options.ignore, locale: options.locale };
|
|
61
|
+
if (options.test) {
|
|
62
|
+
process.exitCode = await runSelfTest(scanOptions);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
48
65
|
if (options.json) {
|
|
49
66
|
const report = await runScan(scanOptions);
|
|
50
67
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function configDir() {
|
|
5
|
+
const xdg = process.env['XDG_CONFIG_HOME'];
|
|
6
|
+
const base = xdg && xdg.length > 0 ? xdg : path.join(homedir(), '.config');
|
|
7
|
+
return path.join(base, 'dep-lens');
|
|
8
|
+
}
|
|
9
|
+
export function configPath() {
|
|
10
|
+
return path.join(configDir(), 'config.json');
|
|
11
|
+
}
|
|
12
|
+
/** Read the user config; returns `{}` if missing or unreadable/invalid. */
|
|
13
|
+
export async function readConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const record = parsed;
|
|
21
|
+
const config = {};
|
|
22
|
+
if (record['locale'] === 'en' || record['locale'] === 'tr') {
|
|
23
|
+
config.locale = record['locale'];
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Write the user config, creating `~/.config/dep-lens/` if needed. */
|
|
32
|
+
export async function writeConfig(config) {
|
|
33
|
+
await mkdir(configDir(), { recursive: true });
|
|
34
|
+
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
35
|
+
}
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Manifest files dep-lens looks for, one entry per supported ecosystem. */
|
|
4
|
+
export const ECOSYSTEM_SIGNATURES = [
|
|
5
|
+
{ id: 'npm', label: 'npm / yarn / pnpm', files: ['package.json'] },
|
|
6
|
+
{ id: 'cargo', label: 'Cargo', files: ['Cargo.toml'] },
|
|
7
|
+
{ id: 'go', label: 'Go', files: ['go.mod'] },
|
|
8
|
+
{
|
|
9
|
+
id: 'python',
|
|
10
|
+
label: 'Python',
|
|
11
|
+
files: ['pyproject.toml', 'requirements.txt', 'Pipfile', 'poetry.lock', 'uv.lock'],
|
|
12
|
+
},
|
|
13
|
+
{ id: 'ruby', label: 'Ruby', files: ['Gemfile', 'Gemfile.lock'] },
|
|
14
|
+
{ id: 'php', label: 'PHP', files: ['composer.json'] },
|
|
15
|
+
{
|
|
16
|
+
id: 'java',
|
|
17
|
+
label: 'Java',
|
|
18
|
+
files: ['pom.xml', 'build.gradle', 'build.gradle.kts', 'gradle.lockfile'],
|
|
19
|
+
},
|
|
20
|
+
{ id: 'dart', label: 'Dart / Flutter', files: ['pubspec.yaml'] },
|
|
21
|
+
{ id: 'cpp', label: 'C/C++', files: ['vcpkg.json', 'conanfile.txt'] },
|
|
22
|
+
];
|
|
23
|
+
/** Ecosystem signatures whose manifest files are present under `dir`. */
|
|
24
|
+
export function detectEcosystems(dir) {
|
|
25
|
+
return ECOSYSTEM_SIGNATURES.filter((eco) => eco.files.some((file) => existsSync(path.join(dir, file))));
|
|
26
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall setup wizard. Runs once after `npm install`, detects the
|
|
4
|
+
* caller's project ecosystems, asks for a default UI language, and checks
|
|
5
|
+
* whether the global npm bin directory (where the `dep-lens` launcher
|
|
6
|
+
* lands) is on PATH.
|
|
7
|
+
*
|
|
8
|
+
* Skips entirely in non-interactive environments (CI, piped installs, or
|
|
9
|
+
* when npm doesn't attach a TTY to lifecycle scripts) so it never blocks an
|
|
10
|
+
* install.
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'node:child_process';
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
import { accent, bad, bold, brand, dim, good } from './ansi.js';
|
|
16
|
+
import { detectEcosystems } from './detect.js';
|
|
17
|
+
import { readConfig, writeConfig } from './config.js';
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
const PROMPT_TIMEOUT_MS = 15_000;
|
|
20
|
+
function isInteractive() {
|
|
21
|
+
if (process.env['CI'] === 'true' || process.env['CI'] === '1') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (process.env['DEP_LENS_SKIP_SETUP'] === '1') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
28
|
+
}
|
|
29
|
+
/** Ask a yes/no question with a default answer if the user just presses enter. */
|
|
30
|
+
async function askYesNo(question, defaultYes) {
|
|
31
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
const suffix = defaultYes ? 'Y/n' : 'y/N';
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
rl.close();
|
|
36
|
+
resolve(defaultYes);
|
|
37
|
+
}, PROMPT_TIMEOUT_MS);
|
|
38
|
+
rl.question(`${question} [${suffix}] `, (answer) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
rl.close();
|
|
41
|
+
const trimmed = answer.trim().toLowerCase();
|
|
42
|
+
if (trimmed === '') {
|
|
43
|
+
resolve(defaultYes);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
resolve(trimmed === 'y' || trimmed === 'yes' || trimmed === 'e' || trimmed === 'evet');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function npmGlobalBinDir() {
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execFileAsync('npm', ['bin', '-g']);
|
|
53
|
+
const dir = stdout.trim();
|
|
54
|
+
return dir.length > 0 ? dir : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execFileAsync('npm', ['prefix', '-g']);
|
|
59
|
+
const prefix = stdout.trim();
|
|
60
|
+
return prefix.length > 0 ? `${prefix}/bin` : null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function pathContains(dir) {
|
|
68
|
+
const pathEnv = process.env['PATH'] ?? '';
|
|
69
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
70
|
+
return pathEnv.split(sep).some((entry) => entry.replace(/[/\\]+$/, '') === dir.replace(/[/\\]+$/, ''));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run the setup wizard. `force` skips the TTY/CI checks (used by
|
|
74
|
+
* `dep-lens --setup`, where the user explicitly asked for it).
|
|
75
|
+
*/
|
|
76
|
+
export async function main(force = false) {
|
|
77
|
+
// Always print a one-line banner so even non-interactive installs show
|
|
78
|
+
// something useful, but never prompt unless we have a real TTY.
|
|
79
|
+
const cwd = process.env['INIT_CWD'] ?? process.cwd();
|
|
80
|
+
const detected = detectEcosystems(cwd);
|
|
81
|
+
process.stdout.write(`\n${bold(brand('dep-lens'))} installed.\n`);
|
|
82
|
+
if (detected.length > 0) {
|
|
83
|
+
const labels = detected.map((eco) => eco.label).join(', ');
|
|
84
|
+
process.stdout.write(`${dim('detected in this project:')} ${good(labels)}\n`);
|
|
85
|
+
}
|
|
86
|
+
if (!force && !isInteractive()) {
|
|
87
|
+
process.stdout.write(`${dim('run')} dep-lens ${dim('to scan, or')} dep-lens --help\n\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write(`\n${bold('Quick setup')} ${dim('(press enter to accept defaults)')}\n`);
|
|
91
|
+
// --- default language -----------------------------------------------
|
|
92
|
+
const config = await readConfig();
|
|
93
|
+
const wantsTurkish = await askYesNo(`${accent('?')} Use Turkish UI by default (--tr)?`, config.locale === 'tr');
|
|
94
|
+
config.locale = wantsTurkish ? 'tr' : 'en';
|
|
95
|
+
await writeConfig(config);
|
|
96
|
+
// --- PATH check --------------------------------------------------------
|
|
97
|
+
const binDir = await npmGlobalBinDir();
|
|
98
|
+
if (binDir !== null && !pathContains(binDir)) {
|
|
99
|
+
process.stdout.write(`\n${bad('!')} ${binDir} is not on your PATH, so ${bold('dep-lens')} may not run yet.\n`);
|
|
100
|
+
const addPath = await askYesNo(`${accent('?')} Add it to your shell profile now?`, true);
|
|
101
|
+
if (addPath) {
|
|
102
|
+
await appendToShellProfile(binDir);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
process.stdout.write(`${dim('add manually:')} export PATH="${binDir}:$PATH"\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(`\n${good('done.')} ${dim('run')} dep-lens ${dim('to get started.')}\n\n`);
|
|
109
|
+
}
|
|
110
|
+
async function appendToShellProfile(binDir) {
|
|
111
|
+
if (process.platform === 'win32') {
|
|
112
|
+
process.stdout.write(`${dim('on Windows, add this to your PATH via System Properties > Environment Variables:')}\n${binDir}\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { homedir } = await import('node:os');
|
|
116
|
+
const { appendFile, readFile } = await import('node:fs/promises');
|
|
117
|
+
const path = await import('node:path');
|
|
118
|
+
const shell = (process.env['SHELL'] ?? '').split('/').pop() ?? '';
|
|
119
|
+
const rcFile = shell === 'fish'
|
|
120
|
+
? path.join(homedir(), '.config', 'fish', 'config.fish')
|
|
121
|
+
: shell === 'zsh'
|
|
122
|
+
? path.join(homedir(), '.zshrc')
|
|
123
|
+
: path.join(homedir(), '.bashrc');
|
|
124
|
+
const line = shell === 'fish'
|
|
125
|
+
? `fish_add_path "${binDir}"`
|
|
126
|
+
: `export PATH="${binDir}:$PATH"`;
|
|
127
|
+
try {
|
|
128
|
+
const existing = await readFile(rcFile, 'utf8').catch(() => '');
|
|
129
|
+
if (existing.includes(binDir)) {
|
|
130
|
+
process.stdout.write(`${dim('already present in')} ${rcFile}\n`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await appendFile(rcFile, `\n# added by dep-lens postinstall\n${line}\n`);
|
|
134
|
+
process.stdout.write(`${good('added to')} ${rcFile}${dim(' - open a new terminal to apply.')}\n`);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
process.stdout.write(`${dim('could not update shell profile, add manually:')} ${line}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Only auto-run when invoked directly as the postinstall script, not when
|
|
141
|
+
// imported by `dep-lens --setup`.
|
|
142
|
+
if (process.argv[1]?.endsWith('postinstall.js')) {
|
|
143
|
+
main().catch(() => {
|
|
144
|
+
// Never fail the install over the setup wizard.
|
|
145
|
+
});
|
|
146
|
+
}
|
package/dist/selftest.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { bad, bold, brand, dim, good, ok } from './ansi.js';
|
|
5
|
+
import { resolveBinaryPath, runScan } from './bridge.js';
|
|
6
|
+
import { detectEcosystems } from './detect.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
function statusGlyph(passed) {
|
|
9
|
+
return passed ? good('PASS') : bad('FAIL');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `dep-lens --test`: verify the native scanner binary runs, scan
|
|
13
|
+
* `options.path`, and report which of the ecosystems detected by their
|
|
14
|
+
* manifest files actually produced packages. Returns the process exit code.
|
|
15
|
+
*/
|
|
16
|
+
export async function runSelfTest(options) {
|
|
17
|
+
const checks = [];
|
|
18
|
+
let exitCode = 0;
|
|
19
|
+
process.stdout.write(`${bold(brand('dep-lens --test'))} ${dim(`(${path.resolve(options.path)})`)}\n\n`);
|
|
20
|
+
// 1. Binary resolves and runs.
|
|
21
|
+
let binaryPath;
|
|
22
|
+
try {
|
|
23
|
+
binaryPath = resolveBinaryPath();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
checks.push({
|
|
27
|
+
label: 'native scanner binary',
|
|
28
|
+
ok: false,
|
|
29
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
30
|
+
});
|
|
31
|
+
printChecks(checks);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await execFileAsync(binaryPath, ['--version']);
|
|
36
|
+
checks.push({ label: 'native scanner binary', ok: true, detail: stdout.trim() });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
checks.push({
|
|
40
|
+
label: 'native scanner binary',
|
|
41
|
+
ok: false,
|
|
42
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
43
|
+
});
|
|
44
|
+
printChecks(checks);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
// 2. Run a scan.
|
|
48
|
+
let report;
|
|
49
|
+
try {
|
|
50
|
+
report = await runScan(options);
|
|
51
|
+
checks.push({
|
|
52
|
+
label: 'scan',
|
|
53
|
+
ok: true,
|
|
54
|
+
detail: `${report.summary.total} package(s) found`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
checks.push({
|
|
59
|
+
label: 'scan',
|
|
60
|
+
ok: false,
|
|
61
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
62
|
+
});
|
|
63
|
+
printChecks(checks);
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
// 3. Per-ecosystem: every manifest dep-lens recognizes in this project
|
|
67
|
+
// should have produced at least one package.
|
|
68
|
+
const detected = detectEcosystems(options.path);
|
|
69
|
+
const foundEcosystems = new Set(report.packages.map((pkg) => pkg.ecosystem));
|
|
70
|
+
for (const eco of detected) {
|
|
71
|
+
const found = foundEcosystems.has(eco.id);
|
|
72
|
+
const count = report.packages.filter((pkg) => pkg.ecosystem === eco.id).length;
|
|
73
|
+
if (!found) {
|
|
74
|
+
exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
checks.push({
|
|
77
|
+
label: eco.label,
|
|
78
|
+
ok: found,
|
|
79
|
+
detail: found ? `${count} package(s)` : 'manifest found but no packages reported',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (detected.length === 0) {
|
|
83
|
+
checks.push({
|
|
84
|
+
label: 'ecosystems',
|
|
85
|
+
ok: true,
|
|
86
|
+
detail: 'no recognized manifests in this directory',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// 4. Unknown licenses are a warning, not a failure.
|
|
90
|
+
if (report.summary.unknown > 0) {
|
|
91
|
+
checks.push({
|
|
92
|
+
label: 'license coverage',
|
|
93
|
+
ok: true,
|
|
94
|
+
detail: `${ok(String(report.summary.unknown))} package(s) with unknown license`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
printChecks(checks);
|
|
98
|
+
process.stdout.write(`\n${exitCode === 0 ? good('all checks passed') : bad('some checks failed')}\n`);
|
|
99
|
+
return exitCode;
|
|
100
|
+
}
|
|
101
|
+
function printChecks(checks) {
|
|
102
|
+
const width = Math.max(...checks.map((c) => c.label.length), 8);
|
|
103
|
+
for (const check of checks) {
|
|
104
|
+
process.stdout.write(` ${statusGlyph(check.ok)} ${check.label.padEnd(width)} ${dim(check.detail)}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunanoir/dep-lens",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Dependency license scanner across npm, Cargo, Go, Python, Ruby, PHP, Java, Dart, and C/C++ with commercial risk reporting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc",
|
|
35
|
-
"test": "tsc && node --test \"dist/test/**/*.test.js\""
|
|
35
|
+
"test": "tsc && node --test \"dist/test/**/*.test.js\"",
|
|
36
|
+
"postinstall": "node dist/postinstall.js"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"ink": "^5.2.1",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"optionalDependencies": {
|
|
42
43
|
"@lunanoir/dep-lens-darwin-arm64": "0.1.0",
|
|
43
44
|
"@lunanoir/dep-lens-darwin-x64": "0.1.0",
|
|
44
|
-
"@lunanoir/dep-lens-linux-x64": "0.1.
|
|
45
|
+
"@lunanoir/dep-lens-linux-x64": "0.1.1",
|
|
45
46
|
"@lunanoir/dep-lens-win32-x64": "0.1.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|