easy-dep-graph 1.1.3 → 1.2.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.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +162 -130
  3. package/bin/index.js +1128 -455
  4. package/package.json +74 -67
package/bin/index.js CHANGED
@@ -1,36 +1,291 @@
1
1
  #! /usr/bin/env node
2
- import shell from "shelljs";
3
- import mustache from "mustache";
4
- import fastify from "fastify";
5
- import open from "open";
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- import semver from "semver";
2
+ import shell from 'shelljs';
3
+ import mustache from 'mustache';
4
+ import fastify from 'fastify';
5
+ import open from 'open';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import semver from 'semver';
9
9
  let flatDeps;
10
+ // ─── Known Malicious / Compromised Packages Database ────────────────
11
+ const knownMaliciousPackages = [
12
+ // === Compromised legitimate packages (specific bad versions) ===
13
+ {
14
+ name: 'axios',
15
+ badVersions: ['1.14.1', '0.30.4'],
16
+ severity: 'critical',
17
+ description: 'Compromised via maintainer account hijack (Mar 2026). Injected RAT malware via plain-crypto-js dependency.',
18
+ },
19
+ {
20
+ name: 'ua-parser-js',
21
+ badVersions: ['0.7.29', '0.8.0', '1.0.0'],
22
+ severity: 'critical',
23
+ description: 'Compromised via account hijack (Oct 2021). Installed crypto miners and Danabot trojan.',
24
+ },
25
+ {
26
+ name: 'coa',
27
+ badVersions: ['2.0.3', '2.0.4', '2.1.1', '2.1.3', '3.1.3'],
28
+ severity: 'critical',
29
+ description: 'Compromised via account hijack (Nov 2021). Malicious preinstall script downloaded Danabot trojan.',
30
+ },
31
+ {
32
+ name: 'rc',
33
+ badVersions: ['1.2.9', '1.3.9', '2.3.9'],
34
+ severity: 'critical',
35
+ description: 'Compromised via account hijack (Nov 2021). Same Danabot trojan payload as coa.',
36
+ },
37
+ {
38
+ name: 'event-stream',
39
+ badVersions: ['3.3.6'],
40
+ severity: 'critical',
41
+ description: 'Compromised via social-engineered maintainer transfer (Nov 2018). Stole Bitcoin from Copay wallets via flatmap-stream.',
42
+ },
43
+ {
44
+ name: 'colors',
45
+ badVersions: ['1.4.1', '1.4.44-liberty-2'],
46
+ severity: 'high',
47
+ description: 'Sabotaged by maintainer (Jan 2022). Infinite loop printing zalgo text causing DoS. Use 1.4.0 instead.',
48
+ },
49
+ {
50
+ name: 'faker',
51
+ badVersions: ['6.6.6'],
52
+ severity: 'high',
53
+ description: 'Sabotaged by maintainer (Jan 2022). Package contents wiped. Use @faker-js/faker instead.',
54
+ },
55
+ {
56
+ name: 'node-ipc',
57
+ badVersions: ['10.1.1', '10.1.2', '9.2.2', '11.0.0', '11.1.0'],
58
+ severity: 'critical',
59
+ description: 'Protestware (Mar 2022). Geo-targeted file destruction for Russian/Belarusian IPs. CVE-2022-23812.',
60
+ },
61
+ {
62
+ name: 'eslint-scope',
63
+ badVersions: ['3.7.2'],
64
+ severity: 'critical',
65
+ description: 'Compromised (Jul 2018). Stole npm tokens from developer machines.',
66
+ },
67
+ // === Always-malicious packages (any version) ===
68
+ {
69
+ name: 'flatmap-stream',
70
+ badVersions: '*',
71
+ severity: 'critical',
72
+ description: 'Malicious package used in event-stream supply chain attack. Stole cryptocurrency credentials.',
73
+ },
74
+ {
75
+ name: 'plain-crypto-js',
76
+ badVersions: '*',
77
+ severity: 'critical',
78
+ description: 'Malicious dependency injected into compromised axios versions. Delivers cross-platform RAT.',
79
+ },
80
+ {
81
+ name: 'peacenotwar',
82
+ badVersions: '*',
83
+ severity: 'high',
84
+ description: 'Protestware dropped by compromised node-ipc. Writes files to user desktop.',
85
+ },
86
+ {
87
+ name: 'getcookies',
88
+ badVersions: '*',
89
+ severity: 'critical',
90
+ description: 'Backdoor hidden in express middleware (May 2018). Remote code execution.',
91
+ },
92
+ {
93
+ name: 'crossenv',
94
+ badVersions: '*',
95
+ severity: 'critical',
96
+ description: 'Typosquat of cross-env. Steals environment variables and npm tokens.',
97
+ },
98
+ {
99
+ name: 'event-strem',
100
+ badVersions: '*',
101
+ severity: 'critical',
102
+ description: 'Typosquat of event-stream. Credential theft.',
103
+ },
104
+ {
105
+ name: 'lodahs',
106
+ badVersions: '*',
107
+ severity: 'critical',
108
+ description: 'Typosquat of lodash. Malware dropper.',
109
+ },
110
+ {
111
+ name: 'babelcli',
112
+ badVersions: '*',
113
+ severity: 'critical',
114
+ description: 'Typosquat of babel-cli. Steals environment variables.',
115
+ },
116
+ {
117
+ name: 'd3.js',
118
+ badVersions: '*',
119
+ severity: 'critical',
120
+ description: 'Typosquat of d3. Credential theft.',
121
+ },
122
+ {
123
+ name: 'ffmpegs',
124
+ badVersions: '*',
125
+ severity: 'critical',
126
+ description: 'Typosquat of ffmpeg. Crypto miner.',
127
+ },
128
+ {
129
+ name: 'gruntcli',
130
+ badVersions: '*',
131
+ severity: 'critical',
132
+ description: 'Typosquat of grunt-cli. Steals environment variables.',
133
+ },
134
+ {
135
+ name: 'http-proxy.js',
136
+ badVersions: '*',
137
+ severity: 'critical',
138
+ description: 'Typosquat of http-proxy. Credential theft.',
139
+ },
140
+ {
141
+ name: 'mongose',
142
+ badVersions: '*',
143
+ severity: 'critical',
144
+ description: 'Typosquat of mongoose. Steals environment variables.',
145
+ },
146
+ {
147
+ name: 'node-fabric',
148
+ badVersions: '*',
149
+ severity: 'critical',
150
+ description: 'Typosquat of fabric. Opens remote shell.',
151
+ },
152
+ {
153
+ name: 'node-opencv',
154
+ badVersions: '*',
155
+ severity: 'critical',
156
+ description: 'Typosquat of opencv. Credential theft.',
157
+ },
158
+ {
159
+ name: 'node-opensl',
160
+ badVersions: '*',
161
+ severity: 'critical',
162
+ description: 'Typosquat of openssl. Credential theft.',
163
+ },
164
+ {
165
+ name: 'node-openssl',
166
+ badVersions: '*',
167
+ severity: 'critical',
168
+ description: 'Typosquat of openssl. Credential theft.',
169
+ },
170
+ {
171
+ name: 'nodecaffe',
172
+ badVersions: '*',
173
+ severity: 'critical',
174
+ description: 'Typosquat of node-caffe. Credential theft.',
175
+ },
176
+ {
177
+ name: 'nodefabric',
178
+ badVersions: '*',
179
+ severity: 'critical',
180
+ description: 'Typosquat of node-fabric. Credential theft.',
181
+ },
182
+ {
183
+ name: 'nodemailer-js',
184
+ badVersions: '*',
185
+ severity: 'critical',
186
+ description: 'Typosquat of nodemailer. Credential theft.',
187
+ },
188
+ {
189
+ name: 'noderequest',
190
+ badVersions: '*',
191
+ severity: 'critical',
192
+ description: 'Typosquat of request. Credential theft.',
193
+ },
194
+ {
195
+ name: 'nodesass',
196
+ badVersions: '*',
197
+ severity: 'critical',
198
+ description: 'Typosquat of node-sass. Credential theft.',
199
+ },
200
+ {
201
+ name: 'nodesqlite',
202
+ badVersions: '*',
203
+ severity: 'critical',
204
+ description: 'Typosquat of sqlite. Credential theft.',
205
+ },
206
+ {
207
+ name: 'shadowsock',
208
+ badVersions: '*',
209
+ severity: 'critical',
210
+ description: 'Typosquat of shadowsocks. Credential theft.',
211
+ },
212
+ {
213
+ name: 'sqlite.js',
214
+ badVersions: '*',
215
+ severity: 'critical',
216
+ description: 'Typosquat of sqlite3. Credential theft.',
217
+ },
218
+ {
219
+ name: 'proxy.js',
220
+ badVersions: '*',
221
+ severity: 'critical',
222
+ description: 'Typosquat of proxy. Credential theft.',
223
+ },
224
+ {
225
+ name: 'discorsd.js',
226
+ badVersions: '*',
227
+ severity: 'critical',
228
+ description: 'Typosquat of discord.js. Token stealer.',
229
+ },
230
+ {
231
+ name: 'colored',
232
+ badVersions: '*',
233
+ severity: 'critical',
234
+ description: 'Typosquat of colors. Credential theft.',
235
+ },
236
+ {
237
+ name: 'mariadb',
238
+ badVersions: '*',
239
+ severity: 'critical',
240
+ description: 'Typosquat of mariasql. Credential theft.',
241
+ },
242
+ {
243
+ name: 'electron-native-notify',
244
+ badVersions: '*',
245
+ severity: 'critical',
246
+ description: 'Malicious package (Jun 2019). Stole cryptocurrency credentials.',
247
+ },
248
+ {
249
+ name: 'rest-client',
250
+ badVersions: '*',
251
+ severity: 'critical',
252
+ description: 'Malicious package (Aug 2019). Injected code to steal credentials.',
253
+ },
254
+ {
255
+ name: 'bootstrap-sass',
256
+ badVersions: '*',
257
+ severity: 'high',
258
+ description: 'Typosquat with crypto miner.',
259
+ },
260
+ ];
10
261
  void (async function main() {
11
262
  // Check if peer dependencies mode
12
- const peerDependenciesMode = process.argv.findIndex((a) => a.toLowerCase() === "--peer-dependencies") !==
13
- -1;
263
+ const peerDependenciesMode = process.argv.findIndex((a) => a.toLowerCase() === '--peer-dependencies') !== -1;
14
264
  // List of packages user wants to see the dependencies
15
- const packagesArgIndex = process.argv.findIndex((a) => a.toLowerCase() === "--packages");
265
+ const packagesArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--packages');
16
266
  const packagesFilter = packagesArgIndex !== -1 ? process.argv[packagesArgIndex + 1] : undefined;
17
267
  // Only get the dependents of a specific package
18
- const packageArgIndex = process.argv.findIndex((a) => a.toLowerCase() === "--package-dependents");
268
+ const packageArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--package-dependents');
19
269
  const packageName = packageArgIndex !== -1 ? process.argv[packageArgIndex + 1] : undefined;
20
270
  // Get port number
21
- const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() === "--port");
271
+ const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--port');
22
272
  const port = portArgIndex !== -1 ? +process.argv[portArgIndex + 1] : 8080;
23
273
  // Get if should not open browser
24
- const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() === "--no-open") === -1;
274
+ const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() === '--no-open') === -1;
25
275
  // Get if should not apply force layout
26
- const shouldApplyForceLayout = process.argv.findIndex((a) => a.toLowerCase() === "--no-force-layout") ===
27
- -1;
276
+ const shouldApplyForceLayout = process.argv.findIndex((a) => a.toLowerCase() === '--no-force-layout') === -1;
277
+ // Check if security scan mode
278
+ const securityScanMode = process.argv.findIndex((a) => a.toLowerCase() === '--security-scan') !== -1;
279
+ if (securityScanMode) {
280
+ await runSecurityScanMode(port, shouldOpenBrowser);
281
+ return;
282
+ }
28
283
  if (peerDependenciesMode) {
29
284
  await runPeerDependenciesMode(port, shouldOpenBrowser);
30
285
  return;
31
286
  }
32
287
  // Run npm list command
33
- const result = shell.exec(`npm list --json ${packageName ?? "--all"}`, {
288
+ const result = shell.exec(`npm list --json ${packageName ?? '--all'}`, {
34
289
  windowsHide: true,
35
290
  silent: true,
36
291
  });
@@ -39,7 +294,7 @@ void (async function main() {
39
294
  console.log(`Generating dependency graph for: "${packageInfo.name}"...`);
40
295
  // Filter dependencies if needed
41
296
  if (packagesFilter != null) {
42
- const packagesFilterList = packagesFilter.split(",").map((d) => d.trim());
297
+ const packagesFilterList = packagesFilter.split(',').map((d) => d.trim());
43
298
  for (const key of Object.keys(packageInfo.dependencies)) {
44
299
  if (!packagesFilterList.includes(key)) {
45
300
  delete packageInfo.dependencies[key];
@@ -75,7 +330,7 @@ void (async function main() {
75
330
  x,
76
331
  y,
77
332
  size: 10,
78
- color: dep[1].isRoot ? "#22c55e" : "#3b82f6",
333
+ color: dep[1].isRoot ? '#22c55e' : '#3b82f6',
79
334
  };
80
335
  })),
81
336
  edges: JSON.stringify(Object.entries(flatDeps)
@@ -91,7 +346,7 @@ void (async function main() {
91
346
  const app = fastify({
92
347
  logger: false,
93
348
  });
94
- app.get("/", (_req, resp) => resp.type("text/html").send(html));
349
+ app.get('/', (_req, resp) => resp.type('text/html').send(html));
95
350
  // Run the server
96
351
  app.listen({ port }, (err, address) => {
97
352
  if (err)
@@ -123,10 +378,10 @@ function flatDepsRecursive(deps, parentDepName) {
123
378
  }
124
379
  }
125
380
  async function runPeerDependenciesMode(port, shouldOpenBrowser) {
126
- console.log("Analyzing peer dependencies...");
381
+ console.log('Analyzing peer dependencies...');
127
382
  // Get the current project's package.json
128
- const packageJsonPath = path.join(process.cwd(), "package.json");
129
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
383
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
384
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
130
385
  const installedPackages = {
131
386
  ...packageJson.dependencies,
132
387
  ...packageJson.devDependencies,
@@ -138,8 +393,8 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
138
393
  const resolvedVersion = resolveVersion(info.versions);
139
394
  const isInPackageJson = installedPackages[name] !== undefined;
140
395
  // Check if installed in node_modules (even if not in package.json)
141
- const nodeModulesPath = path.join(process.cwd(), "node_modules");
142
- const pkgPath = path.join(nodeModulesPath, name, "package.json");
396
+ const nodeModulesPath = path.join(process.cwd(), 'node_modules');
397
+ const pkgPath = path.join(nodeModulesPath, name, 'package.json');
143
398
  const existsInNodeModules = fs.existsSync(pkgPath);
144
399
  const isInstalledByDependency = existsInNodeModules && !isInPackageJson;
145
400
  return {
@@ -163,8 +418,8 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
163
418
  const app = fastify({
164
419
  logger: false,
165
420
  });
166
- app.get("/", (_req, resp) => resp.type("text/html").send(html));
167
- app.post("/install", async (req, resp) => {
421
+ app.get('/', (_req, resp) => resp.type('text/html').send(html));
422
+ app.post('/install', async (req, resp) => {
168
423
  const { package: pkg, version } = req.body;
169
424
  console.log(`Installing ${pkg}@${version}...`);
170
425
  const installResult = shell.exec(`npm install ${pkg}@${version}`, {
@@ -182,7 +437,7 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
182
437
  console.error(`Failed to install ${pkg}@${version}`);
183
438
  return {
184
439
  success: false,
185
- message: installResult.stderr || "Installation failed",
440
+ message: installResult.stderr || 'Installation failed',
186
441
  };
187
442
  }
188
443
  });
@@ -197,7 +452,7 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
197
452
  }
198
453
  function collectPeerDependenciesRecursively(initialPackages) {
199
454
  const peerDeps = {};
200
- const nodeModulesPath = path.join(process.cwd(), "node_modules");
455
+ const nodeModulesPath = path.join(process.cwd(), 'node_modules');
201
456
  const visited = new Set();
202
457
  const queue = Object.keys(initialPackages);
203
458
  while (queue.length > 0) {
@@ -208,11 +463,11 @@ function collectPeerDependenciesRecursively(initialPackages) {
208
463
  }
209
464
  visited.add(packageName);
210
465
  try {
211
- const pkgJsonPath = path.join(nodeModulesPath, packageName, "package.json");
466
+ const pkgJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
212
467
  if (!fs.existsSync(pkgJsonPath)) {
213
468
  continue;
214
469
  }
215
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
470
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
216
471
  // Collect peer dependencies
217
472
  if (pkgJson.peerDependencies) {
218
473
  for (const [peerDepName, peerDepVersion] of Object.entries(pkgJson.peerDependencies)) {
@@ -254,7 +509,7 @@ function collectPeerDependenciesRecursively(initialPackages) {
254
509
  }
255
510
  function resolveVersion(versions) {
256
511
  if (versions.length === 0) {
257
- return { version: "*", isConflict: false };
512
+ return { version: '*', isConflict: false };
258
513
  }
259
514
  if (versions.length === 1) {
260
515
  return { version: versions[0], isConflict: false };
@@ -286,445 +541,863 @@ function resolveVersion(versions) {
286
541
  }
287
542
  else {
288
543
  // Ranges don't intersect - conflict detected
289
- return { version: uniqueVersions.join(" | "), isConflict: true };
544
+ return { version: uniqueVersions.join(' | '), isConflict: true };
290
545
  }
291
546
  }
292
547
  return { version: intersection, isConflict: false };
293
548
  }
294
549
  catch (error) {
295
550
  // If semver parsing fails, fall back to showing all versions
296
- return { version: uniqueVersions.join(" | "), isConflict: true };
551
+ return { version: uniqueVersions.join(' | '), isConflict: true };
297
552
  }
298
553
  }
299
- function getPeerDepsTemplate() {
300
- return `
301
- <html>
302
- <head>
303
- <title>{{projectName}} - Peer Dependencies</title>
304
- <style>
305
- * {
306
- margin: 0;
307
- padding: 0;
308
- box-sizing: border-box;
309
- }
310
-
311
- body {
312
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
313
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
314
- min-height: 100vh;
315
- padding: 2rem;
316
- }
317
-
318
- .container {
319
- max-width: 1200px;
320
- margin: 0 auto;
321
- }
322
-
323
- h1 {
324
- color: white;
325
- margin-bottom: 2rem;
326
- font-size: 2.5rem;
327
- text-align: center;
328
- text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
329
- }
330
-
331
- .peer-deps-list {
332
- background: white;
333
- border-radius: 12px;
334
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
335
- overflow: hidden;
336
- }
337
-
338
- .peer-dep-item {
339
- padding: 1.5rem;
340
- border-bottom: 1px solid #e5e7eb;
341
- transition: background-color 0.2s;
342
- }
343
-
344
- .peer-dep-item:last-child {
345
- border-bottom: none;
346
- }
347
-
348
- .peer-dep-item:hover {
349
- background-color: #f9fafb;
350
- }
351
-
352
- .peer-dep-header {
353
- display: flex;
354
- justify-content: space-between;
355
- align-items: center;
356
- margin-bottom: 0.75rem;
357
- }
358
-
359
- .peer-dep-name {
360
- font-size: 1.25rem;
361
- font-weight: 600;
362
- color: #1f2937;
363
- }
364
-
365
- .peer-dep-version {
366
- font-family: 'Courier New', monospace;
367
- padding: 0.25rem 0.75rem;
368
- background: #f3f4f6;
369
- border-radius: 6px;
370
- font-size: 0.875rem;
371
- color: #4b5563;
372
- }
373
-
374
- .peer-dep-version.conflict {
375
- background: #fee2e2;
376
- color: #dc2626;
377
- font-weight: 600;
378
- }
379
-
380
- .peer-dep-info {
381
- display: flex;
382
- justify-content: space-between;
383
- align-items: center;
384
- flex-wrap: wrap;
385
- gap: 1rem;
386
- }
387
-
388
- .required-by {
389
- flex: 1;
390
- min-width: 200px;
391
- }
392
-
393
- .required-by-label {
394
- font-size: 0.75rem;
395
- color: #6b7280;
396
- text-transform: uppercase;
397
- letter-spacing: 0.05em;
398
- margin-bottom: 0.25rem;
399
- }
400
-
401
- .required-by-list {
402
- display: flex;
403
- flex-wrap: wrap;
404
- gap: 0.5rem;
405
- }
406
-
407
- .required-by-tag {
408
- display: inline-block;
409
- padding: 0.25rem 0.5rem;
410
- background: #dbeafe;
411
- color: #1e40af;
412
- border-radius: 4px;
413
- font-size: 0.75rem;
414
- font-weight: 500;
415
- }
416
-
417
- .install-btn {
418
- padding: 0.5rem 1.5rem;
419
- background: #3b82f6;
420
- color: white;
421
- border: none;
422
- border-radius: 6px;
423
- font-size: 0.875rem;
424
- font-weight: 500;
425
- cursor: pointer;
426
- transition: all 0.2s;
427
- box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
428
- }
429
-
430
- .install-btn:hover {
431
- background: #2563eb;
432
- transform: translateY(-1px);
433
- box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4);
434
- }
435
-
436
- .install-btn:active {
437
- transform: translateY(0);
438
- }
439
-
440
- .install-btn:disabled {
441
- background: #9ca3af;
442
- cursor: not-allowed;
443
- transform: none;
444
- box-shadow: none;
445
- }
446
-
447
- .installed-badge {
448
- display: flex;
449
- align-items: center;
450
- gap: 0.5rem;
451
- padding: 0.5rem 1rem;
452
- background: #d1fae5;
453
- color: #065f46;
454
- border-radius: 6px;
455
- font-size: 0.875rem;
456
- font-weight: 600;
457
- }
458
-
459
- .installed-by-dep-badge {
460
- display: flex;
461
- align-items: center;
462
- gap: 0.5rem;
463
- padding: 0.5rem 1rem;
464
- background: #e0e7ff;
465
- color: #3730a3;
466
- border-radius: 6px;
467
- font-size: 0.875rem;
468
- font-weight: 600;
469
- }
470
-
471
- .checkmark {
472
- color: #10b981;
473
- font-size: 1.25rem;
474
- }
475
-
476
- .message {
477
- margin-top: 0.5rem;
478
- padding: 0.5rem;
479
- border-radius: 4px;
480
- font-size: 0.875rem;
481
- display: none;
482
- }
483
-
484
- .message.success {
485
- background: #d1fae5;
486
- color: #065f46;
487
- display: block;
488
- }
489
-
490
- .message.error {
491
- background: #fee2e2;
492
- color: #dc2626;
493
- display: block;
494
- }
495
-
496
- .no-deps {
497
- padding: 3rem;
498
- text-align: center;
499
- color: #6b7280;
500
- font-size: 1.125rem;
501
- }
502
- </style>
503
- </head>
504
- <body>
505
- <div class="container">
506
- <h1>Peer Dependencies for {{projectName}}</h1>
507
- <div class="peer-deps-list">
508
- {{#peerDeps}}
509
- <div class="peer-dep-item">
510
- <div class="peer-dep-header">
511
- <span class="peer-dep-name">{{name}}</span>
512
- <span class="peer-dep-version {{#isConflict}}conflict{{/isConflict}}">
513
- {{#isConflict}}Conflict: {{/isConflict}}{{version}}
514
- </span>
515
- </div>
516
- <div class="peer-dep-info">
517
- <div class="required-by">
518
- <div class="required-by-label">Required by:</div>
519
- <div class="required-by-list">
520
- {{#requiredBy}}
521
- <span class="required-by-tag">{{.}}</span>
522
- {{/requiredBy}}
523
- </div>
524
- </div>
525
- <div class="action-container">
526
- {{#isInstalled}}
527
- <div class="installed-badge">
528
- Installed
529
- </div>
530
- {{/isInstalled}}
531
- {{#isInstalledByDependency}}
532
- <div class="installed-by-dep-badge">
533
- Installed by dependency
534
- </div>
535
- {{/isInstalledByDependency}}
536
- {{^isInstalled}}
537
- {{^isInstalledByDependency}}
538
- <button class="install-btn" onclick="installPackage('{{name}}', '{{version}}', this)">
539
- npm install
540
- </button>
541
- <div class="message" id="msg-{{name}}"></div>
542
- {{/isInstalledByDependency}}
543
- {{/isInstalled}}
544
- </div>
545
- </div>
546
- </div>
547
- {{/peerDeps}}
548
- {{^peerDeps}}
549
- <div class="no-deps">
550
- No peer dependencies found!
551
- </div>
552
- {{/peerDeps}}
553
- </div>
554
- </div>
555
-
556
- <script>
557
- async function installPackage(packageName, version, button) {
558
- const messageEl = document.getElementById('msg-' + packageName);
559
-
560
- button.disabled = true;
561
- button.textContent = 'Installing...';
562
- messageEl.className = 'message';
563
- messageEl.textContent = '';
564
-
565
- try {
566
- const response = await fetch('/install', {
567
- method: 'POST',
568
- headers: {
569
- 'Content-Type': 'application/json',
570
- },
571
- body: JSON.stringify({
572
- package: packageName,
573
- version: version
574
- })
575
- });
576
-
577
- const result = await response.json();
578
-
579
- if (result.success) {
580
- messageEl.className = 'message success';
581
- messageEl.textContent = result.message;
582
- button.style.display = 'none';
583
-
584
- // Optionally reload after a delay
585
- setTimeout(() => {
586
- location.reload();
587
- }, 2000);
588
- } else {
589
- messageEl.className = 'message error';
590
- messageEl.textContent = result.message;
591
- button.disabled = false;
592
- button.textContent = 'npm install';
593
- }
594
- } catch (error) {
595
- messageEl.className = 'message error';
596
- messageEl.textContent = 'Failed to install package: ' + error.message;
597
- button.disabled = false;
598
- button.textContent = 'npm install';
599
- }
600
- }
601
- </script>
602
- </body>
603
- </html>
604
- `;
605
- }
606
- function getTemplate() {
607
- return `
608
- <html>
609
- <head>
610
- <title>{{name}}'s Dependency Graph</title>
611
- <script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
612
- <script src="https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js"></script>
613
- <script type="text/javascript">
614
- function applyForceLayout(graph, iterations) {
615
- var nodes = graph.nodes();
616
- var edges = graph.edges();
617
-
618
- // Physics constants - lower repulsion for better spacing
619
- var repulsionStrength = 50;
620
- var attractionStrength = 0.01;
621
- var damping = 0.5;
622
-
623
- for (var iter = 0; iter < iterations; iter++) {
624
- var forces = {};
625
-
626
- // Initialize forces
627
- nodes.forEach(function(nodeId) {
628
- forces[nodeId] = { x: 0, y: 0 };
629
- });
630
-
631
- // Repulsive forces between all nodes
632
- for (var i = 0; i < nodes.length; i++) {
633
- for (var j = i + 1; j < nodes.length; j++) {
634
- var node1 = nodes[i];
635
- var node2 = nodes[j];
636
- var attrs1 = graph.getNodeAttributes(node1);
637
- var attrs2 = graph.getNodeAttributes(node2);
638
-
639
- var dx = attrs2.x - attrs1.x;
640
- var dy = attrs2.y - attrs1.y;
641
- var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
642
- var force = repulsionStrength / (distance * distance);
643
-
644
- var fx = (dx / distance) * force;
645
- var fy = (dy / distance) * force;
646
-
647
- forces[node1].x -= fx;
648
- forces[node1].y -= fy;
649
- forces[node2].x += fx;
650
- forces[node2].y += fy;
554
+ // ─── Security Scan Logic ────────────────────────────────────────────
555
+ async function runSecurityScanMode(port, shouldOpenBrowser) {
556
+ console.log('Running security scan...\n');
557
+ const findings = [];
558
+ const nodeModulesPath = path.join(process.cwd(), 'node_modules');
559
+ // Phase 1: Scan node_modules against known malicious packages
560
+ if (fs.existsSync(nodeModulesPath)) {
561
+ const entries = fs.readdirSync(nodeModulesPath);
562
+ for (const entry of entries) {
563
+ // Handle scoped packages (@scope/name)
564
+ if (entry.startsWith('@')) {
565
+ const scopePath = path.join(nodeModulesPath, entry);
566
+ try {
567
+ const scopedEntries = fs.readdirSync(scopePath);
568
+ for (const scopedEntry of scopedEntries) {
569
+ const fullName = `${entry}/${scopedEntry}`;
570
+ checkPackageAgainstDatabase(nodeModulesPath, fullName, findings);
651
571
  }
652
572
  }
653
-
654
- // Attractive forces along edges
655
- edges.forEach(function(edgeId) {
656
- var edge = graph.extremities(edgeId);
657
- var source = edge.source || edge[0];
658
- var target = edge.target || edge[1];
659
- var attrs1 = graph.getNodeAttributes(source);
660
- var attrs2 = graph.getNodeAttributes(target);
661
-
662
- var dx = attrs2.x - attrs1.x;
663
- var dy = attrs2.y - attrs1.y;
664
- var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
665
-
666
- var fx = dx * attractionStrength;
667
- var fy = dy * attractionStrength;
668
-
669
- forces[source].x += fx;
670
- forces[source].y += fy;
671
- forces[target].x -= fx;
672
- forces[target].y -= fy;
673
- });
674
-
675
- // Apply forces with damping
676
- nodes.forEach(function(nodeId) {
677
- var attrs = graph.getNodeAttributes(nodeId);
678
- attrs.x += forces[nodeId].x * damping;
679
- attrs.y += forces[nodeId].y * damping;
680
- });
573
+ catch {
574
+ // Skip unreadable scope directories
575
+ }
576
+ continue;
681
577
  }
578
+ checkPackageAgainstDatabase(nodeModulesPath, entry, findings);
682
579
  }
683
-
684
- function renderGraph() {
685
- var nodes = {{{nodes}}};
686
- var edges = {{{edges}}};
687
-
688
- var graph = new graphology.Graph();
689
-
690
- nodes.forEach(function(node) {
691
- graph.addNode(node.key, {
692
- label: node.label,
693
- x: node.x,
694
- y: node.y,
695
- size: node.size,
696
- color: node.color
697
- });
698
- });
699
-
700
- edges.forEach(function(edge, index) {
701
- graph.mergeEdge(edge.source, edge.target, {
702
- size: 2,
703
- color: '#94a3b8'
580
+ }
581
+ else {
582
+ console.warn('Warning: node_modules directory not found. Skipping local package scan.\n');
583
+ }
584
+ // Phase 2: Run npm audit
585
+ console.log('Running npm audit...');
586
+ const auditResult = shell.exec('npm audit --json', {
587
+ windowsHide: true,
588
+ silent: true,
589
+ });
590
+ if (auditResult.code !== 0 && auditResult.stdout.trim() === '') {
591
+ console.warn('Warning: npm audit failed (missing package-lock.json or no network). Showing known-malicious results only.\n');
592
+ }
593
+ else {
594
+ try {
595
+ const auditData = JSON.parse(auditResult.stdout);
596
+ const vulnerabilities = auditData.vulnerabilities ?? {};
597
+ for (const [pkgName, vulnInfo] of Object.entries(vulnerabilities)) {
598
+ // Skip if already flagged by our known-malicious list
599
+ if (findings.some((f) => f.name === pkgName))
600
+ continue;
601
+ const severity = vulnInfo.severity ?? 'moderate';
602
+ const via = Array.isArray(vulnInfo.via)
603
+ ? vulnInfo.via
604
+ .map((v) => (typeof v === 'string' ? v : (v.title ?? v.name ?? '')))
605
+ .filter(Boolean)
606
+ .join('; ')
607
+ : String(vulnInfo.via ?? '');
608
+ const installedVer = vulnInfo.range ?? vulnInfo.version ?? 'unknown';
609
+ findings.push({
610
+ name: pkgName,
611
+ installedVersion: installedVer,
612
+ severity: severity,
613
+ source: 'npm-audit',
614
+ description: via || `Vulnerability reported by npm audit (${severity})`,
704
615
  });
616
+ }
617
+ }
618
+ catch {
619
+ console.warn('Warning: Could not parse npm audit output.\n');
620
+ }
621
+ }
622
+ // Sort findings: critical first, then high, moderate, low
623
+ const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 };
624
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
625
+ // Phase 3: Terminal output
626
+ printSecurityScanResults(findings);
627
+ // Phase 4: Serve HTML report
628
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
629
+ let projectName = 'Unknown Project';
630
+ try {
631
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
632
+ projectName = packageJson.name ?? projectName;
633
+ }
634
+ catch {
635
+ // Ignore
636
+ }
637
+ const criticalCount = findings.filter((f) => f.severity === 'critical').length;
638
+ const highCount = findings.filter((f) => f.severity === 'high').length;
639
+ const moderateCount = findings.filter((f) => f.severity === 'moderate').length;
640
+ const lowCount = findings.filter((f) => f.severity === 'low').length;
641
+ const data = {
642
+ projectName,
643
+ findings,
644
+ totalCount: findings.length,
645
+ criticalCount,
646
+ highCount,
647
+ moderateCount,
648
+ lowCount,
649
+ hasCritical: criticalCount > 0,
650
+ hasHigh: highCount > 0,
651
+ hasModerate: moderateCount > 0,
652
+ hasLow: lowCount > 0,
653
+ hasFindings: findings.length > 0,
654
+ };
655
+ const html = mustache.render(getSecurityScanTemplate(), data);
656
+ const app = fastify({ logger: false });
657
+ app.get('/', (_req, resp) => resp.type('text/html').send(html));
658
+ app.listen({ port }, (err, address) => {
659
+ if (err)
660
+ throw err;
661
+ console.log(`\nSecurity report available at: ${address}`);
662
+ if (shouldOpenBrowser)
663
+ open(address);
664
+ });
665
+ }
666
+ function checkPackageAgainstDatabase(nodeModulesPath, packageName, findings) {
667
+ const pkgJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
668
+ if (!fs.existsSync(pkgJsonPath))
669
+ return;
670
+ let installedVersion;
671
+ try {
672
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
673
+ installedVersion = pkgJson.version ?? '0.0.0';
674
+ }
675
+ catch {
676
+ return;
677
+ }
678
+ for (const entry of knownMaliciousPackages) {
679
+ if (entry.name !== packageName)
680
+ continue;
681
+ if (entry.badVersions === '*') {
682
+ findings.push({
683
+ name: packageName,
684
+ installedVersion,
685
+ severity: entry.severity,
686
+ source: 'known-malicious',
687
+ description: entry.description,
705
688
  });
706
-
707
- // Apply custom force-directed layout
708
- {{#applyForceLayout}}
709
- applyForceLayout(graph, 100);
710
- {{/applyForceLayout}}
711
-
712
- var container = document.getElementById("dep-graph");
713
- var renderer = new Sigma(graph, container, {
714
- renderEdgeLabels: false,
715
- defaultNodeColor: '#3b82f6',
716
- defaultEdgeColor: '#94a3b8'
689
+ }
690
+ else if (entry.badVersions.includes(installedVersion)) {
691
+ findings.push({
692
+ name: packageName,
693
+ installedVersion,
694
+ severity: entry.severity,
695
+ source: 'known-malicious',
696
+ description: entry.description,
717
697
  });
718
698
  }
719
- </script>
720
- <style>
721
- body { margin: 0; padding: 0; }
722
- #dep-graph { width: 100vw; height: 100vh; background: #f8fafc; }
723
- </style>
724
- </head>
725
- <body onload="renderGraph()">
726
- <div id="dep-graph"></div>
727
- </body>
728
- </html>
699
+ }
700
+ }
701
+ function printSecurityScanResults(findings) {
702
+ if (findings.length === 0) {
703
+ console.log('\x1b[32m✔ No known malicious packages or vulnerabilities found.\x1b[0m\n');
704
+ return;
705
+ }
706
+ const critical = findings.filter((f) => f.severity === 'critical');
707
+ const high = findings.filter((f) => f.severity === 'high');
708
+ const moderate = findings.filter((f) => f.severity === 'moderate');
709
+ const low = findings.filter((f) => f.severity === 'low');
710
+ console.log(`\x1b[1mSecurity Scan Results:\x1b[0m`);
711
+ console.log(`─────────────────────────────────────────────`);
712
+ if (critical.length > 0)
713
+ console.log(` \x1b[31m● ${critical.length} critical\x1b[0m`);
714
+ if (high.length > 0)
715
+ console.log(` \x1b[33m● ${high.length} high\x1b[0m`);
716
+ if (moderate.length > 0)
717
+ console.log(` \x1b[36m● ${moderate.length} moderate\x1b[0m`);
718
+ if (low.length > 0)
719
+ console.log(` \x1b[37m● ${low.length} low\x1b[0m`);
720
+ console.log(`─────────────────────────────────────────────\n`);
721
+ for (const finding of findings) {
722
+ const color = finding.severity === 'critical'
723
+ ? '\x1b[31m'
724
+ : finding.severity === 'high'
725
+ ? '\x1b[33m'
726
+ : finding.severity === 'moderate'
727
+ ? '\x1b[36m'
728
+ : '\x1b[37m';
729
+ const sourceTag = finding.source === 'known-malicious' ? '[KNOWN MALICIOUS]' : '[npm audit]';
730
+ console.log(` ${color}${finding.severity.toUpperCase()}\x1b[0m ${finding.name}@${finding.installedVersion} ${sourceTag}`);
731
+ console.log(` ${finding.description}`);
732
+ console.log();
733
+ }
734
+ }
735
+ // ─── Security Scan HTML Template ────────────────────────────────────
736
+ function getSecurityScanTemplate() {
737
+ return `
738
+ <html>
739
+ <head>
740
+ <title>{{projectName}} - Security Scan</title>
741
+ <style>
742
+ * { margin: 0; padding: 0; box-sizing: border-box; }
743
+
744
+ body {
745
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
746
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
747
+ min-height: 100vh;
748
+ padding: 2rem;
749
+ color: #e2e8f0;
750
+ }
751
+
752
+ .container { max-width: 1200px; margin: 0 auto; }
753
+
754
+ h1 {
755
+ color: white;
756
+ margin-bottom: 0.5rem;
757
+ font-size: 2.5rem;
758
+ text-align: center;
759
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
760
+ }
761
+
762
+ .subtitle {
763
+ text-align: center;
764
+ color: #94a3b8;
765
+ margin-bottom: 2rem;
766
+ font-size: 1.1rem;
767
+ }
768
+
769
+ .summary-cards {
770
+ display: flex;
771
+ gap: 1rem;
772
+ margin-bottom: 2rem;
773
+ flex-wrap: wrap;
774
+ justify-content: center;
775
+ }
776
+
777
+ .summary-card {
778
+ padding: 1.25rem 2rem;
779
+ border-radius: 12px;
780
+ text-align: center;
781
+ min-width: 140px;
782
+ backdrop-filter: blur(10px);
783
+ border: 1px solid rgba(255,255,255,0.1);
784
+ }
785
+
786
+ .summary-card .count {
787
+ font-size: 2.5rem;
788
+ font-weight: 700;
789
+ line-height: 1;
790
+ }
791
+
792
+ .summary-card .label {
793
+ font-size: 0.8rem;
794
+ text-transform: uppercase;
795
+ letter-spacing: 0.1em;
796
+ margin-top: 0.5rem;
797
+ opacity: 0.9;
798
+ }
799
+
800
+ .card-critical { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); }
801
+ .card-critical .count { color: #ef4444; }
802
+ .card-critical .label { color: #fca5a5; }
803
+
804
+ .card-high { background: rgba(245, 158, 11, 0.2); border-color: rgba(245, 158, 11, 0.4); }
805
+ .card-high .count { color: #f59e0b; }
806
+ .card-high .label { color: #fcd34d; }
807
+
808
+ .card-moderate { background: rgba(59, 130, 246, 0.2); border-color: rgba(59, 130, 246, 0.4); }
809
+ .card-moderate .count { color: #3b82f6; }
810
+ .card-moderate .label { color: #93c5fd; }
811
+
812
+ .card-low { background: rgba(107, 114, 128, 0.2); border-color: rgba(107, 114, 128, 0.4); }
813
+ .card-low .count { color: #9ca3af; }
814
+ .card-low .label { color: #d1d5db; }
815
+
816
+ .findings-list {
817
+ background: rgba(255, 255, 255, 0.05);
818
+ border-radius: 12px;
819
+ border: 1px solid rgba(255,255,255,0.1);
820
+ overflow: hidden;
821
+ backdrop-filter: blur(10px);
822
+ }
823
+
824
+ .finding-item {
825
+ padding: 1.25rem 1.5rem;
826
+ border-bottom: 1px solid rgba(255,255,255,0.06);
827
+ transition: background-color 0.2s;
828
+ }
829
+
830
+ .finding-item:last-child { border-bottom: none; }
831
+ .finding-item:hover { background: rgba(255,255,255,0.04); }
832
+
833
+ .finding-header {
834
+ display: flex;
835
+ align-items: center;
836
+ gap: 1rem;
837
+ margin-bottom: 0.5rem;
838
+ flex-wrap: wrap;
839
+ }
840
+
841
+ .severity-badge {
842
+ padding: 0.2rem 0.75rem;
843
+ border-radius: 9999px;
844
+ font-size: 0.7rem;
845
+ font-weight: 700;
846
+ text-transform: uppercase;
847
+ letter-spacing: 0.05em;
848
+ }
849
+
850
+ .severity-critical { background: #ef4444; color: white; }
851
+ .severity-high { background: #f59e0b; color: #1a1a2e; }
852
+ .severity-moderate { background: #3b82f6; color: white; }
853
+ .severity-low { background: #6b7280; color: white; }
854
+
855
+ .finding-name {
856
+ font-size: 1.2rem;
857
+ font-weight: 600;
858
+ color: #f1f5f9;
859
+ }
860
+
861
+ .finding-version {
862
+ font-family: 'Courier New', monospace;
863
+ font-size: 0.85rem;
864
+ padding: 0.15rem 0.6rem;
865
+ background: rgba(255,255,255,0.1);
866
+ border-radius: 4px;
867
+ color: #cbd5e1;
868
+ }
869
+
870
+ .source-tag {
871
+ font-size: 0.7rem;
872
+ padding: 0.15rem 0.6rem;
873
+ border-radius: 4px;
874
+ font-weight: 600;
875
+ text-transform: uppercase;
876
+ letter-spacing: 0.05em;
877
+ }
878
+
879
+ .source-known-malicious { background: rgba(239, 68, 68, 0.2); color: #fca5a5; border: 1px solid rgba(239, 68, 68, 0.3); }
880
+ .source-npm-audit { background: rgba(139, 92, 246, 0.2); color: #c4b5fd; border: 1px solid rgba(139, 92, 246, 0.3); }
881
+
882
+ .finding-description {
883
+ color: #94a3b8;
884
+ font-size: 0.9rem;
885
+ line-height: 1.5;
886
+ padding-left: 0.25rem;
887
+ }
888
+
889
+ .no-findings {
890
+ padding: 4rem 2rem;
891
+ text-align: center;
892
+ }
893
+
894
+ .no-findings .checkmark {
895
+ font-size: 4rem;
896
+ margin-bottom: 1rem;
897
+ }
898
+
899
+ .no-findings h2 {
900
+ color: #22c55e;
901
+ font-size: 1.5rem;
902
+ margin-bottom: 0.5rem;
903
+ }
904
+
905
+ .no-findings p {
906
+ color: #94a3b8;
907
+ }
908
+ </style>
909
+ </head>
910
+ <body>
911
+ <div class="container">
912
+ <h1>Security Scan</h1>
913
+ <p class="subtitle">{{projectName}}</p>
914
+
915
+ {{#hasFindings}}
916
+ <div class="summary-cards">
917
+ {{#hasCritical}}
918
+ <div class="summary-card card-critical">
919
+ <div class="count">{{criticalCount}}</div>
920
+ <div class="label">Critical</div>
921
+ </div>
922
+ {{/hasCritical}}
923
+ {{#hasHigh}}
924
+ <div class="summary-card card-high">
925
+ <div class="count">{{highCount}}</div>
926
+ <div class="label">High</div>
927
+ </div>
928
+ {{/hasHigh}}
929
+ {{#hasModerate}}
930
+ <div class="summary-card card-moderate">
931
+ <div class="count">{{moderateCount}}</div>
932
+ <div class="label">Moderate</div>
933
+ </div>
934
+ {{/hasModerate}}
935
+ {{#hasLow}}
936
+ <div class="summary-card card-low">
937
+ <div class="count">{{lowCount}}</div>
938
+ <div class="label">Low</div>
939
+ </div>
940
+ {{/hasLow}}
941
+ </div>
942
+
943
+ <div class="findings-list">
944
+ {{#findings}}
945
+ <div class="finding-item">
946
+ <div class="finding-header">
947
+ <span class="severity-badge severity-{{severity}}">{{severity}}</span>
948
+ <span class="finding-name">{{name}}</span>
949
+ <span class="finding-version">{{installedVersion}}</span>
950
+ <span class="source-tag source-{{source}}">{{source}}</span>
951
+ </div>
952
+ <div class="finding-description">{{description}}</div>
953
+ </div>
954
+ {{/findings}}
955
+ </div>
956
+ {{/hasFindings}}
957
+
958
+ {{^hasFindings}}
959
+ <div class="findings-list">
960
+ <div class="no-findings">
961
+ <div class="checkmark">&#10003;</div>
962
+ <h2>All Clear</h2>
963
+ <p>No known malicious packages or vulnerabilities were found.</p>
964
+ </div>
965
+ </div>
966
+ {{/hasFindings}}
967
+ </div>
968
+ </body>
969
+ </html>
970
+ `;
971
+ }
972
+ function getPeerDepsTemplate() {
973
+ return `
974
+ <html>
975
+ <head>
976
+ <title>{{projectName}} - Peer Dependencies</title>
977
+ <style>
978
+ * {
979
+ margin: 0;
980
+ padding: 0;
981
+ box-sizing: border-box;
982
+ }
983
+
984
+ body {
985
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
986
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
987
+ min-height: 100vh;
988
+ padding: 2rem;
989
+ }
990
+
991
+ .container {
992
+ max-width: 1200px;
993
+ margin: 0 auto;
994
+ }
995
+
996
+ h1 {
997
+ color: white;
998
+ margin-bottom: 2rem;
999
+ font-size: 2.5rem;
1000
+ text-align: center;
1001
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
1002
+ }
1003
+
1004
+ .peer-deps-list {
1005
+ background: white;
1006
+ border-radius: 12px;
1007
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
1008
+ overflow: hidden;
1009
+ }
1010
+
1011
+ .peer-dep-item {
1012
+ padding: 1.5rem;
1013
+ border-bottom: 1px solid #e5e7eb;
1014
+ transition: background-color 0.2s;
1015
+ }
1016
+
1017
+ .peer-dep-item:last-child {
1018
+ border-bottom: none;
1019
+ }
1020
+
1021
+ .peer-dep-item:hover {
1022
+ background-color: #f9fafb;
1023
+ }
1024
+
1025
+ .peer-dep-header {
1026
+ display: flex;
1027
+ justify-content: space-between;
1028
+ align-items: center;
1029
+ margin-bottom: 0.75rem;
1030
+ }
1031
+
1032
+ .peer-dep-name {
1033
+ font-size: 1.25rem;
1034
+ font-weight: 600;
1035
+ color: #1f2937;
1036
+ }
1037
+
1038
+ .peer-dep-version {
1039
+ font-family: 'Courier New', monospace;
1040
+ padding: 0.25rem 0.75rem;
1041
+ background: #f3f4f6;
1042
+ border-radius: 6px;
1043
+ font-size: 0.875rem;
1044
+ color: #4b5563;
1045
+ }
1046
+
1047
+ .peer-dep-version.conflict {
1048
+ background: #fee2e2;
1049
+ color: #dc2626;
1050
+ font-weight: 600;
1051
+ }
1052
+
1053
+ .peer-dep-info {
1054
+ display: flex;
1055
+ justify-content: space-between;
1056
+ align-items: center;
1057
+ flex-wrap: wrap;
1058
+ gap: 1rem;
1059
+ }
1060
+
1061
+ .required-by {
1062
+ flex: 1;
1063
+ min-width: 200px;
1064
+ }
1065
+
1066
+ .required-by-label {
1067
+ font-size: 0.75rem;
1068
+ color: #6b7280;
1069
+ text-transform: uppercase;
1070
+ letter-spacing: 0.05em;
1071
+ margin-bottom: 0.25rem;
1072
+ }
1073
+
1074
+ .required-by-list {
1075
+ display: flex;
1076
+ flex-wrap: wrap;
1077
+ gap: 0.5rem;
1078
+ }
1079
+
1080
+ .required-by-tag {
1081
+ display: inline-block;
1082
+ padding: 0.25rem 0.5rem;
1083
+ background: #dbeafe;
1084
+ color: #1e40af;
1085
+ border-radius: 4px;
1086
+ font-size: 0.75rem;
1087
+ font-weight: 500;
1088
+ }
1089
+
1090
+ .install-btn {
1091
+ padding: 0.5rem 1.5rem;
1092
+ background: #3b82f6;
1093
+ color: white;
1094
+ border: none;
1095
+ border-radius: 6px;
1096
+ font-size: 0.875rem;
1097
+ font-weight: 500;
1098
+ cursor: pointer;
1099
+ transition: all 0.2s;
1100
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
1101
+ }
1102
+
1103
+ .install-btn:hover {
1104
+ background: #2563eb;
1105
+ transform: translateY(-1px);
1106
+ box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4);
1107
+ }
1108
+
1109
+ .install-btn:active {
1110
+ transform: translateY(0);
1111
+ }
1112
+
1113
+ .install-btn:disabled {
1114
+ background: #9ca3af;
1115
+ cursor: not-allowed;
1116
+ transform: none;
1117
+ box-shadow: none;
1118
+ }
1119
+
1120
+ .installed-badge {
1121
+ display: flex;
1122
+ align-items: center;
1123
+ gap: 0.5rem;
1124
+ padding: 0.5rem 1rem;
1125
+ background: #d1fae5;
1126
+ color: #065f46;
1127
+ border-radius: 6px;
1128
+ font-size: 0.875rem;
1129
+ font-weight: 600;
1130
+ }
1131
+
1132
+ .installed-by-dep-badge {
1133
+ display: flex;
1134
+ align-items: center;
1135
+ gap: 0.5rem;
1136
+ padding: 0.5rem 1rem;
1137
+ background: #e0e7ff;
1138
+ color: #3730a3;
1139
+ border-radius: 6px;
1140
+ font-size: 0.875rem;
1141
+ font-weight: 600;
1142
+ }
1143
+
1144
+ .checkmark {
1145
+ color: #10b981;
1146
+ font-size: 1.25rem;
1147
+ }
1148
+
1149
+ .message {
1150
+ margin-top: 0.5rem;
1151
+ padding: 0.5rem;
1152
+ border-radius: 4px;
1153
+ font-size: 0.875rem;
1154
+ display: none;
1155
+ }
1156
+
1157
+ .message.success {
1158
+ background: #d1fae5;
1159
+ color: #065f46;
1160
+ display: block;
1161
+ }
1162
+
1163
+ .message.error {
1164
+ background: #fee2e2;
1165
+ color: #dc2626;
1166
+ display: block;
1167
+ }
1168
+
1169
+ .no-deps {
1170
+ padding: 3rem;
1171
+ text-align: center;
1172
+ color: #6b7280;
1173
+ font-size: 1.125rem;
1174
+ }
1175
+ </style>
1176
+ </head>
1177
+ <body>
1178
+ <div class="container">
1179
+ <h1>Peer Dependencies for {{projectName}}</h1>
1180
+ <div class="peer-deps-list">
1181
+ {{#peerDeps}}
1182
+ <div class="peer-dep-item">
1183
+ <div class="peer-dep-header">
1184
+ <span class="peer-dep-name">{{name}}</span>
1185
+ <span class="peer-dep-version {{#isConflict}}conflict{{/isConflict}}">
1186
+ {{#isConflict}}Conflict: {{/isConflict}}{{version}}
1187
+ </span>
1188
+ </div>
1189
+ <div class="peer-dep-info">
1190
+ <div class="required-by">
1191
+ <div class="required-by-label">Required by:</div>
1192
+ <div class="required-by-list">
1193
+ {{#requiredBy}}
1194
+ <span class="required-by-tag">{{.}}</span>
1195
+ {{/requiredBy}}
1196
+ </div>
1197
+ </div>
1198
+ <div class="action-container">
1199
+ {{#isInstalled}}
1200
+ <div class="installed-badge">
1201
+ Installed
1202
+ </div>
1203
+ {{/isInstalled}}
1204
+ {{#isInstalledByDependency}}
1205
+ <div class="installed-by-dep-badge">
1206
+ Installed by dependency
1207
+ </div>
1208
+ {{/isInstalledByDependency}}
1209
+ {{^isInstalled}}
1210
+ {{^isInstalledByDependency}}
1211
+ <button class="install-btn" onclick="installPackage('{{name}}', '{{version}}', this)">
1212
+ npm install
1213
+ </button>
1214
+ <div class="message" id="msg-{{name}}"></div>
1215
+ {{/isInstalledByDependency}}
1216
+ {{/isInstalled}}
1217
+ </div>
1218
+ </div>
1219
+ </div>
1220
+ {{/peerDeps}}
1221
+ {{^peerDeps}}
1222
+ <div class="no-deps">
1223
+ No peer dependencies found!
1224
+ </div>
1225
+ {{/peerDeps}}
1226
+ </div>
1227
+ </div>
1228
+
1229
+ <script>
1230
+ async function installPackage(packageName, version, button) {
1231
+ const messageEl = document.getElementById('msg-' + packageName);
1232
+
1233
+ button.disabled = true;
1234
+ button.textContent = 'Installing...';
1235
+ messageEl.className = 'message';
1236
+ messageEl.textContent = '';
1237
+
1238
+ try {
1239
+ const response = await fetch('/install', {
1240
+ method: 'POST',
1241
+ headers: {
1242
+ 'Content-Type': 'application/json',
1243
+ },
1244
+ body: JSON.stringify({
1245
+ package: packageName,
1246
+ version: version
1247
+ })
1248
+ });
1249
+
1250
+ const result = await response.json();
1251
+
1252
+ if (result.success) {
1253
+ messageEl.className = 'message success';
1254
+ messageEl.textContent = result.message;
1255
+ button.style.display = 'none';
1256
+
1257
+ // Optionally reload after a delay
1258
+ setTimeout(() => {
1259
+ location.reload();
1260
+ }, 2000);
1261
+ } else {
1262
+ messageEl.className = 'message error';
1263
+ messageEl.textContent = result.message;
1264
+ button.disabled = false;
1265
+ button.textContent = 'npm install';
1266
+ }
1267
+ } catch (error) {
1268
+ messageEl.className = 'message error';
1269
+ messageEl.textContent = 'Failed to install package: ' + error.message;
1270
+ button.disabled = false;
1271
+ button.textContent = 'npm install';
1272
+ }
1273
+ }
1274
+ </script>
1275
+ </body>
1276
+ </html>
1277
+ `;
1278
+ }
1279
+ function getTemplate() {
1280
+ return `
1281
+ <html>
1282
+ <head>
1283
+ <title>{{name}}'s Dependency Graph</title>
1284
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
1285
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js"></script>
1286
+ <script type="text/javascript">
1287
+ function applyForceLayout(graph, iterations) {
1288
+ var nodes = graph.nodes();
1289
+ var edges = graph.edges();
1290
+
1291
+ // Physics constants - lower repulsion for better spacing
1292
+ var repulsionStrength = 50;
1293
+ var attractionStrength = 0.01;
1294
+ var damping = 0.5;
1295
+
1296
+ for (var iter = 0; iter < iterations; iter++) {
1297
+ var forces = {};
1298
+
1299
+ // Initialize forces
1300
+ nodes.forEach(function(nodeId) {
1301
+ forces[nodeId] = { x: 0, y: 0 };
1302
+ });
1303
+
1304
+ // Repulsive forces between all nodes
1305
+ for (var i = 0; i < nodes.length; i++) {
1306
+ for (var j = i + 1; j < nodes.length; j++) {
1307
+ var node1 = nodes[i];
1308
+ var node2 = nodes[j];
1309
+ var attrs1 = graph.getNodeAttributes(node1);
1310
+ var attrs2 = graph.getNodeAttributes(node2);
1311
+
1312
+ var dx = attrs2.x - attrs1.x;
1313
+ var dy = attrs2.y - attrs1.y;
1314
+ var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
1315
+ var force = repulsionStrength / (distance * distance);
1316
+
1317
+ var fx = (dx / distance) * force;
1318
+ var fy = (dy / distance) * force;
1319
+
1320
+ forces[node1].x -= fx;
1321
+ forces[node1].y -= fy;
1322
+ forces[node2].x += fx;
1323
+ forces[node2].y += fy;
1324
+ }
1325
+ }
1326
+
1327
+ // Attractive forces along edges
1328
+ edges.forEach(function(edgeId) {
1329
+ var edge = graph.extremities(edgeId);
1330
+ var source = edge.source || edge[0];
1331
+ var target = edge.target || edge[1];
1332
+ var attrs1 = graph.getNodeAttributes(source);
1333
+ var attrs2 = graph.getNodeAttributes(target);
1334
+
1335
+ var dx = attrs2.x - attrs1.x;
1336
+ var dy = attrs2.y - attrs1.y;
1337
+ var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
1338
+
1339
+ var fx = dx * attractionStrength;
1340
+ var fy = dy * attractionStrength;
1341
+
1342
+ forces[source].x += fx;
1343
+ forces[source].y += fy;
1344
+ forces[target].x -= fx;
1345
+ forces[target].y -= fy;
1346
+ });
1347
+
1348
+ // Apply forces with damping
1349
+ nodes.forEach(function(nodeId) {
1350
+ var attrs = graph.getNodeAttributes(nodeId);
1351
+ attrs.x += forces[nodeId].x * damping;
1352
+ attrs.y += forces[nodeId].y * damping;
1353
+ });
1354
+ }
1355
+ }
1356
+
1357
+ function renderGraph() {
1358
+ var nodes = {{{nodes}}};
1359
+ var edges = {{{edges}}};
1360
+
1361
+ var graph = new graphology.Graph();
1362
+
1363
+ nodes.forEach(function(node) {
1364
+ graph.addNode(node.key, {
1365
+ label: node.label,
1366
+ x: node.x,
1367
+ y: node.y,
1368
+ size: node.size,
1369
+ color: node.color
1370
+ });
1371
+ });
1372
+
1373
+ edges.forEach(function(edge, index) {
1374
+ graph.mergeEdge(edge.source, edge.target, {
1375
+ size: 2,
1376
+ color: '#94a3b8'
1377
+ });
1378
+ });
1379
+
1380
+ // Apply custom force-directed layout
1381
+ {{#applyForceLayout}}
1382
+ applyForceLayout(graph, 100);
1383
+ {{/applyForceLayout}}
1384
+
1385
+ var container = document.getElementById("dep-graph");
1386
+ var renderer = new Sigma(graph, container, {
1387
+ renderEdgeLabels: false,
1388
+ defaultNodeColor: '#3b82f6',
1389
+ defaultEdgeColor: '#94a3b8'
1390
+ });
1391
+ }
1392
+ </script>
1393
+ <style>
1394
+ body { margin: 0; padding: 0; }
1395
+ #dep-graph { width: 100vw; height: 100vh; background: #f8fafc; }
1396
+ </style>
1397
+ </head>
1398
+ <body onload="renderGraph()">
1399
+ <div id="dep-graph"></div>
1400
+ </body>
1401
+ </html>
729
1402
  `;
730
1403
  }