@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 CHANGED
@@ -1,19 +1,24 @@
1
1
  # dep-lens
2
2
 
3
- Scan the licenses of your npm/yarn/pnpm and Cargo dependencies and report
4
- commercial-use risk, in an interactive terminal UI or as JSON/HTML for CI.
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/dep-lens/dep-lens
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
+ }
@@ -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.0",
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.0",
45
+ "@lunanoir/dep-lens-linux-x64": "0.1.1",
45
46
  "@lunanoir/dep-lens-win32-x64": "0.1.0"
46
47
  },
47
48
  "devDependencies": {