Skip to content

fix: npx usage #613#4630

Merged
escapedcat merged 15 commits intomasterfrom
fix/613_npx-usage
Mar 2, 2026
Merged

fix: npx usage #613#4630
escapedcat merged 15 commits intomasterfrom
fix/613_npx-usage

Conversation

@escapedcat
Copy link
Copy Markdown
Member

@escapedcat escapedcat commented Feb 28, 2026

User description

Fixes: #613


PR Type

Bug fix


Description

  • Add npx cache detection for package and plugin resolution

  • Support running commitlint via npx with extended configs

  • Refactor loadPlugin to accept options object with searchPaths

  • Enable plugin resolution from npx cache (for npx/npm exec usage)


Diagram Walkthrough

flowchart LR
  npx["npx execution"] -->|cache detection| cache["~/.npm/_npx cache"]
  cache -->|resolve extends| extends["resolve-extends"]
  cache -->|resolve plugins| plugins["load-plugin"]
  extends -->|config resolution| config["Extended configs"]
  plugins -->|plugin resolution| loaded["Loaded plugins"]
Loading

File Walkthrough

Relevant files
Enhancement
index.ts
Add npx cache detection for config resolution                       

@commitlint/resolve-extends/src/index.ts

  • Add getNpxCachePath() function to detect and locate npx cache
    directory
  • Integrate npx cache path into resolveGlobalSilent() for config
    resolution
  • Check most recently modified npx cache directory for package
    resolution
  • Handle errors gracefully when reading npx cache
+41/-0   
load-plugin.ts
Refactor plugin loading with npx cache support                     

@commitlint/load/src/utils/load-plugin.ts

  • Add getNpxCachePath() function to detect npx cache directory
  • Create LoadPluginOptions interface with debug and searchPaths
    properties
  • Refactor loadPlugin() to accept options object instead of boolean
    debug parameter
  • Implement search path priority: npx cache first, then provided
    searchPaths, then default resolution
  • Add fallback logic to resolve plugin paths when not found in search
    paths
+88/-22 
load.ts
Update loadPlugin call signature                                                 

@commitlint/load/src/load.ts

  • Update loadPlugin() call to use new options object format
  • Pass debug flag within options object instead of as separate parameter
  • Maintain backward compatibility with default options
+3/-5     

When running commitlint via npx or npm exec, packages are downloaded
to a temporary npx cache directory (e.g., ~/.npm/_npx) instead of
traditional global node_modules. This commit adds detection and
resolution from the npx cache directory to support running
commitlint via npx.

This helps with issue #613 - enabling usage like:
  npx @commitlint/cli --extends @commitlint/config-conventional

Part of fixing #613
- Add npx cache detection to load-plugin.ts (similar to resolve-extends)
- Add LoadPluginOptions interface with searchPaths support
- This enables plugins to be resolved from npx cache when running
  via npx/npm exec, and from extended config paths when provided

This helps with issue #613 - enabling plugin resolution when using:
  npx @commitlint/cli --extends @commitlint/config-conventional

Part of fixing #613
@escapedcat escapedcat changed the title Fix/613 npx usage fix: npx usage #613 Feb 28, 2026
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Feb 28, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Untrusted module loading

Description: The new plugin resolution logic searches and loads modules from the most-recent
~/.npm/_npx/

/node_modules path, which could allow loading attacker-controlled code if that
cache location is writable/poisoned on the system or in shared environments, expanding the
trust boundary beyond normal Node resolution.
load-plugin.ts [17-150]

Referred Code
function getNpxCachePath(): string | void {
	const home = os.homedir();
	const npxPath = path.join(home, ".npm", "_npx");

	if (!fs.existsSync(npxPath)) {
		return undefined;
	}

	try {
		const entries = fs.readdirSync(npxPath, { withFileTypes: true });
		const dirs = entries
			.filter((entry) => entry.isDirectory())
			.map((entry) => ({
				name: entry.name,
				mtime: fs.statSync(path.join(npxPath, entry.name)).mtime,
			}))
			.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());

		if (dirs.length > 0) {
			return path.join(npxPath, dirs[0].name, "node_modules");
		}


 ... (clipped 113 lines)
Ticket Compliance
🟡
🎫 #613
🟢 commitlint should support being executed via npx while still being able to resolve
extended configs (e.g., extends: ['@commitlint/config-conventional']).
When @commitlint/cli and @commitlint/config-conventional are installed globally, running
commitlint from any directory should successfully resolve @commitlint/config-conventional.

Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Swallowed exceptions: Multiple catch {} blocks silently ignore filesystem and module-load errors (e.g., npx
cache read and search-path imports), reducing diagnosability and potentially hiding real
failure causes.

Referred Code
	try {
		const entries = fs.readdirSync(npxPath, { withFileTypes: true });
		const dirs = entries
			.filter((entry) => entry.isDirectory())
			.map((entry) => ({
				name: entry.name,
				mtime: fs.statSync(path.join(npxPath, entry.name)).mtime,
			}))
			.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());

		if (dirs.length > 0) {
			return path.join(npxPath, dirs[0].name, "node_modules");
		}
	} catch {
		// Ignore errors reading npx cache
	}

	return undefined;
}

const dynamicImport = async <T>(id: string): Promise<T> => {


 ... (clipped 81 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Error details exposed: The thrown MissingPluginError includes the raw underlying resolver error?.message, which
may expose internal paths/environment details depending on how the error is surfaced to
end users.

Referred Code
const message = error?.message || "Unknown error occurred";
throw new MissingPluginError(pluginName, message, {
	pluginName: longName,
	commitlintPath: path.resolve(__dirname, "../.."),
});

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured console logs: New/relocated console.error and debug console.log output is unstructured and may include
filesystem paths (e.g., resolvedPath), which could be sensitive depending on the runtime
context and log collection.

Referred Code
			console.error(pc.red(`Failed to load plugin ${longName}.`));

			const message = error?.message || "Unknown error occurred";
			throw new MissingPluginError(pluginName, message, {
				pluginName: longName,
				commitlintPath: path.resolve(__dirname, "../.."),
			});
		}

		throw pluginLoadErr;
	}
}

// This step is costly, so skip if debug is disabled
if (debug) {
	if (!resolvedPath) {
		try {
			resolvedPath = require.resolve(longName);
		} catch {
			// Ignore
		}


 ... (clipped 19 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated search paths: The new searchPaths option is used for filesystem-based module resolution without
validation/normalization, which could be risky if any portion is influenced by untrusted
configuration inputs.

Referred Code
export interface LoadPluginOptions {
	debug?: boolean;
	searchPaths?: string[];
}

export default async function loadPlugin(
	plugins: PluginRecords,
	pluginName: string,
	options: LoadPluginOptions = {},
): Promise<PluginRecords> {
	const { debug = false, searchPaths = [] } = options;
	const longName = normalizePackageName(pluginName);
	const shortName = getShorthandName(longName);

	// Get npx cache path for additional search
	const npxCachePath = getNpxCachePath();
	const allSearchPaths = npxCachePath
		? [npxCachePath, ...searchPaths]
		: searchPaths;

	if (pluginName.match(/\s+/u)) {


 ... (clipped 24 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@codesandbox-ci
Copy link
Copy Markdown

codesandbox-ci bot commented Feb 28, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Feb 28, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Use require.resolve for search paths
Suggestion Impact:The plugin-loading logic was changed to resolve plugins from custom search paths using require.resolve(longName, { paths: [searchPath] }) and then import the resolved entry, replacing the prior fs.existsSync(path.join(searchPath, longName)) approach. It also added a separate npx-cache resolution step.

code diff:

-		// Try to load from additional search paths first (e.g., npx cache, extended config's node_modules)
-		for (const searchPath of allSearchPaths) {
-			const pluginPath = path.join(searchPath, longName);
-			if (fs.existsSync(pluginPath)) {
-				try {
-					plugin = await dynamicImport<Plugin>(pluginPath);
-					resolvedPath = pluginPath;
-					break;
-				} catch {
-					// Try next path
+		// Try to load from npx cache directories using require.resolve
+		const npxResolvedPath = resolveFromNpxCache(longName);
+		if (npxResolvedPath) {
+			try {
+				plugin = await dynamicImport<Plugin>(npxResolvedPath);
+				resolvedPath = npxResolvedPath;
+			} catch (err) {
+				if (debug) {
+					console.debug(
+						`Failed to load plugin ${longName} from npx cache: ${(err as Error).message}`,
+					);
 				}
 			}
 		}
 
-		// If not found in search paths, try default resolution
+		// Try to load from additional search paths (extended config's node_modules)
+		if (!plugin) {
+			for (const searchPath of searchPaths) {
+				try {
+					resolvedPath = require.resolve(longName, { paths: [searchPath] });
+					plugin = await dynamicImport<Plugin>(resolvedPath);
+					break;
+				} catch (err) {
+					if (debug) {
+						console.debug(
+							`Failed to load plugin ${longName} from ${searchPath}: ${(err as Error).message}`,
+						);
+					}
+				}
+			}
+		}

Refactor the plugin search loop to use require.resolve with the paths option.
This leverages Node's module resolution algorithm to correctly locate the
plugin's entry file within custom search paths.

@commitlint/load/src/utils/load-plugin.ts [85-96]

 for (const searchPath of allSearchPaths) {
-  const pluginPath = path.join(searchPath, longName);
-  if (fs.existsSync(pluginPath)) {
-    try {
-      plugin = await dynamicImport<Plugin>(pluginPath);
-      resolvedPath = pluginPath;
-      break;
-    } catch {
-      // Try next path
-    }
+  try {
+    const moduleEntry = require.resolve(longName, { paths: [searchPath] });
+    plugin = await dynamicImport<Plugin>(pathToFileURL(moduleEntry).toString());
+    resolvedPath = moduleEntry;
+    break;
+  } catch {
+    // Try next path
   }
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out a flaw in the current implementation, which only checks for a directory with the plugin's name. Using require.resolve with the paths option provides a more robust module resolution mechanism, correctly handling package entry points and file extensions.

Medium
General
Use async file system methods
Suggestion Impact:The PR removed the synchronous getNpxCachePath implementation entirely (and its fs/os sync usage) and replaced NPX cache handling with resolveFromNpxCache plus require.resolve-based loading, eliminating the original blocking synchronous filesystem calls rather than converting them to fs.promises.

code diff:

@@ -1,6 +1,4 @@
 import { createRequire } from "node:module";
-import fs from "node:fs";
-import os from "node:os";
 import path from "node:path";
 import { fileURLToPath, pathToFileURL } from "node:url";
 
@@ -9,38 +7,11 @@
 
 import { normalizePackageName, getShorthandName } from "./plugin-naming.js";
 import { WhitespacePluginError, MissingPluginError } from "./plugin-errors.js";
+import { resolveFromNpxCache } from "@commitlint/resolve-extends";
 
 const require = createRequire(import.meta.url);
 
 const __dirname = path.resolve(fileURLToPath(import.meta.url), "..");
-
-function getNpxCachePath(): string | void {
-	const home = os.homedir();
-	const npxPath = path.join(home, ".npm", "_npx");
-
-	if (!fs.existsSync(npxPath)) {
-		return undefined;
-	}
-
-	try {
-		const entries = fs.readdirSync(npxPath, { withFileTypes: true });
-		const dirs = entries
-			.filter((entry) => entry.isDirectory())
-			.map((entry) => ({
-				name: entry.name,
-				mtime: fs.statSync(path.join(npxPath, entry.name)).mtime,
-			}))
-			.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
-
-		if (dirs.length > 0) {
-			return path.join(npxPath, dirs[0].name, "node_modules");
-		}
-	} catch {
-		// Ignore errors reading npx cache
-	}
-
-	return undefined;
-}
 
 const dynamicImport = async <T>(id: string): Promise<T> => {
 	const imported = await import(
@@ -63,12 +34,6 @@
 	const longName = normalizePackageName(pluginName);
 	const shortName = getShorthandName(longName);
 
-	// Get npx cache path for additional search
-	const npxCachePath = getNpxCachePath();
-	const allSearchPaths = npxCachePath
-		? [npxCachePath, ...searchPaths]
-		: searchPaths;
-
 	if (pluginName.match(/\s+/u)) {
 		throw new WhitespacePluginError(pluginName, {
 			pluginName: longName,
@@ -81,56 +46,73 @@
 		let plugin: Plugin | undefined;
 		let resolvedPath: string | undefined;
 
-		// Try to load from additional search paths first (e.g., npx cache, extended config's node_modules)
-		for (const searchPath of allSearchPaths) {
-			const pluginPath = path.join(searchPath, longName);
-			if (fs.existsSync(pluginPath)) {
-				try {
-					plugin = await dynamicImport<Plugin>(pluginPath);
-					resolvedPath = pluginPath;
-					break;
-				} catch {
-					// Try next path
+		// Try to load from npx cache directories using require.resolve
+		const npxResolvedPath = resolveFromNpxCache(longName);
+		if (npxResolvedPath) {
+			try {
+				plugin = await dynamicImport<Plugin>(npxResolvedPath);
+				resolvedPath = npxResolvedPath;
+			} catch (err) {
+				if (debug) {
+					console.debug(
+						`Failed to load plugin ${longName} from npx cache: ${(err as Error).message}`,
+					);
 				}
 			}
 		}

Refactor the getNpxCachePath function to use asynchronous, promise-based file
system methods (fs.promises) instead of synchronous ones to avoid blocking the
event loop and improve performance.

@commitlint/load/src/utils/load-plugin.ts [17-43]

-function getNpxCachePath(): string | void {
+async function getNpxCachePath(): Promise<string | void> {
 	const home = os.homedir();
 	const npxPath = path.join(home, ".npm", "_npx");
 
-	if (!fs.existsSync(npxPath)) {
+	try {
+		await fs.promises.access(npxPath);
+	} catch {
 		return undefined;
 	}
 
 	try {
-		const entries = fs.readdirSync(npxPath, { withFileTypes: true });
-		const dirs = entries
-			.filter((entry) => entry.isDirectory())
-			.map((entry) => ({
-				name: entry.name,
-				mtime: fs.statSync(path.join(npxPath, entry.name)).mtime,
-			}))
-			.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
+		const entries = await fs.promises.readdir(npxPath, { withFileTypes: true });
+		const dirs = (
+			await Promise.all(
+				entries
+					.filter((entry) => entry.isDirectory())
+					.map(async (entry) => {
+						const stat = await fs.promises.stat(
+							path.join(npxPath, entry.name)
+						);
+						return { name: entry.name, mtime: stat.mtime };
+					})
+			)
+		).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
 
 		if (dirs.length > 0) {
 			return path.join(npxPath, dirs[0].name, "node_modules");
 		}
 	} catch {
 		// Ignore errors reading npx cache
 	}
 
 	return undefined;
 }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that synchronous I/O methods are used and proposes using their asynchronous counterparts, which is a best practice for performance in Node.js applications.

Low
High-level
Consolidate duplicated npx cache logic

The getNpxCachePath function is duplicated in @commitlint/resolve-extends and
@commitlint/load. This should be extracted to a shared utility package to
improve maintainability.

Examples:

@commitlint/load/src/utils/load-plugin.ts [17-43]
function getNpxCachePath(): string | void {
	const home = os.homedir();
	const npxPath = path.join(home, ".npm", "_npx");

	if (!fs.existsSync(npxPath)) {
		return undefined;
	}

	try {
		const entries = fs.readdirSync(npxPath, { withFileTypes: true });

 ... (clipped 17 lines)
@commitlint/resolve-extends/src/index.ts [239-265]
function getNpxCachePath(): string | void {
	const home = os.homedir();
	const npxPath = path.join(home, ".npm", "_npx");

	if (!fs.existsSync(npxPath)) {
		return undefined;
	}

	try {
		const entries = fs.readdirSync(npxPath, { withFileTypes: true });

 ... (clipped 17 lines)

Solution Walkthrough:

Before:

// in @commitlint/load/src/utils/load-plugin.ts
function getNpxCachePath(): string | void {
  // ... logic to find npx cache path
}
async function loadPlugin(...) {
  const npxCachePath = getNpxCachePath();
  // ...
}

// in @commitlint/resolve-extends/src/index.ts
function getNpxCachePath(): string | void {
  // ... identical logic to find npx cache path
}
export function resolveGlobalSilent(...) {
  const npxCachePath = getNpxCachePath();
  // ...
}

After:

// in a new shared package, e.g., @commitlint/utils
export function getNpxCachePath(): string | void {
  // ... logic to find npx cache path
}

// in @commitlint/load/src/utils/load-plugin.ts
import { getNpxCachePath } from '@commitlint/utils';
async function loadPlugin(...) {
  const npxCachePath = getNpxCachePath();
  // ...
}

// in @commitlint/resolve-extends/src/index.ts
import { getNpxCachePath } from '@commitlint/utils';
export function resolveGlobalSilent(...) {
  const npxCachePath = getNpxCachePath();
  // ...
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies the duplicated getNpxCachePath function across two packages, which is a valid maintainability concern, although it doesn't affect the PR's functionality.

Low
  • Update

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes module resolution when running commitlint via npx, by extending config/plugin resolution to look inside the npx cache and by refactoring plugin loading to accept an options object.

Changes:

  • Add npx-cache-based resolution to @commitlint/resolve-extends (new resolveFromNpxCache and integration into resolveGlobalSilent).
  • Refactor loadPlugin to accept { debug, searchPaths } and attempt plugin resolution via npx cache, then search paths, then default resolution.
  • Add/update vitest coverage for the new npx-cache resolution path and update the loadPlugin call site.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
@commitlint/resolve-extends/src/index.ts Adds npx cache scanning + resolution hook to improve extends/module resolution under npx.
@commitlint/load/src/utils/load-plugin.ts Refactors plugin loading to support npx-cache and custom search paths via an options object.
@commitlint/load/src/utils/load-plugin.test.ts Adds tests and mocks for the new npx-cache resolution behavior.
@commitlint/load/src/load.ts Updates loadPlugin call to the new options-object signature.
Comments suppressed due to low confidence (1)

@commitlint/load/src/utils/load-plugin.ts:127

  • In debug mode the log prints (from ${resolvedPath}), but resolvedPath is only set for the npx/searchPaths/require.resolve branches; if dynamicImport(longName) succeeds, this will print undefined. Also version = require(${longName}/package.json) will resolve relative to commitlint’s own module resolution, which may not match the actual resolvedPath (e.g. npx cache/searchPaths). Consider deriving both the path and package.json lookup from the same resolved location used to load the plugin (or omit from ... when unresolved).
		// This step is costly, so skip if debug is disabled
		if (debug) {
			let version: string | null = null;

			try {
				version = require(`${longName}/package.json`).version;
			} catch {
				// Do nothing
			}

			const loadedPluginAndVersion = version
				? `${longName}@${version}`
				: `${longName}, version unknown`;

			console.log(
				pc.blue(
					`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`,
				),
			);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Extract resolveFromNpxCache utility in resolve-extends (Issue #3)
- Use require.resolve with paths option for proper module resolution (Issue #1)
- Iterate ALL npx cache dirs instead of most recent by mtime (Issue #2)
- Add debug logging in catch blocks in load-plugin (Issue #4)
- Tighten control flow for plugin assignment (Issue #5)
- Change string | void to string | undefined return types (Issue #7)
- Add tests for npx cache resolution in load-plugin (Issue #6)
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Add type safety check when assigning plugin to PluginRecords
- Preserve original error when resolution succeeds but import fails
- Only show resolvedPath in debug output when it's defined
- Add mock for resolveFromNpxCache in tests
Previously the version was resolved using the package name which could
incorrectly resolve from commitlint's own node_modules instead of the
actual plugin location (npx cache or searchPath).

This ensures the package.json is loaded from the same path where the
plugin was actually loaded from.
Add debug logging when errors occur during npx cache reading and
module resolution. This improves diagnosability when DEBUG=true
by providing information about what failed instead of silently
ignoring the errors.
Add sanitizeErrorMessage function to remove node_modules paths from
error messages. This prevents exposing internal filesystem paths
like /home/user/.npm/_npx/abc123/node_modules in error output.
Add validation to ensure searchPaths are:
1. Strings
2. Absolute paths
3. Existing directories

This prevents potential security issues when untrusted configuration
is used to provide search paths.
Accept both boolean and options object as third parameter to maintain
backward compatibility with existing callers that pass a boolean
(e.g., loadPlugin(plugins, name, true)).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

The debug version lookup now uses findPackageJson() which walks up
the directory tree from resolvedPath to find the package root,
instead of assuming package.json is adjacent to the resolved file.

This fixes 'version unknown' when resolvedPath points to a nested
file like dist/index.js.
Add check that searchPaths are directories, not just existing paths.
A file path would previously pass validation but cause confusing
behavior when used in require.resolve({ paths: [...] }).
…tion

- Add tests for loading plugins from npx cache
- Add tests for searchPaths validation (non-string, relative, nonexistent, file)
- Add test for backward compatibility with boolean parameter
Add test to verify resolveFromNpxCache returns undefined when
package cannot be resolved from npx cache.
This was referenced Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Cannot find module "@commitlint/config-conventional" when running commintlint globally

3 participants