ghostimport 0.1.1 → 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.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/cli.js +117 -91
  3. package/package.json +9 -2
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
@@ -268,7 +268,7 @@ import path3 from "path";
268
268
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"]);
269
269
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
270
270
  function walkFiles(dir) {
271
- const results2 = [];
271
+ const results = [];
272
272
  function walk(current) {
273
273
  let entries;
274
274
  try {
@@ -280,12 +280,12 @@ function walkFiles(dir) {
280
280
  if (entry.isDirectory()) {
281
281
  if (!SKIP_DIRS.has(entry.name)) walk(path3.join(current, entry.name));
282
282
  } else if (entry.isFile()) {
283
- 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));
284
284
  }
285
285
  }
286
286
  }
287
287
  walk(dir);
288
- return results2;
288
+ return results;
289
289
  }
290
290
  function readPackageJsonDeps(dir) {
291
291
  const pkgPath = path3.join(dir, "package.json");
@@ -395,7 +395,7 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
395
395
  const allPkgs = [...importMap.keys()].filter(
396
396
  (pkg) => !matchesIgnore(pkg, conf.ignore) && !workspacePkgs.has(pkg) && !tsconfigAliases.has(pkg)
397
397
  );
398
- const results2 = {
398
+ const results = {
399
399
  scanned: files.length,
400
400
  packages: allPkgs.length,
401
401
  hallucinated: [],
@@ -425,24 +425,24 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
425
425
  }
426
426
  onProgress?.({ pkg, exists, error, total: allPkgs.length, done: i + j + 1 });
427
427
  if (exists === false) {
428
- results2.hallucinated.push({ pkg, files: matchedFiles });
428
+ results.hallucinated.push({ pkg, files: matchedFiles });
429
429
  } else if (exists === null && error) {
430
- results2.errors.push({ pkg, error, files: matchedFiles });
430
+ results.errors.push({ pkg, error, files: matchedFiles });
431
431
  } else if (exists === true && !declaredDeps.has(pkg)) {
432
- results2.notInPackageJson.push({ pkg, files: matchedFiles });
432
+ results.notInPackageJson.push({ pkg, files: matchedFiles });
433
433
  }
434
434
  }
435
435
  }
436
436
  if (useCache) saveCache(cache);
437
- results2.cacheHits = cacheHits;
437
+ results.cacheHits = cacheHits;
438
438
  if (scary) {
439
- for (const { pkg, files: matchedFiles } of results2.hallucinated) {
440
- 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" });
441
441
  }
442
- for (const { pkg, files: matchedFiles } of results2.notInPackageJson) {
442
+ for (const { pkg, files: matchedFiles } of results.notInPackageJson) {
443
443
  const info = await checkScary(pkg);
444
444
  if (info.exists === true && info.risk !== "low") {
445
- results2.scary.push({
445
+ results.scary.push({
446
446
  pkg,
447
447
  files: matchedFiles,
448
448
  type: "suspicious",
@@ -456,7 +456,7 @@ async function scan(targetDir2, { onProgress, useCache = true, scary = false, co
456
456
  }
457
457
  }
458
458
  }
459
- return results2;
459
+ return results;
460
460
  }
461
461
 
462
462
  // src/cli.ts
@@ -479,6 +479,7 @@ var targetDir = path5.resolve(args.find((a) => !a.startsWith("--") && !a.startsW
479
479
  var flags = {
480
480
  quiet: args.includes("--quiet") || args.includes("-q"),
481
481
  json: args.includes("--json"),
482
+ watch: args.includes("--watch") || args.includes("-w"),
482
483
  noUndeclared: args.includes("--no-undeclared"),
483
484
  noCache: args.includes("--no-cache"),
484
485
  scary: args.includes("--scary"),
@@ -499,6 +500,7 @@ ${c.bold("Usage:")}
499
500
  ${c.bold("Options:")}
500
501
  --quiet, -q Only show problems (no progress)
501
502
  --json Output results as JSON
503
+ --watch, -w Watch for file changes and re-scan
502
504
  --no-undeclared Skip "imported but not in package.json" warnings
503
505
  --scary Check for supply chain attack risk (squatting)
504
506
  --no-cache Skip the local registry cache
@@ -522,97 +524,121 @@ ${c.bold("Examples:")}
522
524
  process.exit(0);
523
525
  }
524
526
  var config = loadConfig(targetDir);
525
- if (!flags.quiet && !flags.json) {
526
- console.log(`
527
+ async function runScan() {
528
+ if (!flags.quiet && !flags.json) {
529
+ console.log(`
527
530
  ${c.bold("ghostimport")} ${c.gray(`v${version}`)}`);
528
- console.log(c.gray(`Scanning ${targetDir}`));
529
- if (flags.scary) console.log(c.magenta(" \u26A0 Scary mode: checking supply chain risk"));
530
- console.log();
531
- }
532
- var lastProgress = "";
533
- var results = await scan(targetDir, {
534
- useCache: !flags.noCache,
535
- scary: flags.scary,
536
- config,
537
- onProgress: flags.json || flags.quiet ? void 0 : ({ pkg, done, total }) => {
538
- const pct = Math.round(done / total * 100);
539
- const bar = "\u2588".repeat(Math.floor(pct / 5)) + "\u2591".repeat(20 - Math.floor(pct / 5));
540
- const line = ` ${c.gray(bar)} ${pct}% ${c.dim(pkg.slice(0, 30))}`;
541
- process.stdout.write("\r" + line + " ".repeat(Math.max(0, lastProgress.length - line.length)));
542
- 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();
543
534
  }
544
- });
545
- if (!flags.json && !flags.quiet && lastProgress) {
546
- process.stdout.write("\r" + " ".repeat(lastProgress.length + 5) + "\r");
547
- }
548
- if (flags.json) {
549
- console.log(JSON.stringify(results, null, 2));
550
- process.exit(results.hallucinated.length > 0 ? 1 : 0);
551
- }
552
- var totalIssues = results.hallucinated.length + (flags.noUndeclared ? 0 : results.notInPackageJson.length);
553
- console.log(
554
- ` ${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"
555
- );
556
- if (results.hallucinated.length === 0) {
557
- console.log(c.green(" \u2713 No hallucinated packages found"));
558
- } else {
559
- console.log(c.bold(c.red(` \u2717 ${results.hallucinated.length} hallucinated package${results.hallucinated.length > 1 ? "s" : ""} (do not exist on npm):
560
- `)));
561
- for (const { pkg, files } of results.hallucinated) {
562
- console.log(` ${c.red("\u25CF")} ${c.bold(pkg)}`);
563
- for (const f of files.slice(0, 3)) console.log(` ${c.gray("\u21B3")} ${c.dim(f)}`);
564
- 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");
565
550
  }
566
- }
567
- if (flags.scary && results.scary.length > 0) {
568
- console.log();
569
- const available = results.scary.filter((s) => s.type === "available");
570
- const suspicious = results.scary.filter((s) => s.type === "suspicious");
571
- if (available.length > 0) {
572
- 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):
573
563
  `)));
574
- for (const { pkg } of available) {
575
- console.log(` ${c.magenta("\u25CF")} ${c.bold(pkg)}`);
576
- console.log(` ${c.red("\u21B3 Anyone can register this name with a malicious postinstall script")}`);
577
- 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`)}`);
578
568
  }
579
569
  }
580
- if (suspicious.length > 0) {
570
+ if (flags.scary && results.scary.length > 0) {
581
571
  console.log();
582
- 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):
583
586
  `)));
584
- for (const entry of suspicious) {
585
- if (entry.type !== "suspicious") continue;
586
- console.log(` ${c.magenta("\u25CF")} ${c.bold(entry.pkg)} ${c.gray(`(created ${entry.created}, ${entry.downloads ?? "?"} downloads/week)`)}`);
587
- 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
+ }
588
592
  }
589
593
  }
590
- }
591
- if (!flags.noUndeclared && results.notInPackageJson.length > 0) {
592
- console.log();
593
- 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:
594
597
  `)));
595
- for (const { pkg, files } of results.notInPackageJson.slice(0, 10)) {
596
- console.log(` ${c.yellow("\u25CF")} ${c.bold(pkg)}`);
597
- 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
+ }
598
605
  }
599
- if (results.notInPackageJson.length > 10) {
600
- 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)`));
601
609
  }
602
- }
603
- if (results.errors.length > 0) {
604
610
  console.log();
605
- console.log(c.gray(` \u26A1 ${results.errors.length} package(s) could not be checked (network/timeout)`));
606
- }
607
- console.log();
608
- if (totalIssues === 0 && results.scary.length === 0) {
609
- console.log(c.green(c.bold(" All good! \u2713\n")));
610
- } else if (flags.scary && results.scary.length > 0) {
611
- const scaryCount = results.scary.filter((s) => s.type === "available").length;
612
- 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" : ""}
613
616
  `)));
614
- } else {
615
- 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" : ""}.
616
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);
617
644
  }
618
- process.exit(results.hallucinated.length > 0 ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostimport",
3
- "version": "0.1.1",
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": {