package-versioner 0.8.3 → 0.8.5

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
 
@@ -159,7 +162,7 @@ Customize behaviour by creating a `version.config.json` file in your project roo
159
162
  #### Monorepo-Specific Options
160
163
  - `synced`: Whether all packages should be versioned together (default: true)
161
164
  - `skip`: Array of package names or patterns to exclude from versioning. Supports exact names, scope wildcards, path patterns, and global wildcards (e.g., ["@scope/package-a", "@scope/*", "packages/**/*"])
162
- - `packages`: Array of package names or patterns to target for versioning. Supports exact names, scope wildcards, and global wildcards (e.g., ["@scope/package-a", "@scope/*", "*"])
165
+ - `packages`: Array of package names or patterns to target for versioning. Supports exact names, scope wildcards, path patterns and global wildcards (e.g., ["@scope/package-a", "@scope/*", "*"])
163
166
  - `mainPackage`: Package name whose commit history should drive version determination
164
167
  - `packageSpecificTags`: Whether to enable package-specific tagging behaviour (default: false)
165
168
  - `updateInternalDependencies`: How to update internal dependencies ("patch", "minor", "major", or "inherit")
@@ -185,6 +188,15 @@ Target all packages within a specific scope:
185
188
  }
186
189
  ```
187
190
 
191
+ #### Path Patterns / Globs
192
+ Target all packages in a directory or matching a path pattern:
193
+ ```json
194
+ {
195
+ "packages": ["packages/**/*", "examples/**"]
196
+ }
197
+ ```
198
+ This will match all packages in nested directories under `packages/` or `examples/`.
199
+
188
200
  #### Global Wildcard
189
201
  Target all packages in the workspace:
190
202
  ```json
@@ -197,7 +209,7 @@ Target all packages in the workspace:
197
209
  Combine different pattern types:
198
210
  ```json
199
211
  {
200
- "packages": ["@mycompany/*", "@utils/logger", "legacy-package"]
212
+ "packages": ["@mycompany/*", "@utils/logger", "legacy-package", "packages/**/*"]
201
213
  }
202
214
  ```
203
215
 
package/dist/index.cjs CHANGED
@@ -759,7 +759,7 @@ function matchesPackageNamePattern(packageName, pattern) {
759
759
  }
760
760
 
761
761
  // src/core/versionStrategies.ts
762
- var import_node_child_process5 = require("child_process");
762
+ var import_node_child_process4 = require("child_process");
763
763
  var import_node_fs7 = __toESM(require("fs"), 1);
764
764
  var path8 = __toESM(require("path"), 1);
765
765
 
@@ -1041,9 +1041,14 @@ function formatCommitMessage(template, version, packageName, additionalContext)
1041
1041
  }
1042
1042
 
1043
1043
  // src/git/tagsAndBranches.ts
1044
- function getCommitsLength(pkgRoot) {
1044
+ function getCommitsLength(pkgRoot, sinceTag) {
1045
1045
  try {
1046
- const gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1046
+ let gitCommand;
1047
+ if (sinceTag && sinceTag.trim() !== "") {
1048
+ gitCommand = `git rev-list --count ${sinceTag}..HEAD ${pkgRoot}`;
1049
+ } else {
1050
+ gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1051
+ }
1047
1052
  const amount = execSync3(gitCommand).toString().trim();
1048
1053
  return Number(amount);
1049
1054
  } catch (error) {
@@ -1317,7 +1322,6 @@ function updatePackageVersion(packagePath, version) {
1317
1322
  }
1318
1323
 
1319
1324
  // src/package/packageProcessor.ts
1320
- var import_node_child_process4 = require("child_process");
1321
1325
  var fs8 = __toESM(require("fs"), 1);
1322
1326
  var import_node_path7 = __toESM(require("path"), 1);
1323
1327
  var import_node_process4 = require("process");
@@ -1376,18 +1380,41 @@ function getVersionFromManifests(packageDir) {
1376
1380
  manifestType: null
1377
1381
  };
1378
1382
  }
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
1383
 
1387
1384
  // src/utils/versionUtils.ts
1388
1385
  var import_node_fs6 = __toESM(require("fs"), 1);
1389
1386
  var import_semver2 = __toESM(require("semver"), 1);
1390
1387
  var TOML2 = __toESM(require("smol-toml"), 1);
1388
+
1389
+ // src/git/tagVerification.ts
1390
+ function verifyTag(tagName, cwd5) {
1391
+ if (!tagName || tagName.trim() === "") {
1392
+ return { exists: false, reachable: false, error: "Empty tag name" };
1393
+ }
1394
+ try {
1395
+ execSync3(`git rev-parse --verify "${tagName}"`, {
1396
+ cwd: cwd5,
1397
+ stdio: "ignore"
1398
+ });
1399
+ return { exists: true, reachable: true };
1400
+ } catch (error) {
1401
+ const errorMessage = error instanceof Error ? error.message : String(error);
1402
+ if (errorMessage.includes("unknown revision") || errorMessage.includes("bad revision") || errorMessage.includes("No such ref")) {
1403
+ return {
1404
+ exists: false,
1405
+ reachable: false,
1406
+ error: `Tag '${tagName}' not found in repository`
1407
+ };
1408
+ }
1409
+ return {
1410
+ exists: false,
1411
+ reachable: false,
1412
+ error: `Git error: ${errorMessage}`
1413
+ };
1414
+ }
1415
+ }
1416
+
1417
+ // src/utils/versionUtils.ts
1391
1418
  var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
1392
1419
  function normalizePrereleaseIdentifier(prereleaseIdentifier, config) {
1393
1420
  if (prereleaseIdentifier === true) {
@@ -1421,6 +1448,59 @@ function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
1421
1448
  }
1422
1449
  return import_semver2.default.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
1423
1450
  }
1451
+ async function getBestVersionSource(tagName, packageVersion, cwd5) {
1452
+ if (!(tagName == null ? void 0 : tagName.trim())) {
1453
+ 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" };
1454
+ }
1455
+ const verification = verifyTag(tagName, cwd5);
1456
+ if (!verification.exists || !verification.reachable) {
1457
+ if (packageVersion) {
1458
+ log(
1459
+ `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
1460
+ "warning"
1461
+ );
1462
+ return { source: "package", version: packageVersion, reason: "Git tag unreachable" };
1463
+ }
1464
+ log(
1465
+ `Git tag '${tagName}' unreachable and no package version available, using initial version`,
1466
+ "warning"
1467
+ );
1468
+ return {
1469
+ source: "initial",
1470
+ version: "0.1.0",
1471
+ reason: "Git tag unreachable, no package version"
1472
+ };
1473
+ }
1474
+ if (!packageVersion) {
1475
+ return {
1476
+ source: "git",
1477
+ version: tagName,
1478
+ reason: "Git tag exists, no package version to compare"
1479
+ };
1480
+ }
1481
+ try {
1482
+ const cleanTagVersion = tagName.replace(/^.*?([0-9])/, "$1");
1483
+ const cleanPackageVersion = packageVersion;
1484
+ if (import_semver2.default.gt(cleanPackageVersion, cleanTagVersion)) {
1485
+ log(
1486
+ `Package version ${packageVersion} is newer than git tag ${tagName}, using package version`,
1487
+ "info"
1488
+ );
1489
+ return { source: "package", version: packageVersion, reason: "Package version is newer" };
1490
+ }
1491
+ if (import_semver2.default.gt(cleanTagVersion, cleanPackageVersion)) {
1492
+ log(
1493
+ `Git tag ${tagName} is newer than package version ${packageVersion}, using git tag`,
1494
+ "info"
1495
+ );
1496
+ return { source: "git", version: tagName, reason: "Git tag is newer" };
1497
+ }
1498
+ return { source: "git", version: tagName, reason: "Versions equal, using git tag" };
1499
+ } catch (error) {
1500
+ log(`Failed to compare versions, defaulting to git tag: ${error}`, "warning");
1501
+ return { source: "git", version: tagName, reason: "Version comparison failed" };
1502
+ }
1503
+ }
1424
1504
 
1425
1505
  // src/core/versionCalculator.ts
1426
1506
  async function calculateVersion(config, options) {
@@ -1452,76 +1532,35 @@ async function calculateVersion(config, options) {
1452
1532
  return `${packageName}@${prefix}`;
1453
1533
  }, escapeRegExp3 = function(string) {
1454
1534
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1535
+ }, getCurrentVersionFromSource2 = function() {
1536
+ if (!versionSource) {
1537
+ if (hasNoTags) {
1538
+ return initialVersion;
1539
+ }
1540
+ const cleanedTag = import_semver3.default.clean(latestTag) || latestTag;
1541
+ return import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1542
+ }
1543
+ if (versionSource.source === "git") {
1544
+ const cleanedTag = import_semver3.default.clean(versionSource.version) || versionSource.version;
1545
+ return import_semver3.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1546
+ }
1547
+ return versionSource.version;
1455
1548
  };
1456
- var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3;
1549
+ var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3, getCurrentVersionFromSource = getCurrentVersionFromSource2;
1457
1550
  const originalPrefix = versionPrefix || "";
1458
1551
  const tagSearchPattern = determineTagSearchPattern2(name, originalPrefix);
1459
1552
  const escapedTagPattern = escapeRegExp3(tagSearchPattern);
1460
- if (!hasNoTags && pkgPath) {
1553
+ let versionSource;
1554
+ if (pkgPath) {
1461
1555
  const packageDir = pkgPath || (0, import_node_process3.cwd)();
1462
1556
  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
- }
1557
+ const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
1558
+ versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir);
1559
+ log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
1511
1560
  }
1512
1561
  const specifiedType = type;
1513
1562
  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";
1563
+ const currentVersion = getCurrentVersionFromSource2();
1525
1564
  if (STANDARD_BUMP_TYPES.includes(specifiedType) && (import_semver3.default.prerelease(currentVersion) || normalizedPrereleaseId)) {
1526
1565
  log(
1527
1566
  normalizedPrereleaseId ? `Creating prerelease version with identifier '${normalizedPrereleaseId}' using ${specifiedType}` : `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
@@ -1551,17 +1590,7 @@ To fix this mismatch:
1551
1590
  }
1552
1591
  }
1553
1592
  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";
1593
+ const currentVersion = getCurrentVersionFromSource2();
1565
1594
  log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
1566
1595
  return bumpVersion(currentVersion, branchVersionType, normalizedPrereleaseId);
1567
1596
  }
@@ -1571,35 +1600,34 @@ To fix this mismatch:
1571
1600
  bumper.loadPreset(preset);
1572
1601
  const recommendedBump = await bumper.bump();
1573
1602
  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
1603
+ const currentVersion = getCurrentVersionFromSource2();
1604
+ if (versionSource && versionSource.source === "git") {
1605
+ const checkPath = pkgPath || (0, import_node_process3.cwd)();
1606
+ const commitsLength = getCommitsLength(checkPath, versionSource.version);
1607
+ if (commitsLength === 0) {
1608
+ log(
1609
+ `No new commits found for ${name || "project"} since ${versionSource.version}, skipping version bump`,
1610
+ "info"
1582
1611
  );
1612
+ return "";
1583
1613
  }
1584
- return initialVersion;
1585
- }
1586
- const checkPath = pkgPath || (0, import_node_process3.cwd)();
1587
- const commitsLength = getCommitsLength(checkPath);
1588
- if (commitsLength === 0) {
1614
+ } else if (versionSource && versionSource.source === "package") {
1589
1615
  log(
1590
- `No new commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1591
- "info"
1616
+ `Using package version ${versionSource.version} as base, letting conventional commits determine bump necessity`,
1617
+ "debug"
1592
1618
  );
1593
- return "";
1594
1619
  }
1595
1620
  if (!releaseTypeFromCommits) {
1596
- log(
1597
- `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1598
- "info"
1599
- );
1621
+ if (latestTag && latestTag.trim() !== "") {
1622
+ log(
1623
+ `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1624
+ "info"
1625
+ );
1626
+ } else {
1627
+ log(`No relevant commits found for ${name || "project"}, skipping version bump`, "info");
1628
+ }
1600
1629
  return "";
1601
1630
  }
1602
- const currentVersion = import_semver3.default.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1603
1631
  return bumpVersion(currentVersion, releaseTypeFromCommits, normalizedPrereleaseId);
1604
1632
  } catch (error) {
1605
1633
  log(`Failed to calculate version for ${name || "project"}`, "error");
@@ -1620,40 +1648,6 @@ To fix this mismatch:
1620
1648
  throw error;
1621
1649
  }
1622
1650
  }
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
1651
 
1658
1652
  // src/utils/packageMatching.ts
1659
1653
  var import_micromatch2 = __toESM(require("micromatch"), 1);
@@ -1803,14 +1797,14 @@ var PackageProcessor = class {
1803
1797
  try {
1804
1798
  let revisionRange;
1805
1799
  if (latestTag) {
1806
- try {
1807
- (0, import_node_child_process4.execSync)(`git rev-parse --verify "${latestTag}"`, {
1808
- cwd: pkgPath,
1809
- stdio: "ignore"
1810
- });
1800
+ const verification = verifyTag(latestTag, pkgPath);
1801
+ if (verification.exists && verification.reachable) {
1811
1802
  revisionRange = `${latestTag}..HEAD`;
1812
- } catch {
1813
- log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1803
+ } else {
1804
+ log(
1805
+ `Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`,
1806
+ "debug"
1807
+ );
1814
1808
  revisionRange = "HEAD";
1815
1809
  }
1816
1810
  } else {
@@ -1999,7 +1993,7 @@ function createSyncedStrategy(config) {
1999
1993
  mainPackage
2000
1994
  } = config;
2001
1995
  const formattedPrefix = formatVersionPrefix(versionPrefix || "v");
2002
- const latestTag = await getLatestTag();
1996
+ let latestTag = await getLatestTag();
2003
1997
  let mainPkgPath = packages.root;
2004
1998
  let mainPkgName;
2005
1999
  if (mainPackage) {
@@ -2022,6 +2016,21 @@ function createSyncedStrategy(config) {
2022
2016
  "warning"
2023
2017
  );
2024
2018
  }
2019
+ if (mainPkgName) {
2020
+ const packageSpecificTag = await getLatestTagForPackage(mainPkgName, formattedPrefix, {
2021
+ tagTemplate,
2022
+ packageSpecificTags: config.packageSpecificTags
2023
+ });
2024
+ if (packageSpecificTag) {
2025
+ latestTag = packageSpecificTag;
2026
+ log(`Using package-specific tag for ${mainPkgName}: ${latestTag}`, "debug");
2027
+ } else {
2028
+ log(
2029
+ `No package-specific tag found for ${mainPkgName}, using global tag: ${latestTag}`,
2030
+ "debug"
2031
+ );
2032
+ }
2033
+ }
2025
2034
  const nextVersion = await calculateVersion(config, {
2026
2035
  latestTag,
2027
2036
  versionPrefix: formattedPrefix,
@@ -2164,7 +2173,7 @@ function createSingleStrategy(config) {
2164
2173
  let revisionRange;
2165
2174
  if (latestTag) {
2166
2175
  try {
2167
- (0, import_node_child_process5.execSync)(`git rev-parse --verify "${latestTag}"`, {
2176
+ (0, import_node_child_process4.execSync)(`git rev-parse --verify "${latestTag}"`, {
2168
2177
  cwd: pkgPath,
2169
2178
  stdio: "ignore"
2170
2179
  });
@@ -2451,11 +2460,22 @@ async function run() {
2451
2460
  program.command("version", { isDefault: true }).description("Version a package or packages based on configuration").option(
2452
2461
  "-c, --config <path>",
2453
2462
  "Path to config file (defaults to version.config.json in current directory)"
2454
- ).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) => {
2463
+ ).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) => {
2455
2464
  if (options.json) {
2456
2465
  enableJsonOutput(options.dryRun);
2457
2466
  }
2458
2467
  try {
2468
+ const originalCwd = process.cwd();
2469
+ if (options.projectDir && options.projectDir !== originalCwd) {
2470
+ try {
2471
+ process.chdir(options.projectDir);
2472
+ log(`Changed working directory to: ${options.projectDir}`, "debug");
2473
+ } catch (error) {
2474
+ throw new Error(
2475
+ `Failed to change to directory "${options.projectDir}": ${error instanceof Error ? error.message : String(error)}`
2476
+ );
2477
+ }
2478
+ }
2459
2479
  const config = await loadConfig(options.config);
2460
2480
  log(`Loaded configuration from ${options.config || "version.config.json"}`, "info");
2461
2481
  if (options.dryRun) config.dryRun = true;
package/dist/index.js CHANGED
@@ -726,7 +726,7 @@ function matchesPackageNamePattern(packageName, pattern) {
726
726
  }
727
727
 
728
728
  // src/core/versionStrategies.ts
729
- import { execSync as execSync5 } from "child_process";
729
+ import { execSync as execSync4 } from "child_process";
730
730
  import fs9 from "fs";
731
731
  import * as path8 from "path";
732
732
 
@@ -1008,9 +1008,14 @@ function formatCommitMessage(template, version, packageName, additionalContext)
1008
1008
  }
1009
1009
 
1010
1010
  // src/git/tagsAndBranches.ts
1011
- function getCommitsLength(pkgRoot) {
1011
+ function getCommitsLength(pkgRoot, sinceTag) {
1012
1012
  try {
1013
- const gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1013
+ let gitCommand;
1014
+ if (sinceTag && sinceTag.trim() !== "") {
1015
+ gitCommand = `git rev-list --count ${sinceTag}..HEAD ${pkgRoot}`;
1016
+ } else {
1017
+ gitCommand = `git rev-list --count HEAD ^$(git describe --tags --abbrev=0) ${pkgRoot}`;
1018
+ }
1014
1019
  const amount = execSync3(gitCommand).toString().trim();
1015
1020
  return Number(amount);
1016
1021
  } catch (error) {
@@ -1284,7 +1289,6 @@ function updatePackageVersion(packagePath, version) {
1284
1289
  }
1285
1290
 
1286
1291
  // src/package/packageProcessor.ts
1287
- import { execSync as execSync4 } from "child_process";
1288
1292
  import * as fs8 from "fs";
1289
1293
  import path7 from "path";
1290
1294
  import { exit } from "process";
@@ -1343,18 +1347,41 @@ function getVersionFromManifests(packageDir) {
1343
1347
  manifestType: null
1344
1348
  };
1345
1349
  }
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
1350
 
1354
1351
  // src/utils/versionUtils.ts
1355
1352
  import fs7 from "fs";
1356
1353
  import semver2 from "semver";
1357
1354
  import * as TOML2 from "smol-toml";
1355
+
1356
+ // src/git/tagVerification.ts
1357
+ function verifyTag(tagName, cwd5) {
1358
+ if (!tagName || tagName.trim() === "") {
1359
+ return { exists: false, reachable: false, error: "Empty tag name" };
1360
+ }
1361
+ try {
1362
+ execSync3(`git rev-parse --verify "${tagName}"`, {
1363
+ cwd: cwd5,
1364
+ stdio: "ignore"
1365
+ });
1366
+ return { exists: true, reachable: true };
1367
+ } catch (error) {
1368
+ const errorMessage = error instanceof Error ? error.message : String(error);
1369
+ if (errorMessage.includes("unknown revision") || errorMessage.includes("bad revision") || errorMessage.includes("No such ref")) {
1370
+ return {
1371
+ exists: false,
1372
+ reachable: false,
1373
+ error: `Tag '${tagName}' not found in repository`
1374
+ };
1375
+ }
1376
+ return {
1377
+ exists: false,
1378
+ reachable: false,
1379
+ error: `Git error: ${errorMessage}`
1380
+ };
1381
+ }
1382
+ }
1383
+
1384
+ // src/utils/versionUtils.ts
1358
1385
  var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
1359
1386
  function normalizePrereleaseIdentifier(prereleaseIdentifier, config) {
1360
1387
  if (prereleaseIdentifier === true) {
@@ -1388,6 +1415,59 @@ function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
1388
1415
  }
1389
1416
  return semver2.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
1390
1417
  }
1418
+ async function getBestVersionSource(tagName, packageVersion, cwd5) {
1419
+ if (!(tagName == null ? void 0 : tagName.trim())) {
1420
+ 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" };
1421
+ }
1422
+ const verification = verifyTag(tagName, cwd5);
1423
+ if (!verification.exists || !verification.reachable) {
1424
+ if (packageVersion) {
1425
+ log(
1426
+ `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
1427
+ "warning"
1428
+ );
1429
+ return { source: "package", version: packageVersion, reason: "Git tag unreachable" };
1430
+ }
1431
+ log(
1432
+ `Git tag '${tagName}' unreachable and no package version available, using initial version`,
1433
+ "warning"
1434
+ );
1435
+ return {
1436
+ source: "initial",
1437
+ version: "0.1.0",
1438
+ reason: "Git tag unreachable, no package version"
1439
+ };
1440
+ }
1441
+ if (!packageVersion) {
1442
+ return {
1443
+ source: "git",
1444
+ version: tagName,
1445
+ reason: "Git tag exists, no package version to compare"
1446
+ };
1447
+ }
1448
+ try {
1449
+ const cleanTagVersion = tagName.replace(/^.*?([0-9])/, "$1");
1450
+ const cleanPackageVersion = packageVersion;
1451
+ if (semver2.gt(cleanPackageVersion, cleanTagVersion)) {
1452
+ log(
1453
+ `Package version ${packageVersion} is newer than git tag ${tagName}, using package version`,
1454
+ "info"
1455
+ );
1456
+ return { source: "package", version: packageVersion, reason: "Package version is newer" };
1457
+ }
1458
+ if (semver2.gt(cleanTagVersion, cleanPackageVersion)) {
1459
+ log(
1460
+ `Git tag ${tagName} is newer than package version ${packageVersion}, using git tag`,
1461
+ "info"
1462
+ );
1463
+ return { source: "git", version: tagName, reason: "Git tag is newer" };
1464
+ }
1465
+ return { source: "git", version: tagName, reason: "Versions equal, using git tag" };
1466
+ } catch (error) {
1467
+ log(`Failed to compare versions, defaulting to git tag: ${error}`, "warning");
1468
+ return { source: "git", version: tagName, reason: "Version comparison failed" };
1469
+ }
1470
+ }
1391
1471
 
1392
1472
  // src/core/versionCalculator.ts
1393
1473
  async function calculateVersion(config, options) {
@@ -1419,76 +1499,35 @@ async function calculateVersion(config, options) {
1419
1499
  return `${packageName}@${prefix}`;
1420
1500
  }, escapeRegExp3 = function(string) {
1421
1501
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1502
+ }, getCurrentVersionFromSource2 = function() {
1503
+ if (!versionSource) {
1504
+ if (hasNoTags) {
1505
+ return initialVersion;
1506
+ }
1507
+ const cleanedTag = semver3.clean(latestTag) || latestTag;
1508
+ return semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1509
+ }
1510
+ if (versionSource.source === "git") {
1511
+ const cleanedTag = semver3.clean(versionSource.version) || versionSource.version;
1512
+ return semver3.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1513
+ }
1514
+ return versionSource.version;
1422
1515
  };
1423
- var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3;
1516
+ var determineTagSearchPattern = determineTagSearchPattern2, escapeRegExp2 = escapeRegExp3, getCurrentVersionFromSource = getCurrentVersionFromSource2;
1424
1517
  const originalPrefix = versionPrefix || "";
1425
1518
  const tagSearchPattern = determineTagSearchPattern2(name, originalPrefix);
1426
1519
  const escapedTagPattern = escapeRegExp3(tagSearchPattern);
1427
- if (!hasNoTags && pkgPath) {
1520
+ let versionSource;
1521
+ if (pkgPath) {
1428
1522
  const packageDir = pkgPath || cwd3();
1429
1523
  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
- }
1524
+ const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
1525
+ versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir);
1526
+ log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
1478
1527
  }
1479
1528
  const specifiedType = type;
1480
1529
  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";
1530
+ const currentVersion = getCurrentVersionFromSource2();
1492
1531
  if (STANDARD_BUMP_TYPES.includes(specifiedType) && (semver3.prerelease(currentVersion) || normalizedPrereleaseId)) {
1493
1532
  log(
1494
1533
  normalizedPrereleaseId ? `Creating prerelease version with identifier '${normalizedPrereleaseId}' using ${specifiedType}` : `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
@@ -1518,17 +1557,7 @@ To fix this mismatch:
1518
1557
  }
1519
1558
  }
1520
1559
  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";
1560
+ const currentVersion = getCurrentVersionFromSource2();
1532
1561
  log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
1533
1562
  return bumpVersion(currentVersion, branchVersionType, normalizedPrereleaseId);
1534
1563
  }
@@ -1538,35 +1567,34 @@ To fix this mismatch:
1538
1567
  bumper.loadPreset(preset);
1539
1568
  const recommendedBump = await bumper.bump();
1540
1569
  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
1570
+ const currentVersion = getCurrentVersionFromSource2();
1571
+ if (versionSource && versionSource.source === "git") {
1572
+ const checkPath = pkgPath || cwd3();
1573
+ const commitsLength = getCommitsLength(checkPath, versionSource.version);
1574
+ if (commitsLength === 0) {
1575
+ log(
1576
+ `No new commits found for ${name || "project"} since ${versionSource.version}, skipping version bump`,
1577
+ "info"
1549
1578
  );
1579
+ return "";
1550
1580
  }
1551
- return initialVersion;
1552
- }
1553
- const checkPath = pkgPath || cwd3();
1554
- const commitsLength = getCommitsLength(checkPath);
1555
- if (commitsLength === 0) {
1581
+ } else if (versionSource && versionSource.source === "package") {
1556
1582
  log(
1557
- `No new commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1558
- "info"
1583
+ `Using package version ${versionSource.version} as base, letting conventional commits determine bump necessity`,
1584
+ "debug"
1559
1585
  );
1560
- return "";
1561
1586
  }
1562
1587
  if (!releaseTypeFromCommits) {
1563
- log(
1564
- `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1565
- "info"
1566
- );
1588
+ if (latestTag && latestTag.trim() !== "") {
1589
+ log(
1590
+ `No relevant commits found for ${name || "project"} since ${latestTag}, skipping version bump`,
1591
+ "info"
1592
+ );
1593
+ } else {
1594
+ log(`No relevant commits found for ${name || "project"}, skipping version bump`, "info");
1595
+ }
1567
1596
  return "";
1568
1597
  }
1569
- const currentVersion = semver3.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
1570
1598
  return bumpVersion(currentVersion, releaseTypeFromCommits, normalizedPrereleaseId);
1571
1599
  } catch (error) {
1572
1600
  log(`Failed to calculate version for ${name || "project"}`, "error");
@@ -1587,40 +1615,6 @@ To fix this mismatch:
1587
1615
  throw error;
1588
1616
  }
1589
1617
  }
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
1618
 
1625
1619
  // src/utils/packageMatching.ts
1626
1620
  import micromatch2 from "micromatch";
@@ -1770,14 +1764,14 @@ var PackageProcessor = class {
1770
1764
  try {
1771
1765
  let revisionRange;
1772
1766
  if (latestTag) {
1773
- try {
1774
- execSync4(`git rev-parse --verify "${latestTag}"`, {
1775
- cwd: pkgPath,
1776
- stdio: "ignore"
1777
- });
1767
+ const verification = verifyTag(latestTag, pkgPath);
1768
+ if (verification.exists && verification.reachable) {
1778
1769
  revisionRange = `${latestTag}..HEAD`;
1779
- } catch {
1780
- log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1770
+ } else {
1771
+ log(
1772
+ `Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`,
1773
+ "debug"
1774
+ );
1781
1775
  revisionRange = "HEAD";
1782
1776
  }
1783
1777
  } else {
@@ -1966,7 +1960,7 @@ function createSyncedStrategy(config) {
1966
1960
  mainPackage
1967
1961
  } = config;
1968
1962
  const formattedPrefix = formatVersionPrefix(versionPrefix || "v");
1969
- const latestTag = await getLatestTag();
1963
+ let latestTag = await getLatestTag();
1970
1964
  let mainPkgPath = packages.root;
1971
1965
  let mainPkgName;
1972
1966
  if (mainPackage) {
@@ -1989,6 +1983,21 @@ function createSyncedStrategy(config) {
1989
1983
  "warning"
1990
1984
  );
1991
1985
  }
1986
+ if (mainPkgName) {
1987
+ const packageSpecificTag = await getLatestTagForPackage(mainPkgName, formattedPrefix, {
1988
+ tagTemplate,
1989
+ packageSpecificTags: config.packageSpecificTags
1990
+ });
1991
+ if (packageSpecificTag) {
1992
+ latestTag = packageSpecificTag;
1993
+ log(`Using package-specific tag for ${mainPkgName}: ${latestTag}`, "debug");
1994
+ } else {
1995
+ log(
1996
+ `No package-specific tag found for ${mainPkgName}, using global tag: ${latestTag}`,
1997
+ "debug"
1998
+ );
1999
+ }
2000
+ }
1992
2001
  const nextVersion = await calculateVersion(config, {
1993
2002
  latestTag,
1994
2003
  versionPrefix: formattedPrefix,
@@ -2131,7 +2140,7 @@ function createSingleStrategy(config) {
2131
2140
  let revisionRange;
2132
2141
  if (latestTag) {
2133
2142
  try {
2134
- execSync5(`git rev-parse --verify "${latestTag}"`, {
2143
+ execSync4(`git rev-parse --verify "${latestTag}"`, {
2135
2144
  cwd: pkgPath,
2136
2145
  stdio: "ignore"
2137
2146
  });
@@ -2417,11 +2426,22 @@ async function run() {
2417
2426
  program.command("version", { isDefault: true }).description("Version a package or packages based on configuration").option(
2418
2427
  "-c, --config <path>",
2419
2428
  "Path to config file (defaults to version.config.json in current directory)"
2420
- ).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) => {
2429
+ ).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) => {
2421
2430
  if (options.json) {
2422
2431
  enableJsonOutput(options.dryRun);
2423
2432
  }
2424
2433
  try {
2434
+ const originalCwd = process.cwd();
2435
+ if (options.projectDir && options.projectDir !== originalCwd) {
2436
+ try {
2437
+ process.chdir(options.projectDir);
2438
+ log(`Changed working directory to: ${options.projectDir}`, "debug");
2439
+ } catch (error) {
2440
+ throw new Error(
2441
+ `Failed to change to directory "${options.projectDir}": ${error instanceof Error ? error.message : String(error)}`
2442
+ );
2443
+ }
2444
+ }
2425
2445
  const config = await loadConfig(options.config);
2426
2446
  log(`Loaded configuration from ${options.config || "version.config.json"}`, "info");
2427
2447
  if (options.dryRun) config.dryRun = true;
@@ -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
@@ -53,7 +53,7 @@
53
53
  "minLength": 1
54
54
  },
55
55
  "default": [],
56
- "description": "Array of package names or patterns that determines which packages will be processed for versioning. When specified, only packages matching these patterns will be versioned. When empty or not specified, all workspace packages will be processed. Supports exact names (e.g., '@scope/package-a'), scope wildcards (e.g., '@scope/*'), and global wildcards (e.g., '*')"
56
+ "description": "Array of package names or patterns that determines which packages will be processed for versioning. When specified, only packages matching these patterns will be versioned. When empty or not specified, all workspace packages will be processed. Supports exact names (e.g., '@scope/package-a'), scope wildcards (e.g., '@scope/*'), path patterns (e.g., 'packages/**/*', 'examples/**'), and global wildcards (e.g., '*')."
57
57
  },
58
58
  "mainPackage": {
59
59
  "type": "string",
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.3",
4
+ "version": "0.8.5",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",