package-versioner 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,6 +55,9 @@ npx package-versioner --bump patch --prerelease alpha
55
55
  # Target specific packages (only in async/independent mode, comma-separated)
56
56
  npx package-versioner -t @scope/package-a,@scope/package-b
57
57
 
58
+ # Run from a different directory
59
+ npx package-versioner --project-dir /path/to/project
60
+
58
61
  # Perform a dry run: calculates version, logs actions, but makes no file changes or Git commits/tags
59
62
  npx package-versioner --dry-run
60
63
 
package/dist/index.cjs CHANGED
@@ -472,7 +472,7 @@ function getAllVersionTags(since, versionPrefix = "v") {
472
472
  const allTags = tagOutput.split("\n").filter((tag) => !!tag);
473
473
  let filteredTags = allTags;
474
474
  if (since) {
475
- const sinceIndex = allTags.findIndex((tag) => tag === since);
475
+ const sinceIndex = allTags.indexOf(since);
476
476
  if (sinceIndex >= 0) {
477
477
  filteredTags = allTags.slice(sinceIndex);
478
478
  } else {
@@ -556,7 +556,7 @@ async function regenerateChangelog(options) {
556
556
  const allTagsCmd = `git tag --list "${versionPrefix}*" --sort=creatordate`;
557
557
  const allTagsOutput = (0, import_node_child_process2.execSync)(allTagsCmd, { encoding: "utf8" }).trim();
558
558
  const allTags = allTagsOutput.split("\n").filter((tag) => !!tag);
559
- const sinceIndex = allTags.findIndex((tag) => tag === since);
559
+ const sinceIndex = allTags.indexOf(since);
560
560
  const actualPreviousTag = sinceIndex > 0 ? allTags[sinceIndex - 1] : null;
561
561
  if (actualPreviousTag) {
562
562
  tagRange = `${actualPreviousTag}..${currentTag.tag}`;
@@ -694,8 +694,12 @@ function filterPackagesByConfig(packages, configTargets, workspaceRoot) {
694
694
  for (const target of configTargets) {
695
695
  const dirMatches = filterByDirectoryPattern(packages, target, workspaceRoot);
696
696
  const nameMatches = filterByPackageNamePattern(packages, target);
697
- dirMatches.forEach((pkg) => matchedPackages.add(pkg));
698
- nameMatches.forEach((pkg) => matchedPackages.add(pkg));
697
+ for (const pkg of dirMatches) {
698
+ matchedPackages.add(pkg);
699
+ }
700
+ for (const pkg of nameMatches) {
701
+ matchedPackages.add(pkg);
702
+ }
699
703
  }
700
704
  return Array.from(matchedPackages);
701
705
  }
@@ -759,7 +763,7 @@ function matchesPackageNamePattern(packageName, pattern) {
759
763
  }
760
764
 
761
765
  // src/core/versionStrategies.ts
762
- var import_node_child_process5 = require("child_process");
766
+ var import_node_child_process4 = require("child_process");
763
767
  var import_node_fs7 = __toESM(require("fs"), 1);
764
768
  var path8 = __toESM(require("path"), 1);
765
769
 
@@ -1041,9 +1045,14 @@ function formatCommitMessage(template, version, packageName, additionalContext)
1041
1045
  }
1042
1046
 
1043
1047
  // src/git/tagsAndBranches.ts
1044
- function getCommitsLength(pkgRoot) {
1048
+ function getCommitsLength(pkgRoot, sinceTag) {
1045
1049
  try {
1046
- const gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1050
+ let gitCommand;
1051
+ if (sinceTag && sinceTag.trim() !== "") {
1052
+ gitCommand = `git rev-list --count ${sinceTag}..HEAD ${pkgRoot}`;
1053
+ } else {
1054
+ gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1055
+ }
1047
1056
  const amount = execSync3(gitCommand).toString().trim();
1048
1057
  return Number(amount);
1049
1058
  } catch (error) {
@@ -1317,7 +1326,6 @@ function updatePackageVersion(packagePath, version) {
1317
1326
  }
1318
1327
 
1319
1328
  // src/package/packageProcessor.ts
1320
- var import_node_child_process4 = require("child_process");
1321
1329
  var fs8 = __toESM(require("fs"), 1);
1322
1330
  var import_node_path7 = __toESM(require("path"), 1);
1323
1331
  var import_node_process4 = require("process");
@@ -1376,18 +1384,41 @@ function getVersionFromManifests(packageDir) {
1376
1384
  manifestType: null
1377
1385
  };
1378
1386
  }
1379
- function throwIfNoManifestsFound(packageDir) {
1380
- const packageJsonPath = import_node_path6.default.join(packageDir, "package.json");
1381
- const cargoTomlPath = import_node_path6.default.join(packageDir, "Cargo.toml");
1382
- throw new Error(
1383
- `Neither package.json nor Cargo.toml found at ${packageDir}. Checked paths: ${packageJsonPath}, ${cargoTomlPath}. Cannot determine version.`
1384
- );
1385
- }
1386
1387
 
1387
1388
  // src/utils/versionUtils.ts
1388
1389
  var import_node_fs6 = __toESM(require("fs"), 1);
1389
1390
  var import_semver2 = __toESM(require("semver"), 1);
1390
1391
  var TOML2 = __toESM(require("smol-toml"), 1);
1392
+
1393
+ // src/git/tagVerification.ts
1394
+ function verifyTag(tagName, cwd5) {
1395
+ if (!tagName || tagName.trim() === "") {
1396
+ return { exists: false, reachable: false, error: "Empty tag name" };
1397
+ }
1398
+ try {
1399
+ execSync3(`git rev-parse --verify "${tagName}"`, {
1400
+ cwd: cwd5,
1401
+ stdio: "ignore"
1402
+ });
1403
+ return { exists: true, reachable: true };
1404
+ } catch (error) {
1405
+ const errorMessage = error instanceof Error ? error.message : String(error);
1406
+ if (errorMessage.includes("unknown revision") || errorMessage.includes("bad revision") || errorMessage.includes("No such ref")) {
1407
+ return {
1408
+ exists: false,
1409
+ reachable: false,
1410
+ error: `Tag '${tagName}' not found in repository`
1411
+ };
1412
+ }
1413
+ return {
1414
+ exists: false,
1415
+ reachable: false,
1416
+ error: `Git error: ${errorMessage}`
1417
+ };
1418
+ }
1419
+ }
1420
+
1421
+ // src/utils/versionUtils.ts
1391
1422
  var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
1392
1423
  function normalizePrereleaseIdentifier(prereleaseIdentifier, config) {
1393
1424
  if (prereleaseIdentifier === true) {
@@ -1421,6 +1452,59 @@ function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
1421
1452
  }
1422
1453
  return import_semver2.default.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
1423
1454
  }
1455
+ async function getBestVersionSource(tagName, packageVersion, cwd5) {
1456
+ if (!(tagName == null ? void 0 : tagName.trim())) {
1457
+ 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" };
1458
+ }
1459
+ const verification = verifyTag(tagName, cwd5);
1460
+ if (!verification.exists || !verification.reachable) {
1461
+ if (packageVersion) {
1462
+ log(
1463
+ `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
1464
+ "warning"
1465
+ );
1466
+ return { source: "package", version: packageVersion, reason: "Git tag unreachable" };
1467
+ }
1468
+ log(
1469
+ `Git tag '${tagName}' unreachable and no package version available, using initial version`,
1470
+ "warning"
1471
+ );
1472
+ return {
1473
+ source: "initial",
1474
+ version: "0.1.0",
1475
+ reason: "Git tag unreachable, no package version"
1476
+ };
1477
+ }
1478
+ if (!packageVersion) {
1479
+ return {
1480
+ source: "git",
1481
+ version: tagName,
1482
+ reason: "Git tag exists, no package version to compare"
1483
+ };
1484
+ }
1485
+ try {
1486
+ const cleanTagVersion = tagName.replace(/^.*?([0-9])/, "$1");
1487
+ const cleanPackageVersion = packageVersion;
1488
+ if (import_semver2.default.gt(cleanPackageVersion, cleanTagVersion)) {
1489
+ log(
1490
+ `Package version ${packageVersion} is newer than git tag ${tagName}, using package version`,
1491
+ "info"
1492
+ );
1493
+ return { source: "package", version: packageVersion, reason: "Package version is newer" };
1494
+ }
1495
+ if (import_semver2.default.gt(cleanTagVersion, cleanPackageVersion)) {
1496
+ log(
1497
+ `Git tag ${tagName} is newer than package version ${packageVersion}, using git tag`,
1498
+ "info"
1499
+ );
1500
+ return { source: "git", version: tagName, reason: "Git tag is newer" };
1501
+ }
1502
+ return { source: "git", version: tagName, reason: "Versions equal, using git tag" };
1503
+ } catch (error) {
1504
+ log(`Failed to compare versions, defaulting to git tag: ${error}`, "warning");
1505
+ return { source: "git", version: tagName, reason: "Version comparison failed" };
1506
+ }
1507
+ }
1424
1508
 
1425
1509
  // src/core/versionCalculator.ts
1426
1510
  async function calculateVersion(config, options) {
@@ -1452,84 +1536,50 @@ async function calculateVersion(config, options) {
1452
1536
  return `${packageName}@${prefix}`;
1453
1537
  }, escapeRegExp3 = function(string) {
1454
1538
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1539
+ }, getCurrentVersionFromSource2 = function() {
1540
+ if (!versionSource) {
1541
+ if (hasNoTags) {
1542
+ return initialVersion;
1543
+ }
1544
+ const cleanedTag = import_semver3.default.clean(latestTag) || latestTag;
1545
+ return import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1546
+ }
1547
+ if (versionSource.source === "git") {
1548
+ const cleanedTag = import_semver3.default.clean(versionSource.version) || versionSource.version;
1549
+ return import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1550
+ }
1551
+ return versionSource.version;
1455
1552
  };
1456
- var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3;
1553
+ var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3, getCurrentVersionFromSource = getCurrentVersionFromSource2;
1457
1554
  const originalPrefix = versionPrefix || "";
1458
1555
  const tagSearchPattern = determineTagSearchPattern2(name, originalPrefix);
1459
1556
  const escapedTagPattern = escapeRegExp3(tagSearchPattern);
1460
- if (!hasNoTags && pkgPath) {
1557
+ let versionSource;
1558
+ if (pkgPath) {
1461
1559
  const packageDir = pkgPath || (0, import_node_process3.cwd)();
1462
1560
  const manifestResult = getVersionFromManifests(packageDir);
1463
- if (manifestResult.manifestFound && manifestResult.version) {
1464
- const cleanedTag = import_semver3.default.clean(latestTag) || latestTag;
1465
- const tagVersion = import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1466
- const packageVersion = manifestResult.version;
1467
- if (import_semver3.default.gt(packageVersion, tagVersion)) {
1468
- log(
1469
- `Warning: Version mismatch detected!
1470
- \u2022 ${manifestResult.manifestType} version: ${packageVersion}
1471
- \u2022 Latest Git tag version: ${tagVersion} (from ${latestTag})
1472
- \u2022 Package version is AHEAD of Git tags
1473
-
1474
- This usually happens when:
1475
- \u2022 A version was released but the tag wasn't pushed to the remote repository
1476
- \u2022 The ${manifestResult.manifestType} was manually updated without creating a corresponding tag
1477
- \u2022 You're running in CI and the latest tag isn't available yet
1478
-
1479
- The tool will use the Git tag version (${tagVersion}) as the base for calculation.
1480
- Expected next version will be based on ${tagVersion}, not ${packageVersion}.
1481
-
1482
- To fix this mismatch:
1483
- \u2022 Push missing tags: git push origin --tags
1484
- \u2022 Or use package version as base by ensuring tags are up to date`,
1485
- "warning"
1486
- );
1487
- } else if (import_semver3.default.gt(tagVersion, packageVersion)) {
1488
- log(
1489
- `Warning: Version mismatch detected!
1490
- \u2022 ${manifestResult.manifestType} version: ${packageVersion}
1491
- \u2022 Latest Git tag version: ${tagVersion} (from ${latestTag})
1492
- \u2022 Git tag version is AHEAD of package version
1493
-
1494
- This usually happens when:
1495
- \u2022 A release was tagged but the ${manifestResult.manifestType} wasn't updated
1496
- \u2022 You're on an older branch that hasn't been updated with the latest version
1497
- \u2022 Automated release process created tags but didn't update manifest files
1498
- \u2022 You pulled tags but not the corresponding commits that update the package version
1499
-
1500
- The tool will use the Git tag version (${tagVersion}) as the base for calculation.
1501
- This will likely result in a version that's already been released.
1502
-
1503
- To fix this mismatch:
1504
- \u2022 Update ${manifestResult.manifestType}: Set version to ${tagVersion} or higher
1505
- \u2022 Or checkout the branch/commit that corresponds to the tag
1506
- \u2022 Or ensure your branch is up to date with the latest changes`,
1507
- "warning"
1508
- );
1509
- }
1510
- }
1561
+ const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
1562
+ versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir);
1563
+ log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
1511
1564
  }
1512
1565
  const specifiedType = type;
1513
1566
  if (specifiedType) {
1514
- if (hasNoTags) {
1515
- return getPackageVersionFallback(
1516
- pkgPath,
1517
- name,
1518
- specifiedType,
1519
- normalizedPrereleaseId,
1520
- initialVersion
1521
- );
1522
- }
1523
- const cleanedTag = import_semver3.default.clean(latestTag) || latestTag;
1524
- const currentVersion = import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1525
- if (STANDARD_BUMP_TYPES.includes(specifiedType) && (import_semver3.default.prerelease(currentVersion) || normalizedPrereleaseId)) {
1567
+ const currentVersion = getCurrentVersionFromSource2();
1568
+ const isCurrentPrerelease = import_semver3.default.prerelease(currentVersion);
1569
+ const explicitlyRequestedPrerelease = config.isPrerelease;
1570
+ if (STANDARD_BUMP_TYPES.includes(specifiedType) && (isCurrentPrerelease || explicitlyRequestedPrerelease)) {
1571
+ const prereleaseId2 = explicitlyRequestedPrerelease || isCurrentPrerelease ? normalizedPrereleaseId : void 0;
1526
1572
  log(
1527
- normalizedPrereleaseId ? `Creating prerelease version with identifier '${normalizedPrereleaseId}' using ${specifiedType}` : `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
1573
+ explicitlyRequestedPrerelease ? `Creating prerelease version with identifier '${prereleaseId2}' using ${specifiedType}` : `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
1528
1574
  "debug"
1529
1575
  );
1530
- return bumpVersion(currentVersion, specifiedType, normalizedPrereleaseId);
1576
+ return bumpVersion(currentVersion, specifiedType, prereleaseId2);
1531
1577
  }
1532
- return bumpVersion(currentVersion, specifiedType, normalizedPrereleaseId);
1578
+ const isPrereleaseBumpType = ["prerelease", "premajor", "preminor", "prepatch"].includes(
1579
+ specifiedType
1580
+ );
1581
+ const prereleaseId = config.isPrerelease || isPrereleaseBumpType ? normalizedPrereleaseId : void 0;
1582
+ return bumpVersion(currentVersion, specifiedType, prereleaseId);
1533
1583
  }
1534
1584
  if (branchPattern && branchPattern.length > 0) {
1535
1585
  const currentBranch = getCurrentBranch();
@@ -1551,19 +1601,13 @@ To fix this mismatch:
1551
1601
  }
1552
1602
  }
1553
1603
  if (branchVersionType) {
1554
- if (hasNoTags) {
1555
- return getPackageVersionFallback(
1556
- pkgPath,
1557
- name,
1558
- branchVersionType,
1559
- normalizedPrereleaseId,
1560
- initialVersion
1561
- );
1562
- }
1563
- const cleanedTag = import_semver3.default.clean(latestTag) || latestTag;
1564
- const currentVersion = import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1604
+ const currentVersion = getCurrentVersionFromSource2();
1565
1605
  log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
1566
- return bumpVersion(currentVersion, branchVersionType, normalizedPrereleaseId);
1606
+ const isPrereleaseBumpType = ["prerelease", "premajor", "preminor", "prepatch"].includes(
1607
+ branchVersionType
1608
+ );
1609
+ const prereleaseId = config.isPrerelease || isPrereleaseBumpType ? normalizedPrereleaseId : void 0;
1610
+ return bumpVersion(currentVersion, branchVersionType, prereleaseId);
1567
1611
  }
1568
1612
  }
1569
1613
  try {
@@ -1571,36 +1615,39 @@ To fix this mismatch:
1571
1615
  bumper.loadPreset(preset);
1572
1616
  const recommendedBump = await bumper.bump();
1573
1617
  const releaseTypeFromCommits = recommendedBump && "releaseType" in recommendedBump ? recommendedBump.releaseType : void 0;
1574
- if (hasNoTags) {
1575
- if (releaseTypeFromCommits) {
1576
- return getPackageVersionFallback(
1577
- pkgPath,
1578
- name,
1579
- releaseTypeFromCommits,
1580
- normalizedPrereleaseId,
1581
- initialVersion
1618
+ const currentVersion = getCurrentVersionFromSource2();
1619
+ if (versionSource && versionSource.source === "git") {
1620
+ const checkPath = pkgPath || (0, import_node_process3.cwd)();
1621
+ const commitsLength = getCommitsLength(checkPath, versionSource.version);
1622
+ if (commitsLength === 0) {
1623
+ log(
1624
+ `No new commits found for ${name || "project"} since ${versionSource.version}, skipping version bump`,
1625
+ "info"
1582
1626
  );
1627
+ return "";
1583
1628
  }
1584
- return initialVersion;
1585
- }
1586
- const checkPath = pkgPath || (0, import_node_process3.cwd)();
1587
- const commitsLength = getCommitsLength(checkPath);
1588
- if (commitsLength === 0) {
1629
+ } else if (versionSource && versionSource.source === "package") {
1589
1630
  log(
1590
- `No new commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1591
- "info"
1631
+ `Using package version ${versionSource.version} as base, letting conventional commits determine bump necessity`,
1632
+ "debug"
1592
1633
  );
1593
- return "";
1594
1634
  }
1595
1635
  if (!releaseTypeFromCommits) {
1596
- log(
1597
- `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1598
- "info"
1599
- );
1636
+ if (latestTag && latestTag.trim() !== "") {
1637
+ log(
1638
+ `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1639
+ "info"
1640
+ );
1641
+ } else {
1642
+ log(`No relevant commits found for ${name || "project"}, skipping version bump`, "info");
1643
+ }
1600
1644
  return "";
1601
1645
  }
1602
- const currentVersion = import_semver3.default.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1603
- return bumpVersion(currentVersion, releaseTypeFromCommits, normalizedPrereleaseId);
1646
+ const isPrereleaseBumpType = ["prerelease", "premajor", "preminor", "prepatch"].includes(
1647
+ releaseTypeFromCommits
1648
+ );
1649
+ const prereleaseId = config.isPrerelease || isPrereleaseBumpType ? normalizedPrereleaseId : void 0;
1650
+ return bumpVersion(currentVersion, releaseTypeFromCommits, prereleaseId);
1604
1651
  } catch (error) {
1605
1652
  log(`Failed to calculate version for ${name || "project"}`, "error");
1606
1653
  console.error(error);
@@ -1620,40 +1667,6 @@ To fix this mismatch:
1620
1667
  throw error;
1621
1668
  }
1622
1669
  }
1623
- function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentifier, initialVersion) {
1624
- const packageDir = pkgPath || (0, import_node_process3.cwd)();
1625
- const manifestResult = getVersionFromManifests(packageDir);
1626
- if (manifestResult.manifestFound && manifestResult.version) {
1627
- log(
1628
- `No tags found for ${name || "package"}, using ${manifestResult.manifestType} version: ${manifestResult.version} as base`,
1629
- "info"
1630
- );
1631
- return calculateNextVersion(
1632
- manifestResult.version,
1633
- manifestResult.manifestType || "manifest",
1634
- name,
1635
- releaseType,
1636
- prereleaseIdentifier,
1637
- initialVersion
1638
- );
1639
- }
1640
- throwIfNoManifestsFound(packageDir);
1641
- }
1642
- function calculateNextVersion(version, manifestType, name, releaseType, prereleaseIdentifier, initialVersion) {
1643
- log(
1644
- `No tags found for ${name || "package"}, using ${manifestType} version: ${version} as base`,
1645
- "info"
1646
- );
1647
- if (STANDARD_BUMP_TYPES.includes(releaseType) && (import_semver3.default.prerelease(version) || prereleaseIdentifier)) {
1648
- log(
1649
- prereleaseIdentifier ? `Creating prerelease version with identifier '${prereleaseIdentifier}' using ${releaseType}` : `Cleaning prerelease identifier from ${version} for ${releaseType} bump`,
1650
- "debug"
1651
- );
1652
- return bumpVersion(version, releaseType, prereleaseIdentifier);
1653
- }
1654
- const result = bumpVersion(version, releaseType, prereleaseIdentifier);
1655
- return result || initialVersion;
1656
- }
1657
1670
 
1658
1671
  // src/utils/packageMatching.ts
1659
1672
  var import_micromatch2 = __toESM(require("micromatch"), 1);
@@ -1803,14 +1816,14 @@ var PackageProcessor = class {
1803
1816
  try {
1804
1817
  let revisionRange;
1805
1818
  if (latestTag) {
1806
- try {
1807
- (0, import_node_child_process4.execSync)(`git rev-parse --verify "${latestTag}"`, {
1808
- cwd: pkgPath,
1809
- stdio: "ignore"
1810
- });
1819
+ const verification = verifyTag(latestTag, pkgPath);
1820
+ if (verification.exists && verification.reachable) {
1811
1821
  revisionRange = `${latestTag}..HEAD`;
1812
- } catch {
1813
- log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1822
+ } else {
1823
+ log(
1824
+ `Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`,
1825
+ "debug"
1826
+ );
1814
1827
  revisionRange = "HEAD";
1815
1828
  }
1816
1829
  } else {
@@ -2179,7 +2192,7 @@ function createSingleStrategy(config) {
2179
2192
  let revisionRange;
2180
2193
  if (latestTag) {
2181
2194
  try {
2182
- (0, import_node_child_process5.execSync)(`git rev-parse --verify "${latestTag}"`, {
2195
+ (0, import_node_child_process4.execSync)(`git rev-parse --verify "${latestTag}"`, {
2183
2196
  cwd: pkgPath,
2184
2197
  stdio: "ignore"
2185
2198
  });
@@ -2337,11 +2350,10 @@ function createStrategyMap(config) {
2337
2350
  // src/core/versionEngine.ts
2338
2351
  var VersionEngine = class {
2339
2352
  config;
2340
- jsonMode;
2341
2353
  workspaceCache = null;
2342
2354
  strategies;
2343
2355
  currentStrategy;
2344
- constructor(config, jsonMode = false) {
2356
+ constructor(config, _jsonMode = false) {
2345
2357
  if (!config) {
2346
2358
  throw createVersionError("CONFIG_REQUIRED" /* CONFIG_REQUIRED */);
2347
2359
  }
@@ -2350,7 +2362,6 @@ var VersionEngine = class {
2350
2362
  log("No preset specified, using default: conventional-commits", "warning");
2351
2363
  }
2352
2364
  this.config = config;
2353
- this.jsonMode = jsonMode;
2354
2365
  this.strategies = createStrategyMap(config);
2355
2366
  this.currentStrategy = createStrategy(config);
2356
2367
  }
@@ -2466,18 +2477,31 @@ async function run() {
2466
2477
  program.command("version", { isDefault: true }).description("Version a package or packages based on configuration").option(
2467
2478
  "-c, --config <path>",
2468
2479
  "Path to config file (defaults to version.config.json in current directory)"
2469
- ).option("-d, --dry-run", "Dry run (no changes made)", false).option("-b, --bump <type>", "Specify bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --synced", "Use synchronized versioning across all packages").option("-j, --json", "Output results as JSON", false).option("-t, --target <packages>", "Comma-delimited list of package names to target").action(async (options) => {
2480
+ ).option("-d, --dry-run", "Dry run (no changes made)", false).option("-b, --bump <type>", "Specify bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --synced", "Use synchronized versioning across all packages").option("-j, --json", "Output results as JSON", false).option("-t, --target <packages>", "Comma-delimited list of package names to target").option("--project-dir <path>", "Project directory to run commands in", process.cwd()).action(async (options) => {
2470
2481
  if (options.json) {
2471
2482
  enableJsonOutput(options.dryRun);
2472
2483
  }
2473
2484
  try {
2485
+ const originalCwd = process.cwd();
2486
+ if (options.projectDir && options.projectDir !== originalCwd) {
2487
+ try {
2488
+ process.chdir(options.projectDir);
2489
+ log(`Changed working directory to: ${options.projectDir}`, "debug");
2490
+ } catch (error) {
2491
+ throw new Error(
2492
+ `Failed to change to directory "${options.projectDir}": ${error instanceof Error ? error.message : String(error)}`
2493
+ );
2494
+ }
2495
+ }
2474
2496
  const config = await loadConfig(options.config);
2475
2497
  log(`Loaded configuration from ${options.config || "version.config.json"}`, "info");
2476
2498
  if (options.dryRun) config.dryRun = true;
2477
2499
  if (options.synced) config.synced = true;
2478
2500
  if (options.bump) config.type = options.bump;
2479
- if (options.prerelease)
2501
+ if (options.prerelease) {
2480
2502
  config.prereleaseIdentifier = options.prerelease === true ? "next" : options.prerelease;
2503
+ config.isPrerelease = true;
2504
+ }
2481
2505
  const cliTargets = options.target ? options.target.split(",").map((t) => t.trim()) : [];
2482
2506
  const engine = new VersionEngine(config, !!options.json);
2483
2507
  const pkgsResult = await engine.getWorkspacePackages();
package/dist/index.js CHANGED
@@ -439,7 +439,7 @@ function getAllVersionTags(since, versionPrefix = "v") {
439
439
  const allTags = tagOutput.split("\n").filter((tag) => !!tag);
440
440
  let filteredTags = allTags;
441
441
  if (since) {
442
- const sinceIndex = allTags.findIndex((tag) => tag === since);
442
+ const sinceIndex = allTags.indexOf(since);
443
443
  if (sinceIndex >= 0) {
444
444
  filteredTags = allTags.slice(sinceIndex);
445
445
  } else {
@@ -523,7 +523,7 @@ async function regenerateChangelog(options) {
523
523
  const allTagsCmd = `git tag --list "${versionPrefix}*" --sort=creatordate`;
524
524
  const allTagsOutput = execSync2(allTagsCmd, { encoding: "utf8" }).trim();
525
525
  const allTags = allTagsOutput.split("\n").filter((tag) => !!tag);
526
- const sinceIndex = allTags.findIndex((tag) => tag === since);
526
+ const sinceIndex = allTags.indexOf(since);
527
527
  const actualPreviousTag = sinceIndex > 0 ? allTags[sinceIndex - 1] : null;
528
528
  if (actualPreviousTag) {
529
529
  tagRange = `${actualPreviousTag}..${currentTag.tag}`;
@@ -661,8 +661,12 @@ function filterPackagesByConfig(packages, configTargets, workspaceRoot) {
661
661
  for (const target of configTargets) {
662
662
  const dirMatches = filterByDirectoryPattern(packages, target, workspaceRoot);
663
663
  const nameMatches = filterByPackageNamePattern(packages, target);
664
- dirMatches.forEach((pkg) => matchedPackages.add(pkg));
665
- nameMatches.forEach((pkg) => matchedPackages.add(pkg));
664
+ for (const pkg of dirMatches) {
665
+ matchedPackages.add(pkg);
666
+ }
667
+ for (const pkg of nameMatches) {
668
+ matchedPackages.add(pkg);
669
+ }
666
670
  }
667
671
  return Array.from(matchedPackages);
668
672
  }
@@ -726,7 +730,7 @@ function matchesPackageNamePattern(packageName, pattern) {
726
730
  }
727
731
 
728
732
  // src/core/versionStrategies.ts
729
- import { execSync as execSync5 } from "child_process";
733
+ import { execSync as execSync4 } from "child_process";
730
734
  import fs9 from "fs";
731
735
  import * as path8 from "path";
732
736
 
@@ -1008,9 +1012,14 @@ function formatCommitMessage(template, version, packageName, additionalContext)
1008
1012
  }
1009
1013
 
1010
1014
  // src/git/tagsAndBranches.ts
1011
- function getCommitsLength(pkgRoot) {
1015
+ function getCommitsLength(pkgRoot, sinceTag) {
1012
1016
  try {
1013
- const gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1017
+ let gitCommand;
1018
+ if (sinceTag && sinceTag.trim() !== "") {
1019
+ gitCommand = `git rev-list --count ${sinceTag}..HEAD ${pkgRoot}`;
1020
+ } else {
1021
+ gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1022
+ }
1014
1023
  const amount = execSync3(gitCommand).toString().trim();
1015
1024
  return Number(amount);
1016
1025
  } catch (error) {
@@ -1284,7 +1293,6 @@ function updatePackageVersion(packagePath, version) {
1284
1293
  }
1285
1294
 
1286
1295
  // src/package/packageProcessor.ts
1287
- import { execSync as execSync4 } from "child_process";
1288
1296
  import * as fs8 from "fs";
1289
1297
  import path7 from "path";
1290
1298
  import { exit } from "process";
@@ -1343,18 +1351,41 @@ function getVersionFromManifests(packageDir) {
1343
1351
  manifestType: null
1344
1352
  };
1345
1353
  }
1346
- function throwIfNoManifestsFound(packageDir) {
1347
- const packageJsonPath = path6.join(packageDir, "package.json");
1348
- const cargoTomlPath = path6.join(packageDir, "Cargo.toml");
1349
- throw new Error(
1350
- `Neither package.json nor Cargo.toml found at ${packageDir}. Checked paths: ${packageJsonPath}, ${cargoTomlPath}. Cannot determine version.`
1351
- );
1352
- }
1353
1354
 
1354
1355
  // src/utils/versionUtils.ts
1355
1356
  import fs7 from "fs";
1356
1357
  import semver2 from "semver";
1357
1358
  import * as TOML2 from "smol-toml";
1359
+
1360
+ // src/git/tagVerification.ts
1361
+ function verifyTag(tagName, cwd5) {
1362
+ if (!tagName || tagName.trim() === "") {
1363
+ return { exists: false, reachable: false, error: "Empty tag name" };
1364
+ }
1365
+ try {
1366
+ execSync3(`git rev-parse --verify "${tagName}"`, {
1367
+ cwd: cwd5,
1368
+ stdio: "ignore"
1369
+ });
1370
+ return { exists: true, reachable: true };
1371
+ } catch (error) {
1372
+ const errorMessage = error instanceof Error ? error.message : String(error);
1373
+ if (errorMessage.includes("unknown revision") || errorMessage.includes("bad revision") || errorMessage.includes("No such ref")) {
1374
+ return {
1375
+ exists: false,
1376
+ reachable: false,
1377
+ error: `Tag '${tagName}' not found in repository`
1378
+ };
1379
+ }
1380
+ return {
1381
+ exists: false,
1382
+ reachable: false,
1383
+ error: `Git error: ${errorMessage}`
1384
+ };
1385
+ }
1386
+ }
1387
+
1388
+ // src/utils/versionUtils.ts
1358
1389
  var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
1359
1390
  function normalizePrereleaseIdentifier(prereleaseIdentifier, config) {
1360
1391
  if (prereleaseIdentifier === true) {
@@ -1388,6 +1419,59 @@ function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
1388
1419
  }
1389
1420
  return semver2.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
1390
1421
  }
1422
+ async function getBestVersionSource(tagName, packageVersion, cwd5) {
1423
+ if (!(tagName == null ? void 0 : tagName.trim())) {
1424
+ 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" };
1425
+ }
1426
+ const verification = verifyTag(tagName, cwd5);
1427
+ if (!verification.exists || !verification.reachable) {
1428
+ if (packageVersion) {
1429
+ log(
1430
+ `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
1431
+ "warning"
1432
+ );
1433
+ return { source: "package", version: packageVersion, reason: "Git tag unreachable" };
1434
+ }
1435
+ log(
1436
+ `Git tag '${tagName}' unreachable and no package version available, using initial version`,
1437
+ "warning"
1438
+ );
1439
+ return {
1440
+ source: "initial",
1441
+ version: "0.1.0",
1442
+ reason: "Git tag unreachable, no package version"
1443
+ };
1444
+ }
1445
+ if (!packageVersion) {
1446
+ return {
1447
+ source: "git",
1448
+ version: tagName,
1449
+ reason: "Git tag exists, no package version to compare"
1450
+ };
1451
+ }
1452
+ try {
1453
+ const cleanTagVersion = tagName.replace(/^.*?([0-9])/, "$1");
1454
+ const cleanPackageVersion = packageVersion;
1455
+ if (semver2.gt(cleanPackageVersion, cleanTagVersion)) {
1456
+ log(
1457
+ `Package version ${packageVersion} is newer than git tag ${tagName}, using package version`,
1458
+ "info"
1459
+ );
1460
+ return { source: "package", version: packageVersion, reason: "Package version is newer" };
1461
+ }
1462
+ if (semver2.gt(cleanTagVersion, cleanPackageVersion)) {
1463
+ log(
1464
+ `Git tag ${tagName} is newer than package version ${packageVersion}, using git tag`,
1465
+ "info"
1466
+ );
1467
+ return { source: "git", version: tagName, reason: "Git tag is newer" };
1468
+ }
1469
+ return { source: "git", version: tagName, reason: "Versions equal, using git tag" };
1470
+ } catch (error) {
1471
+ log(`Failed to compare versions, defaulting to git tag: ${error}`, "warning");
1472
+ return { source: "git", version: tagName, reason: "Version comparison failed" };
1473
+ }
1474
+ }
1391
1475
 
1392
1476
  // src/core/versionCalculator.ts
1393
1477
  async function calculateVersion(config, options) {
@@ -1419,84 +1503,50 @@ async function calculateVersion(config, options) {
1419
1503
  return `${packageName}@${prefix}`;
1420
1504
  }, escapeRegExp3 = function(string) {
1421
1505
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1506
+ }, getCurrentVersionFromSource2 = function() {
1507
+ if (!versionSource) {
1508
+ if (hasNoTags) {
1509
+ return initialVersion;
1510
+ }
1511
+ const cleanedTag = semver3.clean(latestTag) || latestTag;
1512
+ return semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1513
+ }
1514
+ if (versionSource.source === "git") {
1515
+ const cleanedTag = semver3.clean(versionSource.version) || versionSource.version;
1516
+ return semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1517
+ }
1518
+ return versionSource.version;
1422
1519
  };
1423
- var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3;
1520
+ var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3, getCurrentVersionFromSource = getCurrentVersionFromSource2;
1424
1521
  const originalPrefix = versionPrefix || "";
1425
1522
  const tagSearchPattern = determineTagSearchPattern2(name, originalPrefix);
1426
1523
  const escapedTagPattern = escapeRegExp3(tagSearchPattern);
1427
- if (!hasNoTags && pkgPath) {
1524
+ let versionSource;
1525
+ if (pkgPath) {
1428
1526
  const packageDir = pkgPath || cwd3();
1429
1527
  const manifestResult = getVersionFromManifests(packageDir);
1430
- if (manifestResult.manifestFound && manifestResult.version) {
1431
- const cleanedTag = semver3.clean(latestTag) || latestTag;
1432
- const tagVersion = semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1433
- const packageVersion = manifestResult.version;
1434
- if (semver3.gt(packageVersion, tagVersion)) {
1435
- log(
1436
- `Warning: Version mismatch detected!
1437
- \u2022 ${manifestResult.manifestType} version: ${packageVersion}
1438
- \u2022 Latest Git tag version: ${tagVersion} (from ${latestTag})
1439
- \u2022 Package version is AHEAD of Git tags
1440
-
1441
- This usually happens when:
1442
- \u2022 A version was released but the tag wasn't pushed to the remote repository
1443
- \u2022 The ${manifestResult.manifestType} was manually updated without creating a corresponding tag
1444
- \u2022 You're running in CI and the latest tag isn't available yet
1445
-
1446
- The tool will use the Git tag version (${tagVersion}) as the base for calculation.
1447
- Expected next version will be based on ${tagVersion}, not ${packageVersion}.
1448
-
1449
- To fix this mismatch:
1450
- \u2022 Push missing tags: git push origin --tags
1451
- \u2022 Or use package version as base by ensuring tags are up to date`,
1452
- "warning"
1453
- );
1454
- } else if (semver3.gt(tagVersion, packageVersion)) {
1455
- log(
1456
- `Warning: Version mismatch detected!
1457
- \u2022 ${manifestResult.manifestType} version: ${packageVersion}
1458
- \u2022 Latest Git tag version: ${tagVersion} (from ${latestTag})
1459
- \u2022 Git tag version is AHEAD of package version
1460
-
1461
- This usually happens when:
1462
- \u2022 A release was tagged but the ${manifestResult.manifestType} wasn't updated
1463
- \u2022 You're on an older branch that hasn't been updated with the latest version
1464
- \u2022 Automated release process created tags but didn't update manifest files
1465
- \u2022 You pulled tags but not the corresponding commits that update the package version
1466
-
1467
- The tool will use the Git tag version (${tagVersion}) as the base for calculation.
1468
- This will likely result in a version that's already been released.
1469
-
1470
- To fix this mismatch:
1471
- \u2022 Update ${manifestResult.manifestType}: Set version to ${tagVersion} or higher
1472
- \u2022 Or checkout the branch/commit that corresponds to the tag
1473
- \u2022 Or ensure your branch is up to date with the latest changes`,
1474
- "warning"
1475
- );
1476
- }
1477
- }
1528
+ const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
1529
+ versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir);
1530
+ log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
1478
1531
  }
1479
1532
  const specifiedType = type;
1480
1533
  if (specifiedType) {
1481
- if (hasNoTags) {
1482
- return getPackageVersionFallback(
1483
- pkgPath,
1484
- name,
1485
- specifiedType,
1486
- normalizedPrereleaseId,
1487
- initialVersion
1488
- );
1489
- }
1490
- const cleanedTag = semver3.clean(latestTag) || latestTag;
1491
- const currentVersion = semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1492
- if (STANDARD_BUMP_TYPES.includes(specifiedType) && (semver3.prerelease(currentVersion) || normalizedPrereleaseId)) {
1534
+ const currentVersion = getCurrentVersionFromSource2();
1535
+ const isCurrentPrerelease = semver3.prerelease(currentVersion);
1536
+ const explicitlyRequestedPrerelease = config.isPrerelease;
1537
+ if (STANDARD_BUMP_TYPES.includes(specifiedType) && (isCurrentPrerelease || explicitlyRequestedPrerelease)) {
1538
+ const prereleaseId2 = explicitlyRequestedPrerelease || isCurrentPrerelease ? normalizedPrereleaseId : void 0;
1493
1539
  log(
1494
- normalizedPrereleaseId ? `Creating prerelease version with identifier '${normalizedPrereleaseId}' using ${specifiedType}` : `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
1540
+ explicitlyRequestedPrerelease ? `Creating prerelease version with identifier '${prereleaseId2}' using ${specifiedType}` : `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
1495
1541
  "debug"
1496
1542
  );
1497
- return bumpVersion(currentVersion, specifiedType, normalizedPrereleaseId);
1543
+ return bumpVersion(currentVersion, specifiedType, prereleaseId2);
1498
1544
  }
1499
- return bumpVersion(currentVersion, specifiedType, normalizedPrereleaseId);
1545
+ const isPrereleaseBumpType = ["prerelease", "premajor", "preminor", "prepatch"].includes(
1546
+ specifiedType
1547
+ );
1548
+ const prereleaseId = config.isPrerelease || isPrereleaseBumpType ? normalizedPrereleaseId : void 0;
1549
+ return bumpVersion(currentVersion, specifiedType, prereleaseId);
1500
1550
  }
1501
1551
  if (branchPattern && branchPattern.length > 0) {
1502
1552
  const currentBranch = getCurrentBranch();
@@ -1518,19 +1568,13 @@ To fix this mismatch:
1518
1568
  }
1519
1569
  }
1520
1570
  if (branchVersionType) {
1521
- if (hasNoTags) {
1522
- return getPackageVersionFallback(
1523
- pkgPath,
1524
- name,
1525
- branchVersionType,
1526
- normalizedPrereleaseId,
1527
- initialVersion
1528
- );
1529
- }
1530
- const cleanedTag = semver3.clean(latestTag) || latestTag;
1531
- const currentVersion = semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1571
+ const currentVersion = getCurrentVersionFromSource2();
1532
1572
  log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
1533
- return bumpVersion(currentVersion, branchVersionType, normalizedPrereleaseId);
1573
+ const isPrereleaseBumpType = ["prerelease", "premajor", "preminor", "prepatch"].includes(
1574
+ branchVersionType
1575
+ );
1576
+ const prereleaseId = config.isPrerelease || isPrereleaseBumpType ? normalizedPrereleaseId : void 0;
1577
+ return bumpVersion(currentVersion, branchVersionType, prereleaseId);
1534
1578
  }
1535
1579
  }
1536
1580
  try {
@@ -1538,36 +1582,39 @@ To fix this mismatch:
1538
1582
  bumper.loadPreset(preset);
1539
1583
  const recommendedBump = await bumper.bump();
1540
1584
  const releaseTypeFromCommits = recommendedBump && "releaseType" in recommendedBump ? recommendedBump.releaseType : void 0;
1541
- if (hasNoTags) {
1542
- if (releaseTypeFromCommits) {
1543
- return getPackageVersionFallback(
1544
- pkgPath,
1545
- name,
1546
- releaseTypeFromCommits,
1547
- normalizedPrereleaseId,
1548
- initialVersion
1585
+ const currentVersion = getCurrentVersionFromSource2();
1586
+ if (versionSource && versionSource.source === "git") {
1587
+ const checkPath = pkgPath || cwd3();
1588
+ const commitsLength = getCommitsLength(checkPath, versionSource.version);
1589
+ if (commitsLength === 0) {
1590
+ log(
1591
+ `No new commits found for ${name || "project"} since ${versionSource.version}, skipping version bump`,
1592
+ "info"
1549
1593
  );
1594
+ return "";
1550
1595
  }
1551
- return initialVersion;
1552
- }
1553
- const checkPath = pkgPath || cwd3();
1554
- const commitsLength = getCommitsLength(checkPath);
1555
- if (commitsLength === 0) {
1596
+ } else if (versionSource && versionSource.source === "package") {
1556
1597
  log(
1557
- `No new commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1558
- "info"
1598
+ `Using package version ${versionSource.version} as base, letting conventional commits determine bump necessity`,
1599
+ "debug"
1559
1600
  );
1560
- return "";
1561
1601
  }
1562
1602
  if (!releaseTypeFromCommits) {
1563
- log(
1564
- `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1565
- "info"
1566
- );
1603
+ if (latestTag && latestTag.trim() !== "") {
1604
+ log(
1605
+ `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1606
+ "info"
1607
+ );
1608
+ } else {
1609
+ log(`No relevant commits found for ${name || "project"}, skipping version bump`, "info");
1610
+ }
1567
1611
  return "";
1568
1612
  }
1569
- const currentVersion = semver3.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1570
- return bumpVersion(currentVersion, releaseTypeFromCommits, normalizedPrereleaseId);
1613
+ const isPrereleaseBumpType = ["prerelease", "premajor", "preminor", "prepatch"].includes(
1614
+ releaseTypeFromCommits
1615
+ );
1616
+ const prereleaseId = config.isPrerelease || isPrereleaseBumpType ? normalizedPrereleaseId : void 0;
1617
+ return bumpVersion(currentVersion, releaseTypeFromCommits, prereleaseId);
1571
1618
  } catch (error) {
1572
1619
  log(`Failed to calculate version for ${name || "project"}`, "error");
1573
1620
  console.error(error);
@@ -1587,40 +1634,6 @@ To fix this mismatch:
1587
1634
  throw error;
1588
1635
  }
1589
1636
  }
1590
- function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentifier, initialVersion) {
1591
- const packageDir = pkgPath || cwd3();
1592
- const manifestResult = getVersionFromManifests(packageDir);
1593
- if (manifestResult.manifestFound && manifestResult.version) {
1594
- log(
1595
- `No tags found for ${name || "package"}, using ${manifestResult.manifestType} version: ${manifestResult.version} as base`,
1596
- "info"
1597
- );
1598
- return calculateNextVersion(
1599
- manifestResult.version,
1600
- manifestResult.manifestType || "manifest",
1601
- name,
1602
- releaseType,
1603
- prereleaseIdentifier,
1604
- initialVersion
1605
- );
1606
- }
1607
- throwIfNoManifestsFound(packageDir);
1608
- }
1609
- function calculateNextVersion(version, manifestType, name, releaseType, prereleaseIdentifier, initialVersion) {
1610
- log(
1611
- `No tags found for ${name || "package"}, using ${manifestType} version: ${version} as base`,
1612
- "info"
1613
- );
1614
- if (STANDARD_BUMP_TYPES.includes(releaseType) && (semver3.prerelease(version) || prereleaseIdentifier)) {
1615
- log(
1616
- prereleaseIdentifier ? `Creating prerelease version with identifier '${prereleaseIdentifier}' using ${releaseType}` : `Cleaning prerelease identifier from ${version} for ${releaseType} bump`,
1617
- "debug"
1618
- );
1619
- return bumpVersion(version, releaseType, prereleaseIdentifier);
1620
- }
1621
- const result = bumpVersion(version, releaseType, prereleaseIdentifier);
1622
- return result || initialVersion;
1623
- }
1624
1637
 
1625
1638
  // src/utils/packageMatching.ts
1626
1639
  import micromatch2 from "micromatch";
@@ -1770,14 +1783,14 @@ var PackageProcessor = class {
1770
1783
  try {
1771
1784
  let revisionRange;
1772
1785
  if (latestTag) {
1773
- try {
1774
- execSync4(`git rev-parse --verify "${latestTag}"`, {
1775
- cwd: pkgPath,
1776
- stdio: "ignore"
1777
- });
1786
+ const verification = verifyTag(latestTag, pkgPath);
1787
+ if (verification.exists && verification.reachable) {
1778
1788
  revisionRange = `${latestTag}..HEAD`;
1779
- } catch {
1780
- log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1789
+ } else {
1790
+ log(
1791
+ `Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`,
1792
+ "debug"
1793
+ );
1781
1794
  revisionRange = "HEAD";
1782
1795
  }
1783
1796
  } else {
@@ -2146,7 +2159,7 @@ function createSingleStrategy(config) {
2146
2159
  let revisionRange;
2147
2160
  if (latestTag) {
2148
2161
  try {
2149
- execSync5(`git rev-parse --verify "${latestTag}"`, {
2162
+ execSync4(`git rev-parse --verify "${latestTag}"`, {
2150
2163
  cwd: pkgPath,
2151
2164
  stdio: "ignore"
2152
2165
  });
@@ -2304,11 +2317,10 @@ function createStrategyMap(config) {
2304
2317
  // src/core/versionEngine.ts
2305
2318
  var VersionEngine = class {
2306
2319
  config;
2307
- jsonMode;
2308
2320
  workspaceCache = null;
2309
2321
  strategies;
2310
2322
  currentStrategy;
2311
- constructor(config, jsonMode = false) {
2323
+ constructor(config, _jsonMode = false) {
2312
2324
  if (!config) {
2313
2325
  throw createVersionError("CONFIG_REQUIRED" /* CONFIG_REQUIRED */);
2314
2326
  }
@@ -2317,7 +2329,6 @@ var VersionEngine = class {
2317
2329
  log("No preset specified, using default: conventional-commits", "warning");
2318
2330
  }
2319
2331
  this.config = config;
2320
- this.jsonMode = jsonMode;
2321
2332
  this.strategies = createStrategyMap(config);
2322
2333
  this.currentStrategy = createStrategy(config);
2323
2334
  }
@@ -2432,18 +2443,31 @@ async function run() {
2432
2443
  program.command("version", { isDefault: true }).description("Version a package or packages based on configuration").option(
2433
2444
  "-c, --config <path>",
2434
2445
  "Path to config file (defaults to version.config.json in current directory)"
2435
- ).option("-d, --dry-run", "Dry run (no changes made)", false).option("-b, --bump <type>", "Specify bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --synced", "Use synchronized versioning across all packages").option("-j, --json", "Output results as JSON", false).option("-t, --target <packages>", "Comma-delimited list of package names to target").action(async (options) => {
2446
+ ).option("-d, --dry-run", "Dry run (no changes made)", false).option("-b, --bump <type>", "Specify bump type (patch|minor|major)").option("-p, --prerelease [identifier]", "Create prerelease version").option("-s, --synced", "Use synchronized versioning across all packages").option("-j, --json", "Output results as JSON", false).option("-t, --target <packages>", "Comma-delimited list of package names to target").option("--project-dir <path>", "Project directory to run commands in", process.cwd()).action(async (options) => {
2436
2447
  if (options.json) {
2437
2448
  enableJsonOutput(options.dryRun);
2438
2449
  }
2439
2450
  try {
2451
+ const originalCwd = process.cwd();
2452
+ if (options.projectDir && options.projectDir !== originalCwd) {
2453
+ try {
2454
+ process.chdir(options.projectDir);
2455
+ log(`Changed working directory to: ${options.projectDir}`, "debug");
2456
+ } catch (error) {
2457
+ throw new Error(
2458
+ `Failed to change to directory "${options.projectDir}": ${error instanceof Error ? error.message : String(error)}`
2459
+ );
2460
+ }
2461
+ }
2440
2462
  const config = await loadConfig(options.config);
2441
2463
  log(`Loaded configuration from ${options.config || "version.config.json"}`, "info");
2442
2464
  if (options.dryRun) config.dryRun = true;
2443
2465
  if (options.synced) config.synced = true;
2444
2466
  if (options.bump) config.type = options.bump;
2445
- if (options.prerelease)
2467
+ if (options.prerelease) {
2446
2468
  config.prereleaseIdentifier = options.prerelease === true ? "next" : options.prerelease;
2469
+ config.isPrerelease = true;
2470
+ }
2447
2471
  const cliTargets = options.target ? options.target.split(",").map((t) => t.trim()) : [];
2448
2472
  const engine = new VersionEngine(config, !!options.json);
2449
2473
  const pkgsResult = await engine.getWorkspacePackages();
@@ -180,4 +180,5 @@ Each CI system might have slightly different syntax, so check your CI provider's
180
180
  2. **Use the `fetch-depth: 0`** option in GitHub Actions (or equivalent in other CIs) to ensure access to the full Git history
181
181
  3. **Store the JSON output** as a build artifact for debugging and auditing
182
182
  4. **Consider dry runs** in your preview/staging branches to validate version changes before they're applied
183
- 5. **Be mindful of Git credentials** - ensure your CI has proper permissions for creating commits and tags
183
+ 5. **Use `--project-dir`** when running from a different directory than your project root
184
+ 6. **Be mindful of Git credentials** - ensure your CI has proper permissions for creating commits and tags
@@ -52,7 +52,14 @@ You can configure the preferred format in your `version.config.json`:
52
52
  For projects with existing history, you can regenerate a complete changelog from scratch using the CLI:
53
53
 
54
54
  ```bash
55
+ # Generate changelog in current directory
55
56
  npx package-versioner changelog --regenerate
57
+
58
+ # Generate changelog in a specific directory
59
+ npx package-versioner changelog --regenerate --project-dir /path/to/project
60
+
61
+ # Customize output path and format
62
+ npx package-versioner changelog --regenerate --output CHANGELOG.md --format keep-a-changelog
56
63
  ```
57
64
 
58
65
  This will scan your entire git history and create a comprehensive changelog based on all version tags found in your repository.
@@ -65,78 +65,6 @@ You define patterns in the `branchPattern` array in `version.config.json`. Each
65
65
 
66
66
  This allows you to enforce version bumps based on your branching workflow (e.g., all branches starting with `feature/` result in a minor bump).
67
67
 
68
- ## Monorepo Versioning Modes
69
-
70
- While primarily used for single packages now, `package-versioner` retains options for monorepo workflows, controlled mainly by the `synced` flag in `version.config.json`.
71
-
72
- ### Synced Mode (`synced: true`)
73
-
74
- This is the default if the `synced` flag is present and true.
75
-
76
- - **Behaviour:** The tool calculates **one** version bump based on the overall history (or branch pattern). This single new version is applied to **all** packages within the repository (or just the root `package.json` if not a structured monorepo). A single Git tag is created.
77
- - **Tag Behaviour:**
78
- - In **multi-package monorepos**: Creates global tags like `v1.2.3` regardless of `packageSpecificTags` setting
79
- - In **single-package repositories**: Respects the `packageSpecificTags` setting - can create either `v1.2.3` or `package-name@v1.2.3`
80
- - **Use Case:** Suitable for monorepos where all packages are tightly coupled and released together with the same version number. Also the effective mode for single-package repositories.
81
-
82
- ### Async Mode (`synced: false`)
83
-
84
- *(Note: This mode relies heavily on monorepo tooling and structure, like `pnpm workspaces` and correctly configured package dependencies.)*
85
-
86
- - **Behaviour (Default - No `-t` flag):** The tool analyzes commits to determine which specific packages within the monorepo have changed since the last relevant commit/tag.
87
- - It calculates an appropriate version bump **independently for each changed package** based on the commits affecting that package.
88
- - Only the `package.json` files of the changed packages are updated.
89
- - A **single commit** is created grouping all the version bumps, using the commit message template. **No Git tags are created** in this mode.
90
- - **Use Case:** Suitable for monorepos where packages are versioned independently, but a single commit represents the batch of updates for traceability.
91
-
92
- - **Behaviour (Targeted - With `-t` flag):** When using the `-t, --target <targets>` flag:
93
- - Only the specified packages (respecting the `skip` list) are considered for versioning.
94
- - It calculates an appropriate version bump **independently for each targeted package** based on its commit history.
95
- - The `package.json` file of each successfully updated targeted package is modified.
96
- - An **individual Git tag** (e.g., `packageName@1.2.3`) is created **for each successfully updated package** immediately after its version is bumped.
97
- - Finally, a **single commit** is created including all the updated `package.json` files, using a summary commit message (e.g., `chore(release): pkg-a, pkg-b 1.2.3 [skip-ci]`).
98
- - **Important:** Only package-specific tags are created. The global tag (e.g., `v1.2.3`) is **not** automatically generated in this mode. If your release process (like GitHub Releases) depends on a global tag, you'll need to create it manually in your CI/CD script *after* `package-versioner` completes.
99
- - **Use Case:** Releasing specific packages independently while still tagging each released package individually.
100
-
101
- ## Prerelease Handling
102
-
103
- `package-versioner` provides flexible handling for prerelease versions, allowing both creation of prereleases and promotion to stable releases.
104
-
105
- ### Creating Prereleases
106
-
107
- Use the `--prerelease` flag with an identifier to create a prerelease version:
108
-
109
- ```bash
110
- # Create a beta prerelease
111
- npx package-versioner --bump minor --prerelease beta
112
- # Result: 1.0.0 -> 1.1.0-beta.0
113
- ```
114
-
115
- You can also set a default prerelease identifier in your `version.config.json`:
116
-
117
- ```json
118
- {
119
- "prereleaseIdentifier": "beta"
120
- }
121
- ```
122
-
123
- ### Promoting Prereleases to Stable Releases
124
-
125
- When using standard bump types (`major`, `minor`, `patch`) with the `--bump` flag on a prerelease version, `package-versioner` will automatically clean the prerelease identifier:
126
-
127
- ```bash
128
- # Starting from version 1.0.0-beta.1
129
- npx package-versioner --bump major
130
- # Result: 1.0.0-beta.1 -> 2.0.0 (not 2.0.0-beta.0)
131
- ```
132
-
133
- This intuitive behaviour means you don't need to use an empty prerelease identifier (`--prerelease ""`) to promote a prerelease to a stable version. Simply specify the standard bump type and the tool will automatically produce a clean version number.
134
-
135
- This applies to all standard bump types:
136
- - `--bump major`: 1.0.0-beta.1 -> 2.0.0
137
- - `--bump minor`: 1.0.0-beta.1 -> 1.1.0
138
- - `--bump patch`: 1.0.0-beta.1 -> 1.0.1
139
-
140
68
  ## Package Type Support
141
69
 
142
70
  `package-versioner` supports both JavaScript/TypeScript projects using `package.json` and Rust projects using `Cargo.toml`:
@@ -149,11 +77,6 @@ For JavaScript/TypeScript projects, the tool looks for and updates the `version`
149
77
 
150
78
  For Rust projects, the tool looks for and updates the `package.version` field in `Cargo.toml` files using the same versioning strategies.
151
79
 
152
- When no tags are found for a project, `package-versioner` will:
153
- 1. Look for the `version` in `package.json` if it exists
154
- 2. Look for the `package.version` in `Cargo.toml` if it exists
155
- 3. Fall back to the configured `initialVersion` (default: "0.1.0")
156
-
157
80
  ### Mixed Projects with Both Manifests
158
81
 
159
82
  When both `package.json` and `Cargo.toml` exist in the same directory, `package-versioner` will:
@@ -164,7 +87,48 @@ When both `package.json` and `Cargo.toml` exist in the same directory, `package-
164
87
 
165
88
  This allows you to maintain consistent versioning across JavaScript and Rust components in the same package.
166
89
 
167
- This dual support makes `package-versioner` suitable for both JavaScript/TypeScript and Rust repositories, as well as monorepos or projects containing both types of packages.
90
+ ## Version Source Selection
91
+
92
+ `package-versioner` uses a smart version source selection strategy to determine the base version for calculating the next version:
93
+
94
+ 1. First, it checks for Git tags:
95
+ - In normal mode: Uses the latest reachable tag, falling back to unreachable tags if needed
96
+ - In strict mode (`--strict-reachable`): Only uses reachable tags
97
+
98
+ 2. Then, it checks manifest files (package.json, Cargo.toml):
99
+ - Reads version from package.json if it exists
100
+ - Falls back to Cargo.toml if package.json doesn't exist or has no version
101
+
102
+ 3. Finally, it compares the versions:
103
+ - If both Git tag and manifest versions exist, it uses the newer version
104
+ - If the versions are equal, it prefers the Git tag for better history tracking
105
+ - If only one source has a version, it uses that
106
+ - If no version is found, it uses the default initial version (0.1.0)
107
+
108
+ This strategy ensures that:
109
+ - Version numbers never go backwards
110
+ - Git history is respected when possible
111
+ - Manifest files are considered as valid version sources
112
+ - The tool always has a valid base version to work from
113
+
114
+ For example:
115
+ ```
116
+ Scenario 1:
117
+ - Git tag: v1.0.0
118
+ - package.json: 1.1.0
119
+ Result: Uses 1.1.0 as base (package.json is newer)
120
+
121
+ Scenario 2:
122
+ - Git tag: v1.0.0
123
+ - package.json: 1.0.0
124
+ Result: Uses v1.0.0 as base (versions equal, prefer Git)
125
+
126
+ Scenario 3:
127
+ - Git tag: unreachable v2.0.0
128
+ - package.json: 1.0.0
129
+ Result: Uses 2.0.0 as base in normal mode (unreachable tag is newer)
130
+ Uses 1.0.0 as base in strict mode (unreachable tag ignored)
131
+ ```
168
132
 
169
133
  ## Package Targeting in Monorepos
170
134
 
@@ -430,3 +394,75 @@ For global commit messages, use templates without `${packageName}`:
430
394
  "commitMessage": "chore: release ${version}"
431
395
  }
432
396
  ```
397
+
398
+ ## Monorepo Versioning Modes
399
+
400
+ While primarily used for single packages now, `package-versioner` retains options for monorepo workflows, controlled mainly by the `synced` flag in `version.config.json`.
401
+
402
+ ### Synced Mode (`synced: true`)
403
+
404
+ This is the default if the `synced` flag is present and true.
405
+
406
+ - **Behaviour:** The tool calculates **one** version bump based on the overall history (or branch pattern). This single new version is applied to **all** packages within the repository (or just the root `package.json` if not a structured monorepo). A single Git tag is created.
407
+ - **Tag Behaviour:**
408
+ - In **multi-package monorepos**: Creates global tags like `v1.2.3` regardless of `packageSpecificTags` setting
409
+ - In **single-package repositories**: Respects the `packageSpecificTags` setting - can create either `v1.2.3` or `package-name@v1.2.3`
410
+ - **Use Case:** Suitable for monorepos where all packages are tightly coupled and released together with the same version number. Also the effective mode for single-package repositories.
411
+
412
+ ### Async Mode (`synced: false`)
413
+
414
+ *(Note: This mode relies heavily on monorepo tooling and structure, like `pnpm workspaces` and correctly configured package dependencies.)*
415
+
416
+ - **Behaviour (Default - No `-t` flag):** The tool analyzes commits to determine which specific packages within the monorepo have changed since the last relevant commit/tag.
417
+ - It calculates an appropriate version bump **independently for each changed package** based on the commits affecting that package.
418
+ - Only the `package.json` files of the changed packages are updated.
419
+ - A **single commit** is created grouping all the version bumps, using the commit message template. **No Git tags are created** in this mode.
420
+ - **Use Case:** Suitable for monorepos where packages are versioned independently, but a single commit represents the batch of updates for traceability.
421
+
422
+ - **Behaviour (Targeted - With `-t` flag):** When using the `-t, --target <targets>` flag:
423
+ - Only the specified packages (respecting the `skip` list) are considered for versioning.
424
+ - It calculates an appropriate version bump **independently for each targeted package** based on its commit history.
425
+ - The `package.json` file of each successfully updated targeted package is modified.
426
+ - An **individual Git tag** (e.g., `packageName@1.2.3`) is created **for each successfully updated package** immediately after its version is bumped.
427
+ - Finally, a **single commit** is created including all the updated `package.json` files, using a summary commit message (e.g., `chore(release): pkg-a, pkg-b 1.2.3 [skip-ci]`).
428
+ - **Important:** Only package-specific tags are created. The global tag (e.g., `v1.2.3`) is **not** automatically generated in this mode. If your release process (like GitHub Releases) depends on a global tag, you'll need to create it manually in your CI/CD script *after* `package-versioner` completes.
429
+ - **Use Case:** Releasing specific packages independently while still tagging each released package individually.
430
+
431
+ ## Prerelease Handling
432
+
433
+ `package-versioner` provides flexible handling for prerelease versions, allowing both creation of prereleases and promotion to stable releases.
434
+
435
+ ### Creating Prereleases
436
+
437
+ Use the `--prerelease` flag with an identifier to create a prerelease version:
438
+
439
+ ```bash
440
+ # Create a beta prerelease
441
+ npx package-versioner --bump minor --prerelease beta
442
+ # Result: 1.0.0 -> 1.1.0-beta.0
443
+ ```
444
+
445
+ You can also set a default prerelease identifier in your `version.config.json`:
446
+
447
+ ```json
448
+ {
449
+ "prereleaseIdentifier": "beta"
450
+ }
451
+ ```
452
+
453
+ ### Promoting Prereleases to Stable Releases
454
+
455
+ When using standard bump types (`major`, `minor`, `patch`) with the `--bump` flag on a prerelease version, `package-versioner` will automatically clean the prerelease identifier:
456
+
457
+ ```bash
458
+ # Starting from version 1.0.0-beta.1
459
+ npx package-versioner --bump major
460
+ # Result: 1.0.0-beta.1 -> 2.0.0 (not 2.0.0-beta.0)
461
+ ```
462
+
463
+ This intuitive behaviour means you don't need to use an empty prerelease identifier (`--prerelease ""`) to promote a prerelease to a stable version. Simply specify the standard bump type and the tool will automatically produce a clean version number.
464
+
465
+ This applies to all standard bump types:
466
+ - `--bump major`: 1.0.0-beta.1 -> 2.0.0
467
+ - `--bump minor`: 1.0.0-beta.1 -> 1.1.0
468
+ - `--bump patch`: 1.0.0-beta.1 -> 1.0.1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "package-versioner",
3
3
  "description": "A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.",
4
- "version": "0.8.4",
4
+ "version": "0.8.6",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -37,34 +37,34 @@
37
37
  ]
38
38
  },
39
39
  "devDependencies": {
40
- "@biomejs/biome": "^2.0.6",
40
+ "@biomejs/biome": "^2.2.2",
41
41
  "@types/figlet": "^1.5.5",
42
- "@types/node": "^24.0.10",
42
+ "@types/node": "^24.3.0",
43
43
  "@types/semver": "^7.3.13",
44
44
  "@vitest/coverage-v8": "^3.2.4",
45
- "cross-env": "^7.0.3",
45
+ "cross-env": "^10.0.0",
46
46
  "husky": "^9.1.7",
47
- "lint-staged": "^16.1.2",
47
+ "lint-staged": "^16.1.5",
48
48
  "tsup": "^8.5.0",
49
- "tsx": "^4.20.3",
50
- "typescript": "^5.8.3",
49
+ "tsx": "^4.20.5",
50
+ "typescript": "^5.9.2",
51
51
  "vitest": "^3.2.4"
52
52
  },
53
53
  "dependencies": {
54
- "@manypkg/get-packages": "^3.0.0",
54
+ "@manypkg/get-packages": "^3.1.0",
55
55
  "@types/micromatch": "^4.0.9",
56
- "chalk": "^5.4.1",
56
+ "chalk": "^5.6.0",
57
57
  "commander": "^14.0.0",
58
58
  "conventional-changelog-angular": "^8.0.0",
59
59
  "conventional-changelog-conventional-commits": "npm:conventional-changelog-conventionalcommits@^9.0.0",
60
- "conventional-changelog-conventionalcommits": "^9.0.0",
60
+ "conventional-changelog-conventionalcommits": "^9.1.0",
61
61
  "conventional-commits-filter": "^5.0.0",
62
62
  "conventional-recommended-bump": "^11.2.0",
63
- "figlet": "^1.8.1",
63
+ "figlet": "^1.8.2",
64
64
  "git-semver-tags": "^8.0.0",
65
65
  "micromatch": "^4.0.8",
66
66
  "semver": "^7.7.2",
67
- "smol-toml": "^1.4.1"
67
+ "smol-toml": "^1.4.2"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsup src/index.ts --format esm,cjs --dts",