package-versioner
Version:
A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.
1,435 lines (1,384 loc) • 96.5 kB
JavaScript
#!/usr/bin/env node
import {
BasePackageVersionerError,
addPackageUpdate,
addTag,
enableJsonOutput,
log,
printJsonOutput,
setCommitMessage
} from "./chunk-IXZZQDKS.js";
// src/index.ts
import * as fs10 from "fs";
import path9 from "path";
import { Command } from "commander";
// src/changelog/changelogRegenerator.ts
import { execSync as execSync2 } from "child_process";
import fs from "fs";
import path from "path";
// src/changelog/commitParser.ts
import { execSync } from "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 = 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 = execSync2(command, { encoding: "utf8" }).trim();
if (!tagOutput) {
return [];
}
const allTags = tagOutput.split("\n").filter((tag) => !!tag);
let filteredTags = allTags;
if (since) {
const sinceIndex = allTags.indexOf(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 = execSync2(`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 = path.join(projectDir, "package.json");
let packageName = "";
let repoUrl = options.repoUrl;
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.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 = execSync2("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 = execSync2(allTagsCmd, { encoding: "utf8" }).trim();
const allTags = allTagsOutput.split("\n").filter((tag) => !!tag);
const sinceIndex = allTags.indexOf(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 {
fs.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
import * as fs2 from "fs";
import { cwd } from "process";
function loadConfig(configPath) {
const localProcess = cwd();
const filePath = configPath || `${localProcess}/version.config.json`;
return new Promise((resolve2, 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);
resolve2(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
import { cwd as cwd4 } from "process";
import { getPackagesSync } from "@manypkg/get-packages";
// src/errors/gitError.ts
var GitError = class extends BasePackageVersionerError {
};
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",
["TAG_ALREADY_EXISTS" /* TAG_ALREADY_EXISTS */]: "Git tag already exists"
};
const suggestions = {
["NOT_GIT_REPO" /* NOT_GIT_REPO */]: [
"Initialize git repository with: git init",
"Ensure you are in the correct directory"
],
["TAG_ALREADY_EXISTS" /* TAG_ALREADY_EXISTS */]: [
"Delete the existing tag: git tag -d <tag-name>",
"Use a different version by incrementing manually",
"Check if this version was already released"
],
["GIT_PROCESS_ERROR" /* GIT_PROCESS_ERROR */]: void 0,
["NO_FILES" /* NO_FILES */]: void 0,
["NO_COMMIT_MESSAGE" /* NO_COMMIT_MESSAGE */]: void 0,
["GIT_ERROR" /* GIT_ERROR */]: void 0
};
const baseMessage = messages[code];
const fullMessage = details ? `${baseMessage}: ${details}` : baseMessage;
return new GitError(fullMessage, code, suggestions[code]);
}
// src/errors/versionError.ts
var VersionError = class extends BasePackageVersionerError {
};
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 suggestions = {
["CONFIG_REQUIRED" /* CONFIG_REQUIRED */]: [
"Create a version.config.json file in your project root",
"Check the documentation for configuration examples"
],
["PACKAGES_NOT_FOUND" /* PACKAGES_NOT_FOUND */]: [
"Ensure package.json or Cargo.toml files exist in your project",
"Check workspace configuration (pnpm-workspace.yaml, etc.)",
"Verify file permissions and paths"
],
["WORKSPACE_ERROR" /* WORKSPACE_ERROR */]: [
"Verify workspace configuration files are valid",
"Check that workspace packages are accessible",
"Ensure proper monorepo structure"
],
["INVALID_CONFIG" /* INVALID_CONFIG */]: [
"Validate version.config.json syntax",
"Check configuration against schema",
"Review documentation for valid configuration options"
],
["PACKAGE_NOT_FOUND" /* PACKAGE_NOT_FOUND */]: [
"Verify package name spelling and case",
"Check if package exists in workspace",
"Review packages configuration in version.config.json"
],
["VERSION_CALCULATION_ERROR" /* VERSION_CALCULATION_ERROR */]: [
"Ensure git repository has commits",
"Check conventional commit message format",
"Verify git tags are properly formatted"
]
};
const baseMessage = messages[code];
const fullMessage = details ? `${baseMessage}: ${details}` : baseMessage;
return new VersionError(fullMessage, code, suggestions[code]);
}
// src/utils/packageFiltering.ts
import path2 from "path";
import micromatch from "micromatch";
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);
for (const pkg of dirMatches) {
matchedPackages.add(pkg);
}
for (const pkg of nameMatches) {
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 = path2.relative(workspaceRoot, pkg.dir);
const normalizedRelativePath = relativePath.replace(/\\/g, "/");
if (normalizedPattern === normalizedRelativePath) {
return true;
}
try {
return micromatch.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 micromatch.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
import { execSync as execSync4 } from "child_process";
import fs9 from "fs";
import * as path8 from "path";
// src/changelog/changelogManager.ts
import * as fs3 from "fs";
import * as path3 from "path";
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
import { cwd as cwd2 } from "process";
// src/git/commandExecutor.ts
import { exec, execSync as nativeExecSync } from "child_process";
var execAsync = (command, options) => {
const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
return new Promise((resolve2, reject) => {
exec(
command,
defaultOptions,
(error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve2({ stdout: stdout.toString(), stderr: stderr.toString() });
}
}
);
});
};
var execSync3 = (command, args) => nativeExecSync(command, { maxBuffer: 1024 * 1024 * 10, ...args });
// src/git/repository.ts
import { existsSync as existsSync2, statSync } from "fs";
import { join as join2 } from "path";
function isGitRepository(directory) {
const gitDir = join2(directory, ".git");
if (!existsSync2(gitDir)) {
return false;
}
const stats = 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}`;
try {
return await execAsync(command);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("already exists")) {
throw createGitError(
"TAG_ALREADY_EXISTS" /* TAG_ALREADY_EXISTS */,
`Tag '${tag}' already exists in the repository. Please use a different version or delete the existing tag first.`
);
}
throw createGitError("GIT_ERROR" /* GIT_ERROR */, errorMessage);
}
}
async function gitProcess(options) {
const { files, nextTag, commitMessage, skipHooks, dryRun } = options;
if (!isGitRepository(cwd2())) {
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);
if (errorMessage.includes("already exists") && nextTag) {
log(`Tag '${nextTag}' already exists in the repository.`, "error");
throw createGitError(
"TAG_ALREADY_EXISTS" /* TAG_ALREADY_EXISTS */,
`Tag '${nextTag}' already exists in the repository. Please use a different version or delete the existing tag first.`
);
}
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) {
if (error instanceof GitError) {
throw 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
import { getSemverTags } from "git-semver-tags";
import semver from "semver";
// 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 sync 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 sync 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 getSemverTags({
tagPrefix: versionPrefix
});
if (tags.length === 0) {
return "";
}
const chronologicalLatest = tags[0];
const sortedTags = [...tags].sort((a, b) => {
const versionA = semver.clean(a) || "0.0.0";
const versionB = semver.clean(b) || "0.0.0";
return semver.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 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 = semver.clean(versionA) || "0.0.0";
const cleanVersionB = semver.clean(versionB) || "0.0.0";
return semver.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 = semver.clean(versionA) || "0.0.0";
const cleanVersionB = semver.clean(versionB) || "0.0.0";
return semver.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 = semver.clean(a.split("@")[1] || "") || "0.0.0";
const versionB = semver.clean(b.split("@")[1] || "") || "0.0.0";
return semver.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 = semver.clean(a.split("@")[1] || "") || "0.0.0";
const versionB = semver.clean(b.split("@")[1] || "") || "0.0.0";
return semver.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
import fs5 from "fs";
import path5 from "path";
// src/cargo/cargoHandler.ts
import fs4 from "fs";
import path4 from "path";
import * as TOML from "smol-toml";
function getCargoInfo(cargoPath) {
var _a;
if (!fs4.existsSync(cargoPath)) {
log(`Cargo.toml file not found at: ${cargoPath}`, "error");
throw new Error(`Cargo.toml file not found at: ${cargoPath}`);
}
try {
const fileContent = fs4.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: path4.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 path4.basename(filePath) === "Cargo.toml";
}
function updateCargoVersion(cargoPath, version) {
var _a;
try {
const originalContent = fs4.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);
fs4.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 = fs5.readFileSync(packagePath, "utf8");
const packageJson = JSON.parse(packageContent);
const packageName = packageJson.name;
packageJson.version = version;
fs5.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
import * as fs8 from "fs";
import path7 from "path";
import { exit } from "process";
// src/core/versionCalculator.ts
import { cwd as cwd3 } from "process";
import { Bumper } from "conventional-recommended-bump";
import semver3 from "semver";
// src/utils/manifestHelpers.ts
import fs6 from "fs";
import path6 from "path";
function getVersionFromManifests(packageDir) {
const packageJsonPath = path6.join(packageDir, "package.json");
const cargoTomlPath = path6.join(packageDir, "Cargo.toml");
if (fs6.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs6.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 (fs6.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
import fs7 from "fs";
import semver2 from "semver";
import * as TOML2 from "smol-toml";
// 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 (prereleaseIdentifier && STANDARD_BUMP_TYPES.includes(bumpType) && !semver2.prerelease(currentVersion)) {
const preBumpType = `pre${bumpType}`;
log(
`Creating prerelease version with identifier '${prereleaseIdentifier}' using ${preBumpType}`,
"debug"
);
return semver2.inc(currentVersion, preBumpType, prereleaseIdentifier) || "";
}
if (semver2.prerelease(currentVersion) && STANDARD_BUMP_TYPES.includes(bumpType)) {
const parsed = semver2.parse(currentVersion);
if (!parsed) {
return semver2.inc(currentVersion, bumpType) || "";
}
if (bumpType === "major" && parsed.minor === 0 && parsed.patch === 0 || bumpType === "minor" && parsed.patch === 0 || bumpType === "patch") {
log(`Cleaning prerelease identifier from ${currentVersion} for ${bumpType} bump`, "debug");
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
}
log(`Standard increment for ${currentVersion} with ${bumpType} bump`, "debug");
return semver2.inc(currentVersion, bumpType) || "";
}
return semver2.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
}
async function getBestVersionSource(tagName, packageVersion, cwd5) {
if (!(tagName == null ? void 0 : tagName.trim())) {
return packageVersion ? { source: "package", version: packageVersion, reason: "No git tag provided" } : { source: "initial", version: "0.1.0", reason: "No git tag or package version available" };
}
const verification = verifyTag(tagName, cwd5);
if (!verification.exists || !verification.reachable) {
if (packageVersion) {
log(
`Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
"warning"
);
return { source: "package", version: packageVersion, reason: "Git tag unreachable" };
}
log(
`Git tag '${tagName}' unreachable and no package version available, using initial version`,
"warning"
);
return {
source: "initial",
version: "0.1.0",
reason: "Git tag unreachable, no package version"
};
}
if (!packageVersion) {
return {
source: "git",
version: tagName,
reason: "Git