package-versioner
Version:
A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.
1,429 lines (1,375 loc) • 95.6 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
run: () => run
});
module.exports = __toCommonJS(index_exports);
var fs10 = __toESM(require("fs"), 1);
var import_node_path8 = __toESM(require("path"), 1);
var import_commander = require("commander");
// src/changelog/changelogRegenerator.ts
var import_node_child_process2 = require("child_process");
var import_node_fs = __toESM(require("fs"), 1);
var import_node_path = __toESM(require("path"), 1);
// src/utils/logging.ts
var import_chalk = __toESM(require("chalk"), 1);
var import_figlet = __toESM(require("figlet"), 1);
// src/utils/jsonOutput.ts
var _jsonOutputMode = false;
var _jsonData = {
dryRun: false,
updates: [],
tags: []
};
function enableJsonOutput(dryRun = false) {
_jsonOutputMode = true;
_jsonData.dryRun = dryRun;
_jsonData.updates = [];
_jsonData.tags = [];
_jsonData.commitMessage = void 0;
}
function isJsonOutputMode() {
return _jsonOutputMode;
}
function addPackageUpdate(packageName, newVersion, filePath) {
if (!_jsonOutputMode) return;
_jsonData.updates.push({
packageName,
newVersion,
filePath
});
}
function addTag(tag) {
if (!_jsonOutputMode) return;
_jsonData.tags.push(tag);
}
function setCommitMessage(message) {
if (!_jsonOutputMode) return;
_jsonData.commitMessage = message;
}
function printJsonOutput() {
if (_jsonOutputMode) {
console.log(JSON.stringify(_jsonData, null, 2));
}
}
// src/utils/logging.ts
function log(message, level = "info") {
const showDebug = process.env.DEBUG === "true" || process.env.DEBUG === "1";
if (level === "debug" && !showDebug) {
return;
}
let chalkFn;
switch (level) {
case "success":
chalkFn = import_chalk.default.green;
break;
case "warning":
chalkFn = import_chalk.default.yellow;
break;
case "error":
chalkFn = import_chalk.default.red;
break;
case "debug":
chalkFn = import_chalk.default.gray;
break;
default:
chalkFn = import_chalk.default.blue;
}
if (isJsonOutputMode()) {
if (level === "error") {
chalkFn(message);
console.error(message);
}
return;
}
const formattedMessage = level === "debug" ? `[DEBUG] ${message}` : message;
if (level === "error") {
console.error(chalkFn(formattedMessage));
} else {
console.log(chalkFn(formattedMessage));
}
}
// src/changelog/commitParser.ts
var import_node_child_process = require("child_process");
var CONVENTIONAL_COMMIT_REGEX = /^(\w+)(?:\(([^)]+)\))?(!)?: (.+)(?:\n\n([\s\S]*))?/;
var BREAKING_CHANGE_REGEX = /BREAKING CHANGE: ([\s\S]+?)(?:\n\n|$)/;
function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
try {
const command = `git log ${revisionRange} --pretty=format:"%B---COMMIT_DELIMITER---" --no-merges`;
const output = (0, import_node_child_process.execSync)(command, {
cwd: projectDir,
encoding: "utf8"
});
const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
return commits.map((commit) => parseCommitMessage(commit)).filter((entry) => entry !== null);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("ambiguous argument") && errorMessage.includes("unknown revision")) {
const tagName = revisionRange.split("..")[0] || revisionRange;
if (tagName.startsWith("v") && !tagName.includes("@")) {
log(
`Error: Tag "${tagName}" not found. If you're using package-specific tags (like "package-name@v1.0.0"), you may need to configure "tagTemplate" in your version.config.json to use: \${packageName}@\${prefix}\${version}`,
"error"
);
} else {
log(
`Error: Tag or revision "${tagName}" not found in the repository. Please check if this tag exists or if you need to fetch it from the remote.`,
"error"
);
}
} else {
log(`Error extracting commits: ${errorMessage}`, "error");
}
return [];
}
}
function parseCommitMessage(message) {
const trimmedMessage = message.trim();
const match = trimmedMessage.match(CONVENTIONAL_COMMIT_REGEX);
if (match) {
const [, type, scope, breakingMark, subject, body = ""] = match;
const breakingFromMark = breakingMark === "!";
const breakingChangeMatch = body.match(BREAKING_CHANGE_REGEX);
const hasBreakingChange = breakingFromMark || breakingChangeMatch !== null;
const changelogType = mapCommitTypeToChangelogType(type);
if (!changelogType) {
return null;
}
const issueIds = extractIssueIds(body);
let description = subject;
if (hasBreakingChange) {
description = `**BREAKING** ${description}`;
}
return {
type: changelogType,
description,
scope: scope || void 0,
issueIds: issueIds.length > 0 ? issueIds : void 0,
originalType: type
// Store original type for custom formatting
};
}
if (!trimmedMessage.startsWith("Merge") && !trimmedMessage.match(/^v?\d+\.\d+\.\d+/)) {
const firstLine = trimmedMessage.split("\n")[0].trim();
return {
type: "changed",
description: firstLine
};
}
return null;
}
function mapCommitTypeToChangelogType(type) {
switch (type) {
case "feat":
return "added";
case "fix":
return "fixed";
case "docs":
case "style":
case "refactor":
case "perf":
case "build":
case "ci":
return "changed";
case "revert":
return "removed";
case "chore":
return "changed";
case "test":
return null;
default:
return "changed";
}
}
function extractIssueIds(body) {
const issueRegex = /(?:fix|fixes|close|closes|resolve|resolves)\s+#(\d+)/gi;
const issueIds = [];
let match = issueRegex.exec(body);
while (match !== null) {
issueIds.push(`#${match[1]}`);
match = issueRegex.exec(body);
}
return issueIds;
}
// src/changelog/formatters.ts
function formatChangelogEntries(format, version, date, entries, packageName, repoUrl) {
const formattingEntries = entries.map((entry) => {
const hasBreaking = entry.description.includes("**BREAKING**");
return {
...entry,
breaking: hasBreaking,
// Clean up the description to remove the **BREAKING** prefix since we'll handle it in formatting
description: hasBreaking ? entry.description.replace("**BREAKING** ", "") : entry.description
};
});
return format === "keep-a-changelog" ? formatKeepAChangelogEntries(version, date, formattingEntries, repoUrl) : formatAngularEntries(version, date, formattingEntries, packageName);
}
function formatKeepAChangelogEntries(version, date, entries, repoUrl) {
const added = [];
const changed = [];
const deprecated = [];
const removed = [];
const fixed = [];
const security = [];
for (const entry of entries) {
let entryText;
if (entry.breaking) {
entryText = entry.scope ? `- **BREAKING** **${entry.scope}**: ${entry.description}` : `- **BREAKING** ${entry.description}`;
} else {
entryText = entry.scope ? `- **${entry.scope}**: ${entry.description}` : `- ${entry.description}`;
}
const entryType = entry.originalType || entry.type;
switch (entryType) {
case "feat":
added.push(entryText);
break;
case "fix":
fixed.push(entryText);
break;
case "docs":
case "style":
case "refactor":
case "perf":
case "build":
case "ci":
changed.push(entryText);
break;
case "test":
break;
case "chore":
if (entry.description.toLowerCase().includes("deprecat")) {
deprecated.push(entryText);
} else {
changed.push(entryText);
}
break;
// Keep-a-changelog standard types
case "added":
added.push(entryText);
break;
case "changed":
changed.push(entryText);
break;
case "deprecated":
deprecated.push(entryText);
break;
case "removed":
removed.push(entryText);
break;
case "fixed":
fixed.push(entryText);
break;
case "security":
security.push(entryText);
break;
default:
changed.push(entryText);
}
}
let content = `## [${version}] - ${date}
`;
if (added.length > 0) {
content += `### Added
${added.join("\n")}
`;
}
if (changed.length > 0) {
content += `### Changed
${changed.join("\n")}
`;
}
if (deprecated.length > 0) {
content += `### Deprecated
${deprecated.join("\n")}
`;
}
if (removed.length > 0) {
content += `### Removed
${removed.join("\n")}
`;
}
if (fixed.length > 0) {
content += `### Fixed
${fixed.join("\n")}
`;
}
if (security.length > 0) {
content += `### Security
${security.join("\n")}
`;
}
if (repoUrl) {
content += `[${version}]: ${repoUrl}/compare/v${version}...HEAD
`;
}
return content.trim();
}
function formatAngularEntries(version, date, entries, packageName) {
const features = [];
const bugfixes = [];
const performance = [];
const breaking = [];
for (const entry of entries) {
if (entry.breaking) {
breaking.push(entry);
}
const entryType = entry.originalType || entry.type;
switch (entryType) {
case "feat":
case "added":
features.push(entry);
break;
case "fix":
case "fixed":
bugfixes.push(entry);
break;
case "perf":
performance.push(entry);
break;
}
}
let content = `## [${version}]${packageName ? ` (${packageName})` : ""} (${date})
`;
if (features.length > 0) {
content += "### Features\n\n";
content += formatAngularTypeEntries(features);
content += "\n";
}
if (bugfixes.length > 0) {
content += "### Bug Fixes\n\n";
content += formatAngularTypeEntries(bugfixes);
content += "\n";
}
if (performance.length > 0) {
content += "### Performance Improvements\n\n";
content += formatAngularTypeEntries(performance);
content += "\n";
}
if (breaking.length > 0) {
content += "### BREAKING CHANGES\n\n";
content += formatAngularTypeEntries(breaking);
content += "\n";
}
return content.trim();
}
function formatAngularTypeEntries(entries) {
var _a;
const entriesByScope = /* @__PURE__ */ new Map();
for (const entry of entries) {
const scope = entry.scope || "";
if (!entriesByScope.has(scope)) {
entriesByScope.set(scope, []);
}
(_a = entriesByScope.get(scope)) == null ? void 0 : _a.push(entry);
}
const result = [];
for (const [scope, scopeEntries] of Object.entries(groupEntriesByScope(entries))) {
if (scope !== "undefined" && scope !== "") {
result.push(`* **${scope}:**`);
for (const entry of scopeEntries) {
const description = entry.description.replace("**BREAKING** ", "");
result.push(` * ${description}`);
}
} else {
for (const entry of scopeEntries) {
const description = entry.description.replace("**BREAKING** ", "");
result.push(`* ${description}`);
}
}
}
return result.join("\n");
}
function groupEntriesByScope(entries) {
const result = {};
for (const entry of entries) {
const scope = entry.scope || "";
if (!result[scope]) {
result[scope] = [];
}
result[scope].push(entry);
}
return result;
}
// src/changelog/templates.ts
function getDefaultTemplate(format) {
return format === "keep-a-changelog" ? getKeepAChangelogTemplate() : getAngularTemplate();
}
function getKeepAChangelogTemplate() {
return `# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`;
}
function getAngularTemplate() {
return `# Changelog
`;
}
// src/changelog/changelogRegenerator.ts
function getAllVersionTags(since, versionPrefix = "v") {
try {
const command = `git tag --list "${versionPrefix}*" --sort=creatordate`;
const tagOutput = (0, import_node_child_process2.execSync)(command, { encoding: "utf8" }).trim();
if (!tagOutput) {
return [];
}
const allTags = tagOutput.split("\n").filter((tag) => !!tag);
let filteredTags = allTags;
if (since) {
const sinceIndex = allTags.findIndex((tag) => tag === since);
if (sinceIndex >= 0) {
filteredTags = allTags.slice(sinceIndex);
} else {
log(
`Warning: --since tag "${since}" not found in git history, including all tags`,
"warning"
);
}
}
return filteredTags.map((tag) => {
try {
const date = (0, import_node_child_process2.execSync)(`git log -1 --format=%ad --date=short ${tag}`, {
encoding: "utf8"
}).trim();
const version = tag.replace(new RegExp(`^${versionPrefix}`), "");
return { tag, version, date };
} catch (error) {
log(`Failed to get date for tag ${tag}: ${error}`, "warning");
return { tag, version: tag.replace(new RegExp(`^${versionPrefix}`), ""), date: "Unknown" };
}
});
} catch (error) {
log(`Failed to get version tags: ${error}`, "error");
return [];
}
}
async function regenerateChangelog(options) {
const { format, since, projectDir } = options;
const packageJsonPath = import_node_path.default.join(projectDir, "package.json");
let packageName = "";
let repoUrl = options.repoUrl;
if (import_node_fs.default.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(import_node_fs.default.readFileSync(packageJsonPath, "utf8"));
packageName = packageJson.name || "";
if (!repoUrl && packageJson.repository) {
if (typeof packageJson.repository === "string") {
repoUrl = packageJson.repository;
} else if (packageJson.repository.url) {
repoUrl = packageJson.repository.url;
}
if ((repoUrl == null ? void 0 : repoUrl.startsWith("git+")) && (repoUrl == null ? void 0 : repoUrl.endsWith(".git"))) {
repoUrl = repoUrl.substring(4, repoUrl.length - 4);
}
}
} catch (error) {
log(`Failed to read package.json: ${error}`, "warning");
}
}
let versionPrefix = "v";
try {
const allTags = (0, import_node_child_process2.execSync)("git tag --list", { encoding: "utf8" }).trim().split("\n");
const versionTag = allTags.find((tag) => /^[vV][0-9]/.test(tag));
if (versionTag) {
versionPrefix = versionTag.charAt(0);
}
} catch {
}
let tags = getAllVersionTags(since, versionPrefix);
if (!tags.length && since) {
tags = getAllVersionTags(void 0, versionPrefix);
}
if (!tags.length) {
throw new Error(
'No version tags found in git history. Make sure you have tags that start with the version prefix (usually "v").'
);
}
let changelogContent = getDefaultTemplate(format);
log(`Found ${tags.length} version tags, generating changelog...`, "info");
const versions = [];
for (let i = tags.length - 1; i >= 0; i--) {
const currentTag = tags[i];
const previousTag = i > 0 ? tags[i - 1].tag : null;
log(`Processing changes for ${currentTag.tag}...`, "info");
try {
let tagRange;
if (previousTag) {
tagRange = `${previousTag}..${currentTag.tag}`;
} else if (since && currentTag.tag === since) {
try {
const allTagsCmd = `git tag --list "${versionPrefix}*" --sort=creatordate`;
const allTagsOutput = (0, import_node_child_process2.execSync)(allTagsCmd, { encoding: "utf8" }).trim();
const allTags = allTagsOutput.split("\n").filter((tag) => !!tag);
const sinceIndex = allTags.findIndex((tag) => tag === since);
const actualPreviousTag = sinceIndex > 0 ? allTags[sinceIndex - 1] : null;
if (actualPreviousTag) {
tagRange = `${actualPreviousTag}..${currentTag.tag}`;
} else {
tagRange = currentTag.tag;
}
} catch (error) {
log(`Failed to find previous tag for ${currentTag.tag}: ${error}`, "warning");
tagRange = currentTag.tag;
}
} else {
tagRange = currentTag.tag;
}
const entries = extractChangelogEntriesFromCommits(projectDir, tagRange);
if (!entries.length) {
log(`No changelog entries found for ${currentTag.tag}, adding placeholder entry`, "info");
entries.push({
type: "changed",
description: `Release version ${currentTag.version}`
});
}
versions.unshift(
formatChangelogEntries(
format,
currentTag.version,
currentTag.date,
entries,
packageName,
repoUrl
)
);
} catch (error) {
log(`Failed to process version ${currentTag.tag}: ${error}`, "error");
}
}
changelogContent += versions.join("\n\n");
return changelogContent;
}
async function writeChangelog(content, outputPath, dryRun) {
if (dryRun) {
log("--- Changelog Preview ---", "info");
console.log(content);
log("--- End Preview ---", "info");
return;
}
try {
import_node_fs.default.writeFileSync(outputPath, content, "utf8");
log(`Changelog successfully written to ${outputPath}`, "success");
} catch (error) {
throw new Error(
`Failed to write changelog: ${error instanceof Error ? error.message : String(error)}`
);
}
}
// src/config.ts
var fs2 = __toESM(require("fs"), 1);
var import_node_process = require("process");
function loadConfig(configPath) {
const localProcess = (0, import_node_process.cwd)();
const filePath = configPath || `${localProcess}/version.config.json`;
return new Promise((resolve, reject) => {
fs2.readFile(filePath, "utf-8", (err, data) => {
if (err) {
reject(new Error(`Could not locate the config file at ${filePath}: ${err.message}`));
return;
}
try {
const config = JSON.parse(data);
resolve(config);
} catch (err2) {
const errorMessage = err2 instanceof Error ? err2.message : String(err2);
reject(new Error(`Failed to parse config file ${filePath}: ${errorMessage}`));
}
});
});
}
// src/core/versionEngine.ts
var import_node_process5 = require("process");
var import_get_packages = require("@manypkg/get-packages");
// src/errors/gitError.ts
var GitError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "GitError";
}
};
function createGitError(code, details) {
const messages = {
["NOT_GIT_REPO" /* NOT_GIT_REPO */]: "Not a git repository",
["GIT_PROCESS_ERROR" /* GIT_PROCESS_ERROR */]: "Failed to create new version",
["NO_FILES" /* NO_FILES */]: "No files specified for commit",
["NO_COMMIT_MESSAGE" /* NO_COMMIT_MESSAGE */]: "Commit message is required",
["GIT_ERROR" /* GIT_ERROR */]: "Git operation failed"
};
const baseMessage = messages[code];
const fullMessage = details ? `${baseMessage}: ${details}` : baseMessage;
return new GitError(fullMessage, code);
}
// src/errors/versionError.ts
var VersionError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "VersionError";
}
};
function createVersionError(code, details) {
const messages = {
["CONFIG_REQUIRED" /* CONFIG_REQUIRED */]: "Configuration is required",
["PACKAGES_NOT_FOUND" /* PACKAGES_NOT_FOUND */]: "Failed to get packages information",
["WORKSPACE_ERROR" /* WORKSPACE_ERROR */]: "Failed to get workspace packages",
["INVALID_CONFIG" /* INVALID_CONFIG */]: "Invalid configuration",
["PACKAGE_NOT_FOUND" /* PACKAGE_NOT_FOUND */]: "Package not found",
["VERSION_CALCULATION_ERROR" /* VERSION_CALCULATION_ERROR */]: "Failed to calculate version"
};
const baseMessage = messages[code];
const fullMessage = details ? `${baseMessage}: ${details}` : baseMessage;
return new VersionError(fullMessage, code);
}
// src/utils/packageFiltering.ts
var import_node_path2 = __toESM(require("path"), 1);
var import_micromatch = __toESM(require("micromatch"), 1);
function filterPackagesByConfig(packages, configTargets, workspaceRoot) {
if (configTargets.length === 0) {
log("No config targets specified, returning all packages", "debug");
return packages;
}
const matchedPackages = /* @__PURE__ */ new Set();
for (const target of configTargets) {
const dirMatches = filterByDirectoryPattern(packages, target, workspaceRoot);
const nameMatches = filterByPackageNamePattern(packages, target);
dirMatches.forEach((pkg) => matchedPackages.add(pkg));
nameMatches.forEach((pkg) => matchedPackages.add(pkg));
}
return Array.from(matchedPackages);
}
function filterByDirectoryPattern(packages, pattern, workspaceRoot) {
if (pattern === "./" || pattern === ".") {
return packages.filter((pkg) => pkg.dir === workspaceRoot);
}
const normalizedPattern = pattern.replace(/\\/g, "/");
return packages.filter((pkg) => {
const relativePath = import_node_path2.default.relative(workspaceRoot, pkg.dir);
const normalizedRelativePath = relativePath.replace(/\\/g, "/");
if (normalizedPattern === normalizedRelativePath) {
return true;
}
try {
return import_micromatch.default.isMatch(normalizedRelativePath, normalizedPattern, {
dot: true,
noglobstar: false,
bash: true
});
} catch (error) {
log(
`Invalid directory pattern "${pattern}": ${error instanceof Error ? error.message : String(error)}`,
"warning"
);
return false;
}
});
}
function filterByPackageNamePattern(packages, pattern) {
return packages.filter((pkg) => {
var _a;
if (!((_a = pkg.packageJson) == null ? void 0 : _a.name) || typeof pkg.packageJson.name !== "string") {
return false;
}
return matchesPackageNamePattern(pkg.packageJson.name, pattern);
});
}
function matchesPackageNamePattern(packageName, pattern) {
if (packageName === pattern) {
return true;
}
if (pattern.startsWith("@") && pattern.endsWith("/*") && !pattern.includes("**")) {
const scope = pattern.slice(0, -2);
return packageName.startsWith(`${scope}/`);
}
try {
return import_micromatch.default.isMatch(packageName, pattern, {
dot: true,
contains: false,
noglobstar: false,
bash: true
});
} catch (error) {
log(
`Invalid package name pattern "${pattern}": ${error instanceof Error ? error.message : String(error)}`,
"warning"
);
return false;
}
}
// src/core/versionStrategies.ts
var import_node_child_process4 = require("child_process");
var import_node_fs7 = __toESM(require("fs"), 1);
var path8 = __toESM(require("path"), 1);
// src/changelog/changelogManager.ts
var fs3 = __toESM(require("fs"), 1);
var path3 = __toESM(require("path"), 1);
function updateChangelog(packagePath, packageName, version, entries, repoUrl, format = "keep-a-changelog") {
try {
const changelogPath = path3.join(packagePath, "CHANGELOG.md");
let existingContent = "";
if (fs3.existsSync(changelogPath)) {
existingContent = fs3.readFileSync(changelogPath, "utf8");
}
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const newVersionContent = formatChangelogEntries(
format,
version,
today,
entries,
packageName,
repoUrl
);
let finalContent;
if (existingContent) {
if (format === "keep-a-changelog") {
const headerEndIndex = existingContent.indexOf("\n## ");
if (headerEndIndex > 0) {
const beforeVersions = existingContent.substring(0, headerEndIndex);
const afterVersions = existingContent.substring(headerEndIndex);
finalContent = `${beforeVersions}
${newVersionContent}
${afterVersions}`;
} else {
finalContent = `${existingContent}
${newVersionContent}
`;
}
} else {
const headerEndIndex = existingContent.indexOf("\n## ");
if (headerEndIndex > 0) {
const beforeVersions = existingContent.substring(0, headerEndIndex);
const afterVersions = existingContent.substring(headerEndIndex);
finalContent = `${beforeVersions}
${newVersionContent}
${afterVersions}`;
} else {
finalContent = `${existingContent}
${newVersionContent}
`;
}
}
} else {
if (format === "keep-a-changelog") {
finalContent = `# Changelog
All notable changes to ${packageName} will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
${newVersionContent}
`;
} else {
finalContent = `# Changelog
${newVersionContent}
`;
}
}
log(`Writing changelog to: ${changelogPath}`, "info");
fs3.writeFileSync(changelogPath, finalContent);
log(`Updated changelog at ${changelogPath}`, "success");
} catch (error) {
log(
`Error updating changelog: ${error instanceof Error ? error.message : String(error)}`,
"error"
);
}
}
// src/git/commands.ts
var import_node_process2 = require("process");
// src/git/commandExecutor.ts
var import_node_child_process3 = require("child_process");
var execAsync = (command, options) => {
const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
return new Promise((resolve, reject) => {
(0, import_node_child_process3.exec)(
command,
defaultOptions,
(error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve({ stdout: stdout.toString(), stderr: stderr.toString() });
}
}
);
});
};
var execSync3 = (command, args) => (0, import_node_child_process3.execSync)(command, { maxBuffer: 1024 * 1024 * 10, ...args });
// src/git/repository.ts
var import_node_fs2 = require("fs");
var import_node_path3 = require("path");
function isGitRepository(directory) {
const gitDir = (0, import_node_path3.join)(directory, ".git");
if (!(0, import_node_fs2.existsSync)(gitDir)) {
return false;
}
const stats = (0, import_node_fs2.statSync)(gitDir);
if (!stats.isDirectory()) {
return false;
}
try {
execSync3("git rev-parse --is-inside-work-tree", { cwd: directory });
return true;
} catch (_error) {
return false;
}
}
function getCurrentBranch() {
const result = execSync3("git rev-parse --abbrev-ref HEAD");
return result.toString().trim();
}
// src/git/commands.ts
async function gitAdd(files) {
const command = `git add ${files.join(" ")}`;
return execAsync(command);
}
async function gitCommit(options) {
const command = ["commit"];
if (options.amend) {
command.push("--amend");
}
if (options.author) {
command.push(`--author="${options.author}"`);
}
if (options.date) {
command.push(`--date="${options.date}"`);
}
if (options.skipHooks) {
command.push("--no-verify");
}
command.push(`-m "${options.message}"`);
return execAsync(`git ${command.join(" ")}`);
}
async function createGitTag(options) {
const { tag, message = "", args = "" } = options;
const command = `git tag -a -m "${message}" ${tag} ${args}`;
return execAsync(command);
}
async function gitProcess(options) {
const { files, nextTag, commitMessage, skipHooks, dryRun } = options;
if (!isGitRepository((0, import_node_process2.cwd)())) {
throw createGitError("NOT_GIT_REPO" /* NOT_GIT_REPO */);
}
try {
if (!dryRun) {
await gitAdd(files);
await gitCommit({
message: commitMessage,
skipHooks
});
if (nextTag) {
const tagMessage = `New Version ${nextTag} generated at ${(/* @__PURE__ */ new Date()).toISOString()}`;
await createGitTag({
tag: nextTag,
message: tagMessage
});
}
} else {
log("[DRY RUN] Would add files:", "info");
for (const file of files) {
log(` - ${file}`, "info");
}
log(`[DRY RUN] Would commit with message: "${commitMessage}"`, "info");
if (nextTag) {
log(`[DRY RUN] Would create tag: ${nextTag}`, "info");
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
log(`Git process error: ${errorMessage}`, "error");
if (err instanceof Error && err.stack) {
console.error("Git process stack trace:");
console.error(err.stack);
}
throw createGitError("GIT_PROCESS_ERROR" /* GIT_PROCESS_ERROR */, errorMessage);
}
}
async function createGitCommitAndTag(files, nextTag, commitMessage, skipHooks, dryRun) {
try {
if (!files || files.length === 0) {
throw createGitError("NO_FILES" /* NO_FILES */);
}
if (!commitMessage) {
throw createGitError("NO_COMMIT_MESSAGE" /* NO_COMMIT_MESSAGE */);
}
setCommitMessage(commitMessage);
if (nextTag) {
addTag(nextTag);
}
await gitProcess({
files,
nextTag,
commitMessage,
skipHooks,
dryRun
});
if (!dryRun) {
log(`Created tag: ${nextTag}`, "success");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to create git commit and tag: ${errorMessage}`, "error");
if (error instanceof Error) {
console.error("Git operation error details:");
console.error(error.stack || error.message);
if (errorMessage.includes("Command failed:")) {
const cmdOutput = errorMessage.split("Command failed:")[1];
if (cmdOutput) {
console.error("Git command output:", cmdOutput.trim());
}
}
} else {
console.error("Unknown git error:", error);
}
throw new GitError(`Git operation failed: ${errorMessage}`, "GIT_ERROR" /* GIT_ERROR */);
}
}
// src/git/tagsAndBranches.ts
var import_git_semver_tags = require("git-semver-tags");
var import_semver = __toESM(require("semver"), 1);
// src/utils/formatting.ts
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function formatVersionPrefix(prefix) {
return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
}
function formatTag(version, prefix, packageName, template, packageSpecificTags) {
if ((template == null ? void 0 : template.includes("${packageName}")) && !packageName) {
log(
'Warning: Your tagTemplate contains ${packageName} but no package name is available.\nThis will result in an empty package name in the tag (e.g., "@v1.0.0" instead of "my-package@v1.0.0").\n\nTo fix this:\n\u2022 If using synced mode: Set "packageSpecificTags": true in your config to enable package names in tags\n\u2022 If you want global tags: Remove ${packageName} from your tagTemplate (e.g., use "${prefix}${version}")\n\u2022 If using single/async mode: Ensure your package.json has a valid "name" field',
"warning"
);
}
if (template) {
return template.replace(/\$\{version\}/g, version).replace(/\$\{prefix\}/g, prefix).replace(/\$\{packageName\}/g, packageName || "");
}
if (packageSpecificTags && packageName) {
return `${packageName}@${prefix}${version}`;
}
return `${prefix}${version}`;
}
function formatCommitMessage(template, version, packageName, additionalContext) {
if (template.includes("${packageName}") && !packageName) {
log(
'Warning: Your commitMessage template contains ${packageName} but no package name is available.\nThis will result in an empty package name in the commit message (e.g., "Release @v1.0.0").\n\nTo fix this:\n\u2022 If using synced mode: Set "packageSpecificTags": true to enable package names in commits\n\u2022 If you want generic commit messages: Remove ${packageName} from your commitMessage template\n\u2022 If using single/async mode: Ensure your package.json has a valid "name" field',
"warning"
);
}
let result = template.replace(/\$\{version\}/g, version).replace(/\$\{packageName\}/g, packageName || "");
if (additionalContext) {
for (const [key, value] of Object.entries(additionalContext)) {
const placeholder = `\${${key}}`;
result = result.replace(
new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
value
);
}
}
return result;
}
// src/git/tagsAndBranches.ts
function getCommitsLength(pkgRoot, sinceTag) {
try {
let gitCommand;
if (sinceTag && sinceTag.trim() !== "") {
gitCommand = `git rev-list --count ${sinceTag}..HEAD ${pkgRoot}`;
} else {
gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
}
const amount = execSync3(gitCommand).toString().trim();
return Number(amount);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get number of commits since last tag: ${errorMessage}`, "error");
return 0;
}
}
async function getLatestTag(versionPrefix) {
try {
const tags = await (0, import_git_semver_tags.getSemverTags)({
tagPrefix: versionPrefix
});
if (tags.length === 0) {
return "";
}
const chronologicalLatest = tags[0];
const sortedTags = [...tags].sort((a, b) => {
const versionA = import_semver.default.clean(a) || "0.0.0";
const versionB = import_semver.default.clean(b) || "0.0.0";
return import_semver.default.rcompare(versionA, versionB);
});
const semanticLatest = sortedTags[0];
if (semanticLatest !== chronologicalLatest) {
log(
`Tag ordering differs: chronological latest is ${chronologicalLatest}, semantic latest is ${semanticLatest}`,
"debug"
);
log(`Using semantic latest (${semanticLatest}) to handle out-of-order tag creation`, "info");
}
return semanticLatest;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get latest tag: ${errorMessage}`, "error");
if (error instanceof Error && error.message.includes("No names found")) {
log("No tags found in the repository.", "info");
}
return "";
}
}
async function lastMergeBranchName(branches, baseBranch) {
try {
const escapedBranches = branches.map((branch) => escapeRegExp(branch));
const branchesRegex = `${escapedBranches.join("/(.*)|")}/(.*)`;
const command = `git for-each-ref --sort=-committerdate --format='%(refname:short)' refs/heads --merged ${baseBranch} | grep -o -i -E "${branchesRegex}" | awk -F'[ ]' '{print $1}' | head -n 1`;
const { stdout } = await execAsync(command);
return stdout.trim();
} catch (error) {
console.error(
"Error while getting the last branch name:",
error instanceof Error ? error.message : String(error)
);
return null;
}
}
async function getLatestTagForPackage(packageName, versionPrefix, options) {
try {
const tagTemplate = (options == null ? void 0 : options.tagTemplate) || "${prefix}${version}";
const packageSpecificTags = (options == null ? void 0 : options.packageSpecificTags) ?? false;
const escapedPackageName = escapeRegExp(packageName);
const escapedPrefix = versionPrefix ? escapeRegExp(versionPrefix) : "";
log(
`Looking for tags for package ${packageName} with prefix ${versionPrefix || "none"}, packageSpecificTags: ${packageSpecificTags}`,
"debug"
);
const allTags = await (0, import_git_semver_tags.getSemverTags)({
tagPrefix: versionPrefix
});
log(`Retrieved ${allTags.length} tags: ${allTags.join(", ")}`, "debug");
if (packageSpecificTags) {
const packageTagPattern = escapeRegExp(tagTemplate).replace(/\\\$\\\{packageName\\\}/g, `(?:${escapedPackageName})`).replace(/\\\$\\\{prefix\\\}/g, `(?:${escapedPrefix})`).replace(/\\\$\\\{version\\\}/g, "(?:[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)");
log(`Using package tag pattern: ${packageTagPattern}`, "debug");
const packageTagRegex = new RegExp(`^${packageTagPattern}$`);
let packageTags = allTags.filter((tag) => packageTagRegex.test(tag));
if (packageTags.length > 0) {
const chronologicalFirst = packageTags[0];
const sortedPackageTags2 = [...packageTags].sort((a, b) => {
let versionA = "";
let versionB = "";
if (a.includes("@")) {
const afterAt = a.split("@")[1] || "";
versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
} else {
versionA = a.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
}
if (b.includes("@")) {
const afterAtB = b.split("@")[1] || "";
versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
} else {
versionB = b.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
}
const cleanVersionA = import_semver.default.clean(versionA) || "0.0.0";
const cleanVersionB = import_semver.default.clean(versionB) || "0.0.0";
return import_semver.default.rcompare(cleanVersionA, cleanVersionB);
});
log(`Found ${packageTags.length} package tags using configured pattern`, "debug");
log(`Using semantically latest tag: ${sortedPackageTags2[0]}`, "debug");
if (sortedPackageTags2[0] !== chronologicalFirst) {
log(
`Package tag ordering differs: chronological first is ${chronologicalFirst}, semantic latest is ${sortedPackageTags2[0]}`,
"debug"
);
}
return sortedPackageTags2[0];
}
if (versionPrefix) {
const pattern1 = new RegExp(`^${escapedPackageName}@${escapeRegExp(versionPrefix)}`);
packageTags = allTags.filter((tag) => pattern1.test(tag));
if (packageTags.length > 0) {
const sortedPackageTags2 = [...packageTags].sort((a, b) => {
const afterAt = a.split("@")[1] || "";
const versionA = afterAt.replace(
new RegExp(`^${escapeRegExp(versionPrefix || "")}`),
""
);
const afterAtB = b.split("@")[1] || "";
const versionB = afterAtB.replace(
new RegExp(`^${escapeRegExp(versionPrefix || "")}`),
""
);
const cleanVersionA = import_semver.default.clean(versionA) || "0.0.0";
const cleanVersionB = import_semver.default.clean(versionB) || "0.0.0";
return import_semver.default.rcompare(cleanVersionA, cleanVersionB);
});
log(
`Found ${packageTags.length} package tags using pattern: packageName@${versionPrefix}...`,
"debug"
);
log(`Using semantically latest tag: ${sortedPackageTags2[0]}`, "debug");
return sortedPackageTags2[0];
}
}
if (versionPrefix) {
const pattern2 = new RegExp(`^${escapeRegExp(versionPrefix)}${escapedPackageName}@`);
packageTags = allTags.filter((tag) => pattern2.test(tag));
if (packageTags.length > 0) {
const sortedPackageTags2 = [...packageTags].sort((a, b) => {
const versionA = import_semver.default.clean(a.split("@")[1] || "") || "0.0.0";
const versionB = import_semver.default.clean(b.split("@")[1] || "") || "0.0.0";
return import_semver.default.rcompare(versionA, versionB);
});
log(
`Found ${packageTags.length} package tags using pattern: ${versionPrefix}packageName@...`,
"debug"
);
log(`Using semantically latest tag: ${sortedPackageTags2[0]}`, "debug");
return sortedPackageTags2[0];
}
}
const pattern3 = new RegExp(`^${escapedPackageName}@`);
packageTags = allTags.filter((tag) => pattern3.test(tag));
if (packageTags.length === 0) {
log("No matching tags found for pattern: packageName@version", "debug");
if (allTags.length > 0) {
log(`Available tags: ${allTags.join(", ")}`, "debug");
} else {
log("No tags available in the repository", "debug");
}
return "";
}
const sortedPackageTags = [...packageTags].sort((a, b) => {
const versionA = import_semver.default.clean(a.split("@")[1] || "") || "0.0.0";
const versionB = import_semver.default.clean(b.split("@")[1] || "") || "0.0.0";
return import_semver.default.rcompare(versionA, versionB);
});
log(`Found ${packageTags.length} package tags for ${packageName}`, "debug");
log(`Using semantically latest tag: ${sortedPackageTags[0]}`, "debug");
return sortedPackageTags[0];
}
log(`Package-specific tags disabled for ${packageName}, falling back to global tags`, "debug");
return "";
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Failed to get latest tag for package ${packageName}: ${errorMessage}`, "error");
if (error instanceof Error && error.message.includes("No names found")) {
log(`No tags found for package ${packageName}.`, "info");
}
return "";
}
}
// src/package/packageManagement.ts
var import_node_fs4 = __toESM(require("fs"), 1);
var import_node_path5 = __toESM(require("path"), 1);
// src/cargo/cargoHandler.ts
var import_node_fs3 = __toESM(require("fs"), 1);
var import_node_path4 = __toESM(require("path"), 1);
var TOML = __toESM(require("smol-toml"), 1);
function getCargoInfo(cargoPath) {
var _a;
if (!import_node_fs3.default.existsSync(cargoPath)) {
log(`Cargo.toml file not found at: ${cargoPath}`, "error");
throw new Error(`Cargo.toml file not found at: ${cargoPath}`);
}
try {
const fileContent = import_node_fs3.default.readFileSync(cargoPath, "utf8");
const cargo = TOML.parse(fileContent);
if (!((_a = cargo.package) == null ? void 0 : _a.name)) {
log(`Package name not found in: ${cargoPath}`, "error");
throw new Error(`Package name not found in: ${cargoPath}`);
}
return {
name: cargo.package.name,
version: cargo.package.version || "0.0.0",
path: cargoPath,
dir: import_node_path4.default.dirname(cargoPath),
content: cargo
};
} catch (error) {
log(`Error reading Cargo.toml: ${cargoPath}`, "error");
if (error instanceof Error) {
log(error.message, "error");
throw error;
}
throw new Error(`Failed to process Cargo.toml at ${cargoPath}`);
}
}
function isCargoToml(filePath) {
return import_node_path4.default.basename(filePath) === "Cargo.toml";
}
function updateCargoVersion(cargoPath, version) {
var _a;
try {
const originalContent = import_node_fs3.default.readFileSync(cargoPath, "utf8");
const cargo = TOML.parse(originalContent);
const packageName = (_a = cargo.package) == null ? void 0 : _a.name;
if (!packageName) {
throw new Error(`No package name found in ${cargoPath}`);
}
if (!cargo.package) {
cargo.package = { name: packageName, version };
} else {
cargo.package.version = version;
}
const updatedContent = TOML.stringify(cargo);
import_node_fs3.default.writeFileSync(cargoPath, updatedContent);
addPackageUpdate(packageName, version, cargoPath);
log(`Updated Cargo.toml at ${cargoPath} to version ${version}`, "success");
} catch (error) {
log(`Failed to update Cargo.toml at ${cargoPath}`, "error");
if (error instanceof Error) {
log(error.message, "error");
}
throw error;
}
}
// src/package/packageManagement.ts
function updatePackageVersion(packagePath, version) {
if (isCargoToml(packagePath)) {
updateCargoVersion(packagePath, version);
return;
}
try {
const packageContent = import_node_fs4.default.readFileSync(packagePath, "utf8");
const packageJson = JSON.parse(packageContent);
const packageName = packageJson.name;
packageJson.version = version;
import_node_fs4.default.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}
`);
addPackageUpdate(packageName, version, packagePath);
log(`Updated package.json at ${packagePath} to version ${version}`, "success");
} catch (error) {
log(`Failed to update package.json at ${packagePath}`, "error");
if (error instanceof Error) {
log(error.message, "error");
}
throw error;
}
}
// src/package/packageProcessor.ts
var fs8 = __toESM(require("fs"), 1);
var import_node_path7 = __toESM(require("path"), 1);
var import_node_process4 = require("process");
// src/core/versionCalculator.ts
var import_node_process3 = require("process");
var import_conventional_recommended_bump = require("conventional-recommended-bump");
var import_semver3 = __toESM(require("semver"), 1);
// src/utils/manifestHelpers.ts
var import_node_fs5 = __toESM(require("fs"), 1);
var import_node_path6 = __toESM(require("path"), 1);
function getVersionFromManifests(packageDir) {
const packageJsonPath = import_node_path6.default.join(packageDir, "package.json");
const cargoTomlPath = import_node_path6.default.join(packageDir, "Cargo.toml");
if (import_node_fs5.default.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(import_node_fs5.default.readFileSync(packageJsonPath, "utf-8"));
if (packageJson.version) {
log(`Found version ${packageJson.version} in package.json`, "debug");
return {
version: packageJson.version,
manifestFound: true,
manifestPath: packageJsonPath,
manifestType: "package.json"
};
}
log("No version field found in package.json", "debug");
} catch (packageJsonError) {
const errMsg = packageJsonError instanceof Error ? packageJsonError.message : String(packageJsonError);
log(`Error reading package.json: ${errMsg}`, "warning");
}
}
if (import_node_fs5.default.existsSync(cargoTomlPath)) {
try {
const cargoInfo = getCargoInfo(cargoTomlPath);
if (cargoInfo.version) {
log(`Found version ${cargoInfo.version} in Cargo.toml`, "debug");
return {
version: cargoInfo.version,
manifestFound: true,
manifestPath: cargoTomlPath,
manifestType: "Cargo.toml"
};
}
log("No version field found in Cargo.toml", "debug");
} catch (cargoTomlError) {
const errMsg = cargoTomlError instanceof Error ? cargoTomlError.message : String(cargoTomlError);
log(`Error reading Cargo.toml: ${errMsg}`, "warning");
}
}
return {
version: null,
manifestFound: false,
manifestPath: "",
manifestType: null
};
}
// src/utils/versionUtils.ts
var import_node_fs6 = __toESM(require("fs"), 1);
var import_semver2 = __toESM(require("semver"), 1);
var TOML2 = __toESM(require("smol-toml"), 1);
// src/git/tagVerification.ts
function verifyTag(tagName, cwd5) {
if (!tagName || tagName.trim() === "") {
return { exists: false, reachable: false, error: "Empty tag name" };
}
try {
execSync3(`git rev-parse --verify "${tagName}"`, {
cwd: cwd5,
stdio: "ignore"
});
return { exists: true, reachable: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("unknown revision") || errorMessage.includes("bad revision") || errorMessage.includes("No such ref")) {
return {
exists: false,
reachable: false,
error: `Tag '${tagName}' not found in repository`
};
}
return {
exists: false,
reachable: false,
error: `Git error: ${errorMessage}`
};
}
}
// src/utils/versionUtils.ts
var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
function normalizePrereleaseIdentifier(prereleaseIdentifier, config) {
if (prereleaseIdentifier === true) {
return (config == null ? void 0 : config.prereleaseIdentifier) || "next";
}
if (typeof prereleaseIdentifier === "string") {
return prereleaseIdentifier;
}
return void 0;
}
function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
if (prereleaseIden