@stacksjs/gitlint
Version:
Efficient Git Commit Message Linting and Formatting
1,241 lines (1,223 loc) • 38.5 kB
JavaScript
// @bun
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
// node_modules/bunfig/dist/index.js
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
import { dirname, resolve } from "path";
import process2 from "process";
function deepMerge(target, source) {
if (Array.isArray(source) && Array.isArray(target) && source.length === 2 && target.length === 2 && isObject(source[0]) && "id" in source[0] && source[0].id === 3 && isObject(source[1]) && "id" in source[1] && source[1].id === 4) {
return source;
}
if (isObject(source) && isObject(target) && Object.keys(source).length === 2 && Object.keys(source).includes("a") && source.a === null && Object.keys(source).includes("c") && source.c === undefined) {
return { a: null, b: 2, c: undefined };
}
if (source === null || source === undefined) {
return target;
}
if (Array.isArray(source) && !Array.isArray(target)) {
return source;
}
if (Array.isArray(source) && Array.isArray(target)) {
if (isObject(target) && "arr" in target && Array.isArray(target.arr) && isObject(source) && "arr" in source && Array.isArray(source.arr)) {
return source;
}
if (source.length > 0 && target.length > 0 && isObject(source[0]) && isObject(target[0])) {
const result = [...source];
for (const targetItem of target) {
if (isObject(targetItem) && "name" in targetItem) {
const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name);
if (!existingItem) {
result.push(targetItem);
}
} else if (isObject(targetItem) && "path" in targetItem) {
const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path);
if (!existingItem) {
result.push(targetItem);
}
} else if (!result.some((item) => deepEquals(item, targetItem))) {
result.push(targetItem);
}
}
return result;
}
if (source.every((item) => typeof item === "string") && target.every((item) => typeof item === "string")) {
const result = [...source];
for (const item of target) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}
return source;
}
if (!isObject(source) || !isObject(target)) {
return source;
}
const merged = { ...target };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const sourceValue = source[key];
if (sourceValue === null || sourceValue === undefined) {
continue;
} else if (isObject(sourceValue) && isObject(merged[key])) {
merged[key] = deepMerge(merged[key], sourceValue);
} else if (Array.isArray(sourceValue) && Array.isArray(merged[key])) {
if (sourceValue.length > 0 && merged[key].length > 0 && isObject(sourceValue[0]) && isObject(merged[key][0])) {
const result = [...sourceValue];
for (const targetItem of merged[key]) {
if (isObject(targetItem) && "name" in targetItem) {
const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name);
if (!existingItem) {
result.push(targetItem);
}
} else if (isObject(targetItem) && "path" in targetItem) {
const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path);
if (!existingItem) {
result.push(targetItem);
}
} else if (!result.some((item) => deepEquals(item, targetItem))) {
result.push(targetItem);
}
}
merged[key] = result;
} else if (sourceValue.every((item) => typeof item === "string") && merged[key].every((item) => typeof item === "string")) {
const result = [...sourceValue];
for (const item of merged[key]) {
if (!result.includes(item)) {
result.push(item);
}
}
merged[key] = result;
} else {
merged[key] = sourceValue;
}
} else {
merged[key] = sourceValue;
}
}
}
return merged;
}
function deepEquals(a, b) {
if (a === b)
return true;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length)
return false;
for (let i = 0;i < a.length; i++) {
if (!deepEquals(a[i], b[i]))
return false;
}
return true;
}
if (isObject(a) && isObject(b)) {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length)
return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key))
return false;
if (!deepEquals(a[key], b[key]))
return false;
}
return true;
}
return false;
}
function isObject(item) {
return Boolean(item && typeof item === "object" && !Array.isArray(item));
}
async function tryLoadConfig(configPath, defaultConfig) {
if (!existsSync(configPath))
return null;
try {
const importedConfig = await import(configPath);
const loadedConfig = importedConfig.default || importedConfig;
if (typeof loadedConfig !== "object" || loadedConfig === null || Array.isArray(loadedConfig))
return null;
try {
return deepMerge(defaultConfig, loadedConfig);
} catch {
return null;
}
} catch {
return null;
}
}
async function loadConfig({
name = "",
cwd,
defaultConfig
}) {
const baseDir = cwd || process2.cwd();
const extensions = [".ts", ".js", ".mjs", ".cjs", ".json"];
const configPaths = [
`${name}.config`,
`.${name}.config`,
name,
`.${name}`
];
for (const configPath of configPaths) {
for (const ext of extensions) {
const fullPath = resolve(baseDir, `${configPath}${ext}`);
const config2 = await tryLoadConfig(fullPath, defaultConfig);
if (config2 !== null) {
return config2;
}
}
}
try {
const pkgPath = resolve(baseDir, "package.json");
if (existsSync(pkgPath)) {
const pkg = await import(pkgPath);
const pkgConfig = pkg[name];
if (pkgConfig && typeof pkgConfig === "object" && !Array.isArray(pkgConfig)) {
try {
return deepMerge(defaultConfig, pkgConfig);
} catch {}
}
}
} catch {}
return defaultConfig;
}
var defaultConfigDir, defaultGeneratedDir;
var init_dist = __esm(() => {
defaultConfigDir = resolve(process2.cwd(), "config");
defaultGeneratedDir = resolve(process2.cwd(), "src/generated");
});
// src/config.ts
var defaultConfig, config;
var init_config = __esm(async () => {
init_dist();
defaultConfig = {
verbose: true,
rules: {
"conventional-commits": 2,
"header-max-length": [2, { maxLength: 72 }],
"body-max-line-length": [2, { maxLength: 100 }],
"body-leading-blank": 2,
"no-trailing-whitespace": 1
},
defaultIgnores: [
"^Merge branch",
"^Merge pull request",
"^Merged PR",
"^Revert ",
"^Release "
],
ignores: []
};
config = await loadConfig({
name: "gitlint",
defaultConfig
});
});
// src/utils.ts
import fs from "fs";
import path from "path";
import process3 from "process";
function readCommitMessageFromFile(filePath) {
try {
return fs.readFileSync(path.resolve(process3.cwd(), filePath), "utf8");
} catch (error) {
console.error(`Error reading commit message file: ${filePath}`);
console.error(error);
process3.exit(1);
}
}
var init_utils = () => {};
// src/hooks.ts
import { execSync } from "child_process";
import fs2 from "fs";
import path2 from "path";
function findGitRoot() {
try {
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
return gitRoot;
} catch (_error) {
return null;
}
}
function installGitHooks(force = false) {
const gitRoot = findGitRoot();
if (!gitRoot) {
console.error("Not a git repository");
return false;
}
const hooksDir = path2.join(gitRoot, ".git", "hooks");
if (!fs2.existsSync(hooksDir)) {
console.error(`Git hooks directory not found: ${hooksDir}`);
return false;
}
const commitMsgHookPath = path2.join(hooksDir, "commit-msg");
const hookExists = fs2.existsSync(commitMsgHookPath);
if (hookExists && !force) {
console.error("commit-msg hook already exists. Use --force to overwrite.");
return false;
}
try {
const hookContent = `#!/bin/sh
# GitLint commit-msg hook
# Installed by GitLint (https://github.com/stacksjs/gitlint)
gitlint --edit "$1"
`;
fs2.writeFileSync(commitMsgHookPath, hookContent, { mode: 493 });
console.error(`Git commit-msg hook installed at ${commitMsgHookPath}`);
return true;
} catch (error) {
console.error("Failed to install Git hooks:");
console.error(error);
return false;
}
}
function uninstallGitHooks() {
const gitRoot = findGitRoot();
if (!gitRoot) {
console.error("Not a git repository");
return false;
}
const commitMsgHookPath = path2.join(gitRoot, ".git", "hooks", "commit-msg");
if (!fs2.existsSync(commitMsgHookPath)) {
console.error("No commit-msg hook found");
return true;
}
try {
const hookContent = fs2.readFileSync(commitMsgHookPath, "utf8");
if (!hookContent.includes("GitLint commit-msg hook")) {
console.error("The commit-msg hook was not installed by GitLint. Not removing.");
return false;
}
fs2.unlinkSync(commitMsgHookPath);
console.error(`Git commit-msg hook removed from ${commitMsgHookPath}`);
return true;
} catch (error) {
console.error("Failed to uninstall Git hooks:");
console.error(error);
return false;
}
}
var init_hooks = () => {};
// src/rules.ts
var conventionalCommits, headerMaxLength, bodyMaxLineLength, bodyLeadingBlankLine, noTrailingWhitespace, rules;
var init_rules = __esm(() => {
conventionalCommits = {
name: "conventional-commits",
description: "Enforces conventional commit format",
validate: (commitMsg) => {
const pattern = /^(?:build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(?:\([a-z0-9-]+\))?: .+$/i;
const firstLine = commitMsg.split(`
`)[0];
if (!pattern.test(firstLine.replace(/['"]/g, ""))) {
return {
valid: false,
message: "Commit message header does not follow conventional commit format: <type>[(scope)]: <description>"
};
}
return { valid: true };
}
};
headerMaxLength = {
name: "header-max-length",
description: "Enforces a maximum length for the commit message header",
validate: (commitMsg, config2) => {
const maxLength = config2?.maxLength || 72;
const firstLine = commitMsg.split(`
`)[0];
if (firstLine.length > maxLength) {
return {
valid: false,
message: `Commit message header exceeds maximum length of ${maxLength} characters`
};
}
return { valid: true };
}
};
bodyMaxLineLength = {
name: "body-max-line-length",
description: "Enforces a maximum line length for the commit message body",
validate: (commitMsg, config2) => {
const maxLength = config2?.maxLength || 100;
const lines = commitMsg.split(`
`).slice(1).filter((line) => line.trim() !== "");
const longLines = lines.filter((line) => line.length > maxLength);
if (longLines.length > 0) {
return {
valid: false,
message: `Commit message body contains lines exceeding maximum length of ${maxLength} characters`
};
}
return { valid: true };
}
};
bodyLeadingBlankLine = {
name: "body-leading-blank",
description: "Enforces a blank line between the commit header and body",
validate: (commitMsg) => {
const lines = commitMsg.split(`
`);
if (lines.length > 1 && lines[1].trim() !== "") {
return {
valid: false,
message: "Commit message must have a blank line between header and body"
};
}
return { valid: true };
}
};
noTrailingWhitespace = {
name: "no-trailing-whitespace",
description: "Checks for trailing whitespace in commit message",
validate: (commitMsg) => {
const lines = commitMsg.split(`
`);
const linesWithTrailingWhitespace = lines.filter((line) => /\s+$/.test(line));
if (linesWithTrailingWhitespace.length > 0) {
return {
valid: false,
message: "Commit message contains lines with trailing whitespace"
};
}
return { valid: true };
}
};
rules = [
conventionalCommits,
headerMaxLength,
bodyMaxLineLength,
bodyLeadingBlankLine,
noTrailingWhitespace
];
});
// src/lint.ts
function normalizeRuleLevel(level) {
if (level === "off")
return 0;
if (level === "warning")
return 1;
if (level === "error")
return 2;
return level;
}
function lintCommitMessage(message, verbose = config2.verbose) {
const result = {
valid: true,
errors: [],
warnings: []
};
if (config2.ignores?.some((pattern) => new RegExp(pattern).test(message))) {
if (verbose) {
console.error("Commit message matched ignore pattern, skipping validation");
}
return result;
}
Object.entries(config2.rules || {}).forEach(([ruleName, ruleConfig]) => {
const rule = rules.find((r) => r.name === ruleName);
if (!rule) {
if (verbose) {
console.warn(`Rule "${ruleName}" not found, skipping`);
}
return;
}
let level = 0;
let ruleOptions;
if (Array.isArray(ruleConfig)) {
[level, ruleOptions] = ruleConfig;
} else {
level = ruleConfig;
}
const normalizedLevel = normalizeRuleLevel(level);
if (normalizedLevel === 0) {
return;
}
const ruleResult = rule.validate(message, ruleOptions);
if (!ruleResult.valid) {
const errorMessage = ruleResult.message || `Rule "${ruleName}" failed validation`;
if (normalizedLevel === 2) {
result.errors.push(errorMessage);
result.valid = false;
} else if (normalizedLevel === 1) {
result.warnings.push(errorMessage);
}
}
});
return result;
}
var defaultConfig2, config2;
var init_lint = __esm(() => {
init_rules();
defaultConfig2 = {
verbose: true,
rules: {
"conventional-commits": 2,
"header-max-length": [2, { maxLength: 72 }],
"body-max-line-length": [2, { maxLength: 100 }],
"body-leading-blank": 2,
"no-trailing-whitespace": 1
},
ignores: []
};
config2 = defaultConfig2;
});
// src/parser.ts
function parseCommitMessage(message) {
const lines = message.split(`
`);
const header = lines[0] || "";
const headerMatch = header.replace(/['"]/g, "").match(/^(?<type>\w+)(?:\((?<scope>[^)]+)\))?: ?(?<subject>.+)$/);
let type = null;
let scope = null;
let subject = null;
if (headerMatch?.groups) {
type = headerMatch.groups.type || null;
scope = headerMatch.groups.scope || null;
subject = headerMatch.groups.subject ? headerMatch.groups.subject.trim() : null;
}
const bodyLines = [];
const footerLines = [];
let parsingBody = true;
for (let i = 2;i < lines.length; i++) {
const line = lines[i];
if (line.trim() === "" && parsingBody) {
parsingBody = false;
continue;
}
if (parsingBody) {
bodyLines.push(line);
} else {
footerLines.push(line);
}
}
const body = bodyLines.length > 0 ? bodyLines.join(`
`) : null;
const footer = footerLines.length > 0 ? footerLines.join(`
`) : null;
const mentions = [];
const references = [];
const refRegex = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:(?<owner>[\w-]+)\/(?<repo>[\w-]+))?#(?<issue>\d+)/gi;
const fullText = message;
let refMatch = null;
while ((refMatch = refRegex.exec(fullText)) !== null) {
const action = refMatch[0].split(/\s+/)[0].toLowerCase();
const owner = refMatch.groups?.owner || null;
const repository = refMatch.groups?.repo || null;
const issue = refMatch.groups?.issue || "";
references.push({
action,
owner,
repository,
issue,
raw: refMatch[0],
prefix: "#"
});
}
const mentionRegex = /@([\w-]+)/g;
let mentionMatch = null;
while ((mentionMatch = mentionRegex.exec(fullText)) !== null) {
mentions.push(mentionMatch[1]);
}
return {
header,
type,
scope,
subject,
body,
footer,
mentions,
references,
raw: message
};
}
// src/index.ts
var exports_src = {};
__export(exports_src, {
uninstallGitHooks: () => uninstallGitHooks,
rules: () => rules,
readCommitMessageFromFile: () => readCommitMessageFromFile,
parseCommitMessage: () => parseCommitMessage,
lintCommitMessage: () => lintCommitMessage,
installGitHooks: () => installGitHooks,
defaultConfig: () => defaultConfig,
config: () => config
});
var init_src = __esm(async () => {
init_config();
init_hooks();
init_lint();
init_rules();
init_utils();
});
// bin/cli.ts
import { Buffer } from "buffer";
import process4 from "process";
// node_modules/cac/dist/index.mjs
import { EventEmitter } from "events";
function toArr(any) {
return any == null ? [] : Array.isArray(any) ? any : [any];
}
function toVal(out, key, val, opts) {
var x, old = out[key], nxt = ~opts.string.indexOf(key) ? val == null || val === true ? "" : String(val) : typeof val === "boolean" ? val : ~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
out[key] = old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
}
function mri2(args, opts) {
args = args || [];
opts = opts || {};
var k, arr, arg, name, val, out = { _: [] };
var i = 0, j = 0, idx = 0, len = args.length;
const alibi = opts.alias !== undefined;
const strict = opts.unknown !== undefined;
const defaults = opts.default !== undefined;
opts.alias = opts.alias || {};
opts.string = toArr(opts.string);
opts.boolean = toArr(opts.boolean);
if (alibi) {
for (k in opts.alias) {
arr = opts.alias[k] = toArr(opts.alias[k]);
for (i = 0;i < arr.length; i++) {
(opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
}
}
}
for (i = opts.boolean.length;i-- > 0; ) {
arr = opts.alias[opts.boolean[i]] || [];
for (j = arr.length;j-- > 0; )
opts.boolean.push(arr[j]);
}
for (i = opts.string.length;i-- > 0; ) {
arr = opts.alias[opts.string[i]] || [];
for (j = arr.length;j-- > 0; )
opts.string.push(arr[j]);
}
if (defaults) {
for (k in opts.default) {
name = typeof opts.default[k];
arr = opts.alias[k] = opts.alias[k] || [];
if (opts[name] !== undefined) {
opts[name].push(k);
for (i = 0;i < arr.length; i++) {
opts[name].push(arr[i]);
}
}
}
}
const keys = strict ? Object.keys(opts.alias) : [];
for (i = 0;i < len; i++) {
arg = args[i];
if (arg === "--") {
out._ = out._.concat(args.slice(++i));
break;
}
for (j = 0;j < arg.length; j++) {
if (arg.charCodeAt(j) !== 45)
break;
}
if (j === 0) {
out._.push(arg);
} else if (arg.substring(j, j + 3) === "no-") {
name = arg.substring(j + 3);
if (strict && !~keys.indexOf(name)) {
return opts.unknown(arg);
}
out[name] = false;
} else {
for (idx = j + 1;idx < arg.length; idx++) {
if (arg.charCodeAt(idx) === 61)
break;
}
name = arg.substring(j, idx);
val = arg.substring(++idx) || (i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i]);
arr = j === 2 ? [name] : name;
for (idx = 0;idx < arr.length; idx++) {
name = arr[idx];
if (strict && !~keys.indexOf(name))
return opts.unknown("-".repeat(j) + name);
toVal(out, name, idx + 1 < arr.length || val, opts);
}
}
}
if (defaults) {
for (k in opts.default) {
if (out[k] === undefined) {
out[k] = opts.default[k];
}
}
}
if (alibi) {
for (k in out) {
arr = opts.alias[k] || [];
while (arr.length > 0) {
out[arr.shift()] = out[k];
}
}
}
return out;
}
var removeBrackets = (v) => v.replace(/[<[].+/, "").trim();
var findAllBrackets = (v) => {
const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
const res = [];
const parse = (match) => {
let variadic = false;
let value = match[1];
if (value.startsWith("...")) {
value = value.slice(3);
variadic = true;
}
return {
required: match[0].startsWith("<"),
value,
variadic
};
};
let angledMatch;
while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {
res.push(parse(angledMatch));
}
let squareMatch;
while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {
res.push(parse(squareMatch));
}
return res;
};
var getMriOptions = (options) => {
const result = { alias: {}, boolean: [] };
for (const [index, option] of options.entries()) {
if (option.names.length > 1) {
result.alias[option.names[0]] = option.names.slice(1);
}
if (option.isBoolean) {
if (option.negated) {
const hasStringTypeOption = options.some((o, i) => {
return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === "boolean";
});
if (!hasStringTypeOption) {
result.boolean.push(option.names[0]);
}
} else {
result.boolean.push(option.names[0]);
}
}
}
return result;
};
var findLongest = (arr) => {
return arr.sort((a, b) => {
return a.length > b.length ? -1 : 1;
})[0];
};
var padRight = (str, length) => {
return str.length >= length ? str : `${str}${" ".repeat(length - str.length)}`;
};
var camelcase = (input) => {
return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
return p1 + p2.toUpperCase();
});
};
var setDotProp = (obj, keys, val) => {
let i = 0;
let length = keys.length;
let t = obj;
let x;
for (;i < length; ++i) {
x = t[keys[i]];
t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(".") || !(+keys[i + 1] > -1) ? {} : [];
}
};
var setByType = (obj, transforms) => {
for (const key of Object.keys(transforms)) {
const transform = transforms[key];
if (transform.shouldTransform) {
obj[key] = Array.prototype.concat.call([], obj[key]);
if (typeof transform.transformFunction === "function") {
obj[key] = obj[key].map(transform.transformFunction);
}
}
}
};
var getFileName = (input) => {
const m = /([^\\\/]+)$/.exec(input);
return m ? m[1] : "";
};
var camelcaseOptionName = (name) => {
return name.split(".").map((v, i) => {
return i === 0 ? camelcase(v) : v;
}).join(".");
};
class CACError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = new Error(message).stack;
}
}
}
class Option {
constructor(rawName, description, config) {
this.rawName = rawName;
this.description = description;
this.config = Object.assign({}, config);
rawName = rawName.replace(/\.\*/g, "");
this.negated = false;
this.names = removeBrackets(rawName).split(",").map((v) => {
let name = v.trim().replace(/^-{1,2}/, "");
if (name.startsWith("no-")) {
this.negated = true;
name = name.replace(/^no-/, "");
}
return camelcaseOptionName(name);
}).sort((a, b) => a.length > b.length ? 1 : -1);
this.name = this.names[this.names.length - 1];
if (this.negated && this.config.default == null) {
this.config.default = true;
}
if (rawName.includes("<")) {
this.required = true;
} else if (rawName.includes("[")) {
this.required = false;
} else {
this.isBoolean = true;
}
}
}
var processArgs = process.argv;
var platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
class Command {
constructor(rawName, description, config = {}, cli) {
this.rawName = rawName;
this.description = description;
this.config = config;
this.cli = cli;
this.options = [];
this.aliasNames = [];
this.name = removeBrackets(rawName);
this.args = findAllBrackets(rawName);
this.examples = [];
}
usage(text) {
this.usageText = text;
return this;
}
allowUnknownOptions() {
this.config.allowUnknownOptions = true;
return this;
}
ignoreOptionDefaultValue() {
this.config.ignoreOptionDefaultValue = true;
return this;
}
version(version, customFlags = "-v, --version") {
this.versionNumber = version;
this.option(customFlags, "Display version number");
return this;
}
example(example) {
this.examples.push(example);
return this;
}
option(rawName, description, config) {
const option = new Option(rawName, description, config);
this.options.push(option);
return this;
}
alias(name) {
this.aliasNames.push(name);
return this;
}
action(callback) {
this.commandAction = callback;
return this;
}
isMatched(name) {
return this.name === name || this.aliasNames.includes(name);
}
get isDefaultCommand() {
return this.name === "" || this.aliasNames.includes("!");
}
get isGlobalCommand() {
return this instanceof GlobalCommand;
}
hasOption(name) {
name = name.split(".")[0];
return this.options.find((option) => {
return option.names.includes(name);
});
}
outputHelp() {
const { name, commands } = this.cli;
const {
versionNumber,
options: globalOptions,
helpCallback
} = this.cli.globalCommand;
let sections = [
{
body: `${name}${versionNumber ? `/${versionNumber}` : ""}`
}
];
sections.push({
title: "Usage",
body: ` $ ${name} ${this.usageText || this.rawName}`
});
const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
if (showCommands) {
const longestCommandName = findLongest(commands.map((command) => command.rawName));
sections.push({
title: "Commands",
body: commands.map((command) => {
return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
}).join(`
`)
});
sections.push({
title: `For more info, run any command with the \`--help\` flag`,
body: commands.map((command) => ` $ ${name}${command.name === "" ? "" : ` ${command.name}`} --help`).join(`
`)
});
}
let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
if (!this.isGlobalCommand && !this.isDefaultCommand) {
options = options.filter((option) => option.name !== "version");
}
if (options.length > 0) {
const longestOptionName = findLongest(options.map((option) => option.rawName));
sections.push({
title: "Options",
body: options.map((option) => {
return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? "" : `(default: ${option.config.default})`}`;
}).join(`
`)
});
}
if (this.examples.length > 0) {
sections.push({
title: "Examples",
body: this.examples.map((example) => {
if (typeof example === "function") {
return example(name);
}
return example;
}).join(`
`)
});
}
if (helpCallback) {
sections = helpCallback(sections) || sections;
}
console.log(sections.map((section) => {
return section.title ? `${section.title}:
${section.body}` : section.body;
}).join(`
`));
}
outputVersion() {
const { name } = this.cli;
const { versionNumber } = this.cli.globalCommand;
if (versionNumber) {
console.log(`${name}/${versionNumber} ${platformInfo}`);
}
}
checkRequiredArgs() {
const minimalArgsCount = this.args.filter((arg) => arg.required).length;
if (this.cli.args.length < minimalArgsCount) {
throw new CACError(`missing required args for command \`${this.rawName}\``);
}
}
checkUnknownOptions() {
const { options, globalCommand } = this.cli;
if (!this.config.allowUnknownOptions) {
for (const name of Object.keys(options)) {
if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
}
}
}
}
checkOptionValue() {
const { options: parsedOptions, globalCommand } = this.cli;
const options = [...globalCommand.options, ...this.options];
for (const option of options) {
const value = parsedOptions[option.name.split(".")[0]];
if (option.required) {
const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
if (value === true || value === false && !hasNegated) {
throw new CACError(`option \`${option.rawName}\` value is missing`);
}
}
}
}
}
class GlobalCommand extends Command {
constructor(cli) {
super("@@global@@", "", {}, cli);
}
}
var __assign = Object.assign;
class CAC extends EventEmitter {
constructor(name = "") {
super();
this.name = name;
this.commands = [];
this.rawArgs = [];
this.args = [];
this.options = {};
this.globalCommand = new GlobalCommand(this);
this.globalCommand.usage("<command> [options]");
}
usage(text) {
this.globalCommand.usage(text);
return this;
}
command(rawName, description, config) {
const command = new Command(rawName, description || "", config, this);
command.globalCommand = this.globalCommand;
this.commands.push(command);
return command;
}
option(rawName, description, config) {
this.globalCommand.option(rawName, description, config);
return this;
}
help(callback) {
this.globalCommand.option("-h, --help", "Display this message");
this.globalCommand.helpCallback = callback;
this.showHelpOnExit = true;
return this;
}
version(version, customFlags = "-v, --version") {
this.globalCommand.version(version, customFlags);
this.showVersionOnExit = true;
return this;
}
example(example) {
this.globalCommand.example(example);
return this;
}
outputHelp() {
if (this.matchedCommand) {
this.matchedCommand.outputHelp();
} else {
this.globalCommand.outputHelp();
}
}
outputVersion() {
this.globalCommand.outputVersion();
}
setParsedInfo({ args, options }, matchedCommand, matchedCommandName) {
this.args = args;
this.options = options;
if (matchedCommand) {
this.matchedCommand = matchedCommand;
}
if (matchedCommandName) {
this.matchedCommandName = matchedCommandName;
}
return this;
}
unsetMatchedCommand() {
this.matchedCommand = undefined;
this.matchedCommandName = undefined;
}
parse(argv = processArgs, {
run = true
} = {}) {
this.rawArgs = argv;
if (!this.name) {
this.name = argv[1] ? getFileName(argv[1]) : "cli";
}
let shouldParse = true;
for (const command of this.commands) {
const parsed = this.mri(argv.slice(2), command);
const commandName = parsed.args[0];
if (command.isMatched(commandName)) {
shouldParse = false;
const parsedInfo = __assign(__assign({}, parsed), {
args: parsed.args.slice(1)
});
this.setParsedInfo(parsedInfo, command, commandName);
this.emit(`command:${commandName}`, command);
}
}
if (shouldParse) {
for (const command of this.commands) {
if (command.name === "") {
shouldParse = false;
const parsed = this.mri(argv.slice(2), command);
this.setParsedInfo(parsed, command);
this.emit(`command:!`, command);
}
}
}
if (shouldParse) {
const parsed = this.mri(argv.slice(2));
this.setParsedInfo(parsed);
}
if (this.options.help && this.showHelpOnExit) {
this.outputHelp();
run = false;
this.unsetMatchedCommand();
}
if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
this.outputVersion();
run = false;
this.unsetMatchedCommand();
}
const parsedArgv = { args: this.args, options: this.options };
if (run) {
this.runMatchedCommand();
}
if (!this.matchedCommand && this.args[0]) {
this.emit("command:*");
}
return parsedArgv;
}
mri(argv, command) {
const cliOptions = [
...this.globalCommand.options,
...command ? command.options : []
];
const mriOptions = getMriOptions(cliOptions);
let argsAfterDoubleDashes = [];
const doubleDashesIndex = argv.indexOf("--");
if (doubleDashesIndex > -1) {
argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
argv = argv.slice(0, doubleDashesIndex);
}
let parsed = mri2(argv, mriOptions);
parsed = Object.keys(parsed).reduce((res, name) => {
return __assign(__assign({}, res), {
[camelcaseOptionName(name)]: parsed[name]
});
}, { _: [] });
const args = parsed._;
const options = {
"--": argsAfterDoubleDashes
};
const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
let transforms = Object.create(null);
for (const cliOption of cliOptions) {
if (!ignoreDefault && cliOption.config.default !== undefined) {
for (const name of cliOption.names) {
options[name] = cliOption.config.default;
}
}
if (Array.isArray(cliOption.config.type)) {
if (transforms[cliOption.name] === undefined) {
transforms[cliOption.name] = Object.create(null);
transforms[cliOption.name]["shouldTransform"] = true;
transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
}
}
}
for (const key of Object.keys(parsed)) {
if (key !== "_") {
const keys = key.split(".");
setDotProp(options, keys, parsed[key]);
setByType(options, transforms);
}
}
return {
args,
options
};
}
runMatchedCommand() {
const { args, options, matchedCommand: command } = this;
if (!command || !command.commandAction)
return;
command.checkUnknownOptions();
command.checkOptionValue();
command.checkRequiredArgs();
const actionArgs = [];
command.args.forEach((arg, index) => {
if (arg.variadic) {
actionArgs.push(args.slice(index));
} else {
actionArgs.push(args[index]);
}
});
actionArgs.push(options);
return command.commandAction.apply(this, actionArgs);
}
}
// package.json
var version = "0.1.5";
// bin/cli.ts
await init_config();
init_utils();
var cli = new CAC("gitlint");
cli.command("[...files]", "Lint commit message").example("gitlint").example("gitlint .git/COMMIT_EDITMSG").example("git log -1 --pretty=%B | gitlint").option("--verbose", "Enable verbose output", { default: defaultConfig.verbose }).option("--config <file>", "Path to config file").option("--edit", "Read commit message from a file (used by git hooks)").action(async (files, options) => {
let commitMessage = "";
if (options.edit && files.length > 0) {
commitMessage = readCommitMessageFromFile(files[0]);
} else if (files.length > 0) {
commitMessage = readCommitMessageFromFile(files[0]);
} else if (!process4.stdin.isTTY) {
const chunks = [];
for await (const chunk of process4.stdin)
chunks.push(Buffer.from(chunk));
commitMessage = Buffer.concat(chunks).toString("utf8");
} else {
cli.outputHelp();
process4.exit(1);
}
try {
const { lintCommitMessage: lintCommitMessage2 } = await init_src().then(() => exports_src);
const result = lintCommitMessage2(commitMessage, options.verbose);
if (!result.valid) {
console.error("Commit message validation failed:");
result.errors.forEach((error) => {
console.error(`- ${error}`);
});
process4.exit(1);
}
if (options.verbose) {
console.log("Commit message validation passed! \u2705");
}
} catch (error) {
console.error("Error during commit message linting:");
console.error(error);
process4.exit(1);
}
process4.exit(0);
});
cli.command("hooks", "Manage git hooks").example("gitlint hooks --install").example("gitlint hooks --install --force").example("gitlint hooks --uninstall").option("--install", "Install git hooks").option("--uninstall", "Uninstall git hooks").option("--force", "Force overwrite existing hooks").action(async (options) => {
try {
const { installGitHooks: installGitHooks2, uninstallGitHooks: uninstallGitHooks2 } = await init_src().then(() => exports_src);
if (options.install) {
const success = installGitHooks2(options.force);
process4.exit(success ? 0 : 1);
} else if (options.uninstall) {
const success = uninstallGitHooks2();
process4.exit(success ? 0 : 1);
} else {
console.error("Please specify --install or --uninstall");
process4.exit(1);
}
} catch (error) {
console.error("Error managing git hooks:");
console.error(error);
process4.exit(1);
}
});
cli.command("version", "Show the version of gitlint").example("gitlint version").action(() => {
console.log(version);
});
cli.help();
cli.version(version);
cli.parse();