ghostimport 0.1.0 → 0.1.3

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
@@ -40,6 +40,8 @@ The result: your build fails, or your CI breaks at 2am, or — in the worst case
40
40
 
41
41
  ## Install
42
42
 
43
+ Requires Node.js 22 or later.
44
+
43
45
  ```bash
44
46
  # Run once (no install needed)
45
47
  npx ghostimport
@@ -71,12 +73,27 @@ ghostimport --json
71
73
  # Skip "missing from package.json" warnings
72
74
  ghostimport --no-undeclared
73
75
 
76
+ # Watch mode (re-scans on file changes)
77
+ ghostimport --watch
78
+
74
79
  # Help
75
80
  ghostimport --help
76
81
  ```
77
82
 
78
83
  ### Add to CI (GitHub Actions)
79
84
 
85
+ Use the built-in action for the simplest setup:
86
+
87
+ ```yaml
88
+ - name: Check for hallucinated packages
89
+ uses: FGuerreir0/ghostimport@main
90
+ with:
91
+ path: '.'
92
+ scary: 'false'
93
+ ```
94
+
95
+ Or run directly:
96
+
80
97
  ```yaml
81
98
  - name: Check for hallucinated packages
82
99
  run: npx ghostimport --quiet
@@ -84,6 +101,18 @@ ghostimport --help
84
101
 
85
102
  This step will fail (exit code 1) if any hallucinated packages are found.
86
103
 
104
+ ### Pre-commit
105
+
106
+ Add to `.pre-commit-config.yaml`:
107
+
108
+ ```yaml
109
+ repos:
110
+ - repo: https://github.com/FGuerreir0/ghostimport
111
+ rev: main
112
+ hooks:
113
+ - id: ghostimport
114
+ ```
115
+
87
116
  ---
88
117
 
89
118
  ## Programmatic API
@@ -163,6 +192,9 @@ interface ScanOptions {
163
192
  **Automatically ignores:**
164
193
  - Node.js built-ins (`fs`, `path`, `crypto`, `node:*`, ...)
165
194
  - Relative imports (`./`, `../`)
195
+ - Path aliases (`@/`, `~/`, `$lib/`, and tsconfig `paths`)
196
+ - URL/protocol imports (`https:`, `data:`, `bun:`, ...)
197
+ - Virtual modules (`virtual:`, Vite/Rollup internals)
166
198
  - `node_modules/`, `dist/`, `.git/`, `build/`
167
199
 
168
200
  **Supported file types:** `.js` `.jsx` `.ts` `.tsx` `.mjs` `.cjs`
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import path5 from "path";
5
- import { readFileSync } from "fs";
5
+ import { readFileSync, watch as fsWatch } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // src/scan.ts
@@ -79,6 +79,15 @@ function isBuiltin(name) {
79
79
  return BUILTIN_MODULES.has(name);
80
80
  }
81
81
  function toPackageName(importPath) {
82
+ if (importPath.startsWith("@/") || importPath.startsWith("~/") || importPath.startsWith("#") || importPath.startsWith("$")) {
83
+ return null;
84
+ }
85
+ if (/^[a-z][a-z0-9+.-]*:/i.test(importPath)) {
86
+ return null;
87
+ }
88
+ if (importPath.startsWith("\0") || importPath.startsWith("virtual:")) {
89
+ return null;
90
+ }
82
91
  if (importPath.startsWith("@")) {
83
92
  const parts = importPath.split("/");
84
93
  if (parts.length < 2) return null;
@@ -259,7 +268,7 @@ import path3 from "path";
259
268
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"]);
260
269
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
261
270
  function walkFiles(dir) {
262
- const results2 = [];
271
+ const results = [];
263
272
  function walk(current) {
264
273
  let entries;
265
274
  try {
@@ -271,12 +280,12 @@ function walkFiles(dir) {
271
280
  if (entry.isDirectory()) {
272
281
  if (!SKIP_DIRS.has(entry.name)) walk(path3.join(current, entry.name));
273
282
  } else if (entry.isFile()) {
274
- if (CODE_EXTS.has(path3.extname(entry.name))) results2.push(path3.join(current, entry.name));
283
+ if (CODE_EXTS.has(path3.extname(entry.name))) results.push(path3.join(current, entry.name));
275
284
  }
276
285
  }
277
286
  }
278
287
  walk(dir);
279
- return results2;
288
+ return results;
280
289
  }
281
290
  function readPackageJsonDeps(dir) {
282
291
  const pkgPath = path3.join(dir, "package.json");
@@ -338,6 +347,27 @@ function readWorkspacePackages(dir) {
338
347
  }
339
348
  return names;
340
349
  }
350
+ function readTsconfigPaths(dir) {
351
+ const aliases = /* @__PURE__ */ new Set();
352
+ const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
353
+ for (const name of candidates) {
354
+ const configPath = path3.join(dir, name);
355
+ if (!fs3.existsSync(configPath)) continue;
356
+ try {
357
+ const raw = fs3.readFileSync(configPath, "utf8");
358
+ const stripped = raw.replace(/\/\/.*$/gm, "");
359
+ const config2 = JSON.parse(stripped);
360
+ const paths = config2.compilerOptions?.paths;
361
+ if (!paths) continue;
362
+ for (const key of Object.keys(paths)) {
363
+ const prefix = key.replace(/\/\*$/, "").replace(/\*$/, "");
364
+ if (prefix) aliases.add(prefix);
365
+ }
366
+ } catch {
367
+ }
368
+ }
369
+ return aliases;
370
+ }
341
371
 
342
372
  // src/scan.ts
343
373
  var CONCURRENCY = 10;
@@ -346,6 +376,7 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
346
376
  const files = walkFiles(targetDir2);
347
377
  const declaredDeps = readPackageJsonDeps(targetDir2);
348
378
  const workspacePkgs = readWorkspacePackages(targetDir2);
379
+ const tsconfigAliases = readTsconfigPaths(targetDir2);
349
380
  const cache = useCache ? loadCache() : {};
350
381
  let cacheHits = 0;
351
382
  const importMap = /* @__PURE__ */ new Map();
@@ -362,9 +393,9 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
362
393
  }
363
394
  }
364
395
  const allPkgs = [...importMap.keys()].filter(
365
- (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg)
396
+ (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
366
397
  );
367
- const results2 = {
398
+ const results = {
368
399
  scanned: files.length,
369
400
  packages: allPkgs.length,
370
401
  hallucinated: [],
@@ -394,24 +425,24 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
394
425
  }
395
426
  onProgress?.({ pkg, exists, error, total: allPkgs.length, done: i + j + 1 });
396
427
  if (exists === false) {
397
- results2.hallucinated.push({ pkg, files: matchedFiles });
428
+ results.hallucinated.push({ pkg, files: matchedFiles });
398
429
  } else if (exists === null && error) {
399
- results2.errors.push({ pkg, error, files: matchedFiles });
430
+ results.errors.push({ pkg, error, files: matchedFiles });
400
431
  } else if (exists === true && !declaredDeps.has(pkg)) {
401
- results2.notInPackageJson.push({ pkg, files: matchedFiles });
432
+ results.notInPackageJson.push({ pkg, files: matchedFiles });
402
433
  }
403
434
  }
404
435
  }
405
436
  if (useCache) saveCache(cache);
406
- results2.cacheHits = cacheHits;
437
+ results.cacheHits = cacheHits;
407
438
  if (scary) {
408
- for (const { pkg, files: matchedFiles } of results2.hallucinated) {
409
- results2.scary.push({ pkg, files: matchedFiles, type: "available" });
439
+ for (const { pkg, files: matchedFiles } of results.hallucinated) {
440
+ results.scary.push({ pkg, files: matchedFiles, type: "available" });
410
441
  }
411
- for (const { pkg, files: matchedFiles } of results2.notInPackageJson) {
442
+ for (const { pkg, files: matchedFiles } of results.notInPackageJson) {
412
443
  const info = await checkScary(pkg);
413
444
  if (info.exists === true && info.risk !== "low") {
414
- results2.scary.push({
445
+ results.scary.push({
415
446
  pkg,
416
447
  files: matchedFiles,
417
448
  type: "suspicious",
@@ -425,7 +456,7 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
425
456
  }
426
457
  }
427
458
  }
428
- return results2;
459
+ return results;
429
460
  }
430
461
 
431
462
  // src/cli.ts
@@ -448,6 +479,7 @@ var targetDir = path5.resolve(args.find((a) => !a.startsWith("--") && !a.startsW
448
479
  var flags = {
449
480
  quiet: args.includes("--quiet") || args.includes("-q"),
450
481
  json: args.includes("--json"),
482
+ watch: args.includes("--watch") || args.includes("-w"),
451
483
  noUndeclared: args.includes("--no-undeclared"),
452
484
  noCache: args.includes("--no-cache"),
453
485
  scary: args.includes("--scary"),
@@ -468,6 +500,7 @@ ${c.bold("Usage:")}
468
500
  ${c.bold("Options:")}
469
501
  --quiet, -q Only show problems (no progress)
470
502
  --json Output results as JSON
503
+ --watch, -w Watch for file changes and re-scan
471
504
  --no-undeclared Skip "imported but not in package.json" warnings
472
505
  --scary Check for supply chain attack risk (squatting)
473
506
  --no-cache Skip the local registry cache
@@ -491,97 +524,121 @@ ${c.bold("Examples:")}
491
524
  process.exit(0);
492
525
  }
493
526
  var config = loadConfig(targetDir);
494
- if (!flags.quiet && !flags.json) {
495
- console.log(`
527
+ async function runScan() {
528
+ if (!flags.quiet && !flags.json) {
529
+ console.log(`
496
530
  ${c.bold("ghostimport")} ${c.gray(`v${version}`)}`);
497
- console.log(c.gray(`Scanning ${targetDir}`));
498
- if (flags.scary) console.log(c.magenta(" \u26A0 Scary mode: checking supply chain risk"));
499
- console.log();
500
- }
501
- var lastProgress = "";
502
- var results = await scan(targetDir, {
503
- useCache: !flags.noCache,
504
- scary: flags.scary,
505
- config,
506
- onProgress: flags.json || flags.quiet ? void 0 : ({ pkg, done, total }) => {
507
- const pct = Math.round(done / total * 100);
508
- const bar = "\u2588".repeat(Math.floor(pct / 5)) + "\u2591".repeat(20 - Math.floor(pct / 5));
509
- const line = ` ${c.gray(bar)} ${pct}% ${c.dim(pkg.slice(0, 30))}`;
510
- process.stdout.write("\r" + line + " ".repeat(Math.max(0, lastProgress.length - line.length)));
511
- lastProgress = line;
531
+ console.log(c.gray(`Scanning ${targetDir}`));
532
+ if (flags.scary) console.log(c.magenta(" \u26A0 Scary mode: checking supply chain risk"));
533
+ console.log();
512
534
  }
513
- });
514
- if (!flags.json && !flags.quiet && lastProgress) {
515
- process.stdout.write("\r" + " ".repeat(lastProgress.length + 5) + "\r");
516
- }
517
- if (flags.json) {
518
- console.log(JSON.stringify(results, null, 2));
519
- process.exit(results.hallucinated.length > 0 ? 1 : 0);
520
- }
521
- var totalIssues = results.hallucinated.length + (flags.noUndeclared ? 0 : results.notInPackageJson.length);
522
- console.log(
523
- ` ${c.gray("Scanned")} ${c.cyan(results.scanned + " files")} \xB7 ${c.cyan(results.packages + " unique packages")} checked` + (results.cacheHits > 0 ? ` ${c.gray(`(${results.cacheHits} cached)`)}` : "") + "\n"
524
- );
525
- if (results.hallucinated.length === 0) {
526
- console.log(c.green(" \u2713 No hallucinated packages found"));
527
- } else {
528
- console.log(c.bold(c.red(` \u2717 ${results.hallucinated.length} hallucinated package${results.hallucinated.length > 1 ? "s" : ""} (do not exist on npm):
529
- `)));
530
- for (const { pkg, files } of results.hallucinated) {
531
- console.log(` ${c.red("\u25CF")} ${c.bold(pkg)}`);
532
- for (const f of files.slice(0, 3)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
533
- if (files.length > 3) console.log(` ${c.gray(`\u21B3 ...and ${files.length - 3} more files`)}`);
535
+ let lastProgress = "";
536
+ const results = await scan(targetDir, {
537
+ useCache: !flags.noCache,
538
+ scary: flags.scary,
539
+ config,
540
+ onProgress: flags.json || flags.quiet ? void 0 : ({ pkg, done, total }) => {
541
+ const pct = Math.round(done / total * 100);
542
+ const bar = "\u2588".repeat(Math.floor(pct / 5)) + "\u2591".repeat(20 - Math.floor(pct / 5));
543
+ const line = ` ${c.gray(bar)} ${pct}% ${c.dim(pkg.slice(0, 30))}`;
544
+ process.stdout.write("\r" + line + " ".repeat(Math.max(0, lastProgress.length - line.length)));
545
+ lastProgress = line;
546
+ }
547
+ });
548
+ if (!flags.json && !flags.quiet && lastProgress) {
549
+ process.stdout.write("\r" + " ".repeat(lastProgress.length + 5) + "\r");
534
550
  }
535
- }
536
- if (flags.scary && results.scary.length > 0) {
537
- console.log();
538
- const available = results.scary.filter((s) => s.type === "available");
539
- const suspicious = results.scary.filter((s) => s.type === "suspicious");
540
- if (available.length > 0) {
541
- console.log(c.bold(c.magenta(` \u{1F480} ${available.length} package name${available.length > 1 ? "s" : ""} available for malicious registration:
551
+ if (flags.json) {
552
+ console.log(JSON.stringify(results, null, 2));
553
+ return results.hallucinated.length;
554
+ }
555
+ const totalIssues = results.hallucinated.length + (flags.noUndeclared ? 0 : results.notInPackageJson.length);
556
+ console.log(
557
+ ` ${c.gray("Scanned")} ${c.cyan(results.scanned + " files")} \xB7 ${c.cyan(results.packages + " unique packages")} checked` + (results.cacheHits > 0 ? ` ${c.gray(`(${results.cacheHits} cached)`)}` : "") + "\n"
558
+ );
559
+ if (results.hallucinated.length === 0) {
560
+ console.log(c.green(" \u2713 No hallucinated packages found"));
561
+ } else {
562
+ console.log(c.bold(c.red(` \u2717 ${results.hallucinated.length} hallucinated package${results.hallucinated.length > 1 ? "s" : ""} (do not exist on npm):
542
563
  `)));
543
- for (const { pkg } of available) {
544
- console.log(` ${c.magenta("\u25CF")} ${c.bold(pkg)}`);
545
- console.log(` ${c.red("\u21B3 Anyone can register this name with a malicious postinstall script")}`);
546
- console.log(` ${c.red("\u21B3 If installed, it could exfiltrate .env, tokens, SSH keys")}`);
564
+ for (const { pkg, files } of results.hallucinated) {
565
+ console.log(` ${c.red("\u25CF")} ${c.bold(pkg)}`);
566
+ for (const f of files.slice(0, 3)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
567
+ if (files.length > 3) console.log(` ${c.gray(`\u21B3 ...and ${files.length - 3} more files`)}`);
547
568
  }
548
569
  }
549
- if (suspicious.length > 0) {
570
+ if (flags.scary && results.scary.length > 0) {
550
571
  console.log();
551
- console.log(c.bold(c.magenta(` \u{1F575}\uFE0F ${suspicious.length} suspicious package${suspicious.length > 1 ? "s" : ""} (potential squats):
572
+ const available = results.scary.filter((s) => s.type === "available");
573
+ const suspicious = results.scary.filter((s) => s.type === "suspicious");
574
+ if (available.length > 0) {
575
+ console.log(c.bold(c.magenta(` \u{1F480} ${available.length} package name${available.length > 1 ? "s" : ""} available for malicious registration:
576
+ `)));
577
+ for (const { pkg } of available) {
578
+ console.log(` ${c.magenta("\u25CF")} ${c.bold(pkg)}`);
579
+ console.log(` ${c.red("\u21B3 Anyone can register this name with a malicious postinstall script")}`);
580
+ console.log(` ${c.red("\u21B3 If installed, it could exfiltrate .env, tokens, SSH keys")}`);
581
+ }
582
+ }
583
+ if (suspicious.length > 0) {
584
+ console.log();
585
+ console.log(c.bold(c.magenta(` \u{1F575}\uFE0F ${suspicious.length} suspicious package${suspicious.length > 1 ? "s" : ""} (potential squats):
552
586
  `)));
553
- for (const entry of suspicious) {
554
- if (entry.type !== "suspicious") continue;
555
- console.log(` ${c.magenta("\u25CF")} ${c.bold(entry.pkg)} ${c.gray(`(created ${entry.created}, ${entry.downloads ?? "?"} downloads/week)`)}`);
556
- for (const flag of entry.flags) console.log(` ${c.yellow("\u21B3")} ${flag}`);
587
+ for (const entry of suspicious) {
588
+ if (entry.type !== "suspicious") continue;
589
+ console.log(` ${c.magenta("\u25CF")} ${c.bold(entry.pkg)} ${c.gray(`(created ${entry.created}, ${entry.downloads ?? "?"} downloads/week)`)}`);
590
+ for (const flag of entry.flags) console.log(` ${c.yellow("\u21B3")} ${flag}`);
591
+ }
557
592
  }
558
593
  }
559
- }
560
- if (!flags.noUndeclared && results.notInPackageJson.length > 0) {
561
- console.log();
562
- console.log(c.bold(c.yellow(` \u26A0 ${results.notInPackageJson.length} package${results.notInPackageJson.length > 1 ? "s" : ""} imported but missing from package.json:
594
+ if (!flags.noUndeclared && results.notInPackageJson.length > 0) {
595
+ console.log();
596
+ console.log(c.bold(c.yellow(` \u26A0 ${results.notInPackageJson.length} package${results.notInPackageJson.length > 1 ? "s" : ""} imported but missing from package.json:
563
597
  `)));
564
- for (const { pkg, files } of results.notInPackageJson.slice(0, 10)) {
565
- console.log(` ${c.yellow("\u25CF")} ${c.bold(pkg)}`);
566
- for (const f of files.slice(0, 2)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
598
+ for (const { pkg, files } of results.notInPackageJson.slice(0, 10)) {
599
+ console.log(` ${c.yellow("\u25CF")} ${c.bold(pkg)}`);
600
+ for (const f of files.slice(0, 2)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
601
+ }
602
+ if (results.notInPackageJson.length > 10) {
603
+ console.log(` ${c.gray(` ...and ${results.notInPackageJson.length - 10} more`)}`);
604
+ }
567
605
  }
568
- if (results.notInPackageJson.length > 10) {
569
- console.log(` ${c.gray(` ...and ${results.notInPackageJson.length - 10} more`)}`);
606
+ if (results.errors.length > 0) {
607
+ console.log();
608
+ console.log(c.gray(` \u26A1 ${results.errors.length} package(s) could not be checked (network/timeout)`));
570
609
  }
571
- }
572
- if (results.errors.length > 0) {
573
610
  console.log();
574
- console.log(c.gray(` \u26A1 ${results.errors.length} package(s) could not be checked (network/timeout)`));
575
- }
576
- console.log();
577
- if (totalIssues === 0 && results.scary.length === 0) {
578
- console.log(c.green(c.bold(" All good! \u2713\n")));
579
- } else if (flags.scary && results.scary.length > 0) {
580
- const scaryCount = results.scary.filter((s) => s.type === "available").length;
581
- console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""} \xB7 ${scaryCount} supply chain risk${scaryCount > 1 ? "s" : ""}
611
+ if (totalIssues === 0 && results.scary.length === 0) {
612
+ console.log(c.green(c.bold(" All good! \u2713\n")));
613
+ } else if (flags.scary && results.scary.length > 0) {
614
+ const scaryCount = results.scary.filter((s) => s.type === "available").length;
615
+ console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""} \xB7 ${scaryCount} supply chain risk${scaryCount > 1 ? "s" : ""}
582
616
  `)));
583
- } else {
584
- console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""}.
617
+ } else {
618
+ console.log(c.red(c.bold(` Found ${totalIssues} issue${totalIssues > 1 ? "s" : ""}.
585
619
  `)));
620
+ }
621
+ return results.hallucinated.length;
622
+ }
623
+ var issues = await runScan();
624
+ if (flags.watch) {
625
+ const CODE_EXTS2 = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
626
+ let debounce = null;
627
+ console.log(c.gray(` Watching for changes in ${targetDir}...
628
+ `));
629
+ fsWatch(targetDir, { recursive: true }, (_event, filename) => {
630
+ if (!filename) return;
631
+ const ext = path5.extname(filename);
632
+ if (!CODE_EXTS2.has(ext)) return;
633
+ if (filename.includes("node_modules") || filename.includes("dist")) return;
634
+ if (debounce) clearTimeout(debounce);
635
+ debounce = setTimeout(async () => {
636
+ console.clear();
637
+ await runScan();
638
+ console.log(c.gray(` Watching for changes...
639
+ `));
640
+ }, 300);
641
+ });
642
+ } else {
643
+ process.exit(issues > 0 ? 1 : 0);
586
644
  }
587
- process.exit(results.hallucinated.length > 0 ? 1 : 0);
package/dist/files.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare function walkFiles(dir: string): string[];
2
2
  export declare function readPackageJsonDeps(dir: string): Set<string>;
3
3
  export declare function readWorkspacePackages(dir: string): Set<string>;
4
+ export declare function readTsconfigPaths(dir: string): Set<string>;
package/dist/index.cjs CHANGED
@@ -112,6 +112,15 @@ function isBuiltin(name) {
112
112
  return BUILTIN_MODULES.has(name);
113
113
  }
114
114
  function toPackageName(importPath) {
115
+ if (importPath.startsWith("@/") || importPath.startsWith("~/") || importPath.startsWith("#") || importPath.startsWith("$")) {
116
+ return null;
117
+ }
118
+ if (/^[a-z][a-z0-9+.-]*:/i.test(importPath)) {
119
+ return null;
120
+ }
121
+ if (importPath.startsWith("\0") || importPath.startsWith("virtual:")) {
122
+ return null;
123
+ }
115
124
  if (importPath.startsWith("@")) {
116
125
  const parts = importPath.split("/");
117
126
  if (parts.length < 2) return null;
@@ -371,6 +380,27 @@ function readWorkspacePackages(dir) {
371
380
  }
372
381
  return names;
373
382
  }
383
+ function readTsconfigPaths(dir) {
384
+ const aliases = /* @__PURE__ */ new Set();
385
+ const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
386
+ for (const name of candidates) {
387
+ const configPath = import_path3.default.join(dir, name);
388
+ if (!import_fs3.default.existsSync(configPath)) continue;
389
+ try {
390
+ const raw = import_fs3.default.readFileSync(configPath, "utf8");
391
+ const stripped = raw.replace(/\/\/.*$/gm, "");
392
+ const config = JSON.parse(stripped);
393
+ const paths = config.compilerOptions?.paths;
394
+ if (!paths) continue;
395
+ for (const key of Object.keys(paths)) {
396
+ const prefix = key.replace(/\/\*$/, "").replace(/\*$/, "");
397
+ if (prefix) aliases.add(prefix);
398
+ }
399
+ } catch {
400
+ }
401
+ }
402
+ return aliases;
403
+ }
374
404
 
375
405
  // src/scan.ts
376
406
  var import_fs4 = __toESM(require("fs"), 1);
@@ -381,6 +411,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
381
411
  const files = walkFiles(targetDir);
382
412
  const declaredDeps = readPackageJsonDeps(targetDir);
383
413
  const workspacePkgs = readWorkspacePackages(targetDir);
414
+ const tsconfigAliases = readTsconfigPaths(targetDir);
384
415
  const cache = useCache ? loadCache() : {};
385
416
  let cacheHits = 0;
386
417
  const importMap = /* @__PURE__ */ new Map();
@@ -397,7 +428,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
397
428
  }
398
429
  }
399
430
  const allPkgs = [...importMap.keys()].filter(
400
- (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg)
431
+ (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
401
432
  );
402
433
  const results = {
403
434
  scanned: files.length,
package/dist/index.js CHANGED
@@ -68,6 +68,15 @@ function isBuiltin(name) {
68
68
  return BUILTIN_MODULES.has(name);
69
69
  }
70
70
  function toPackageName(importPath) {
71
+ if (importPath.startsWith("@/") || importPath.startsWith("~/") || importPath.startsWith("#") || importPath.startsWith("$")) {
72
+ return null;
73
+ }
74
+ if (/^[a-z][a-z0-9+.-]*:/i.test(importPath)) {
75
+ return null;
76
+ }
77
+ if (importPath.startsWith("\0") || importPath.startsWith("virtual:")) {
78
+ return null;
79
+ }
71
80
  if (importPath.startsWith("@")) {
72
81
  const parts = importPath.split("/");
73
82
  if (parts.length < 2) return null;
@@ -327,6 +336,27 @@ function readWorkspacePackages(dir) {
327
336
  }
328
337
  return names;
329
338
  }
339
+ function readTsconfigPaths(dir) {
340
+ const aliases = /* @__PURE__ */ new Set();
341
+ const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
342
+ for (const name of candidates) {
343
+ const configPath = path3.join(dir, name);
344
+ if (!fs3.existsSync(configPath)) continue;
345
+ try {
346
+ const raw = fs3.readFileSync(configPath, "utf8");
347
+ const stripped = raw.replace(/\/\/.*$/gm, "");
348
+ const config = JSON.parse(stripped);
349
+ const paths = config.compilerOptions?.paths;
350
+ if (!paths) continue;
351
+ for (const key of Object.keys(paths)) {
352
+ const prefix = key.replace(/\/\*$/, "").replace(/\*$/, "");
353
+ if (prefix) aliases.add(prefix);
354
+ }
355
+ } catch {
356
+ }
357
+ }
358
+ return aliases;
359
+ }
330
360
 
331
361
  // src/scan.ts
332
362
  import fs4 from "fs";
@@ -337,6 +367,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
337
367
  const files = walkFiles(targetDir);
338
368
  const declaredDeps = readPackageJsonDeps(targetDir);
339
369
  const workspacePkgs = readWorkspacePackages(targetDir);
370
+ const tsconfigAliases = readTsconfigPaths(targetDir);
340
371
  const cache = useCache ? loadCache() : {};
341
372
  let cacheHits = 0;
342
373
  const importMap = /* @__PURE__ */ new Map();
@@ -353,7 +384,7 @@ async function scan(targetDir, { onProgress, useCache = true, scary = false, con
353
384
  }
354
385
  }
355
386
  const allPkgs = [...importMap.keys()].filter(
356
- (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg)
387
+ (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
357
388
  );
358
389
  const results = {
359
390
  scanned: files.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostimport",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Detects ghost imports in your code - imports that don't exist, hallucinated by AI coding tools.",
5
5
  "keywords": [
6
6
  "npm",
@@ -12,7 +12,13 @@
12
12
  "imports",
13
13
  "security",
14
14
  "lint",
15
- "ghost"
15
+ "ghost",
16
+ "import-validation",
17
+ "ai-safety",
18
+ "supply-chain",
19
+ "hallucination-detection",
20
+ "copilot",
21
+ "pre-commit"
16
22
  ],
17
23
  "homepage": "https://github.com/FGuerreir0/ghostimport#readme",
18
24
  "bugs": {
@@ -50,6 +56,7 @@
50
56
  "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
51
57
  "test": "tsx tests/test.ts",
52
58
  "dev": "tsx src/cli.ts",
59
+ "prepare": "git config core.hooksPath .githooks",
53
60
  "prepublishOnly": "npm run build"
54
61
  },
55
62
  "devDependencies": {