@codluv/versionguard 0.2.0 → 0.3.0

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.
@@ -1,10 +1,11 @@
1
1
  import * as childProcess from "node:child_process";
2
- import { execSync } from "node:child_process";
2
+ import { execFileSync, execSync } from "node:child_process";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
5
+ import { parse as parse$2 } from "smol-toml";
6
+ import * as yaml from "js-yaml";
5
7
  import { globSync } from "glob";
6
8
  import { fileURLToPath } from "node:url";
7
- import * as yaml from "js-yaml";
8
9
  function parseFormat(calverFormat) {
9
10
  const parts = calverFormat.split(".");
10
11
  const result = {
@@ -245,7 +246,7 @@ function validateChangelog(changelogPath, version, strict = true, requireEntry =
245
246
  errors.push("Changelog should include compare links at the bottom");
246
247
  }
247
248
  const versionHeaderMatch = content.match(
248
- new RegExp(`## \\[${escapeRegExp(version)}\\] - ([^\r
249
+ new RegExp(`## \\[${escapeRegExp$2(version)}\\] - ([^\r
249
250
  ]+)`)
250
251
  );
251
252
  if (requireEntry && hasEntryForVersion) {
@@ -285,7 +286,7 @@ function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Dat
285
286
  const updated = `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
286
287
  fs.writeFileSync(changelogPath, updated, "utf-8");
287
288
  }
288
- function escapeRegExp(value) {
289
+ function escapeRegExp$2(value) {
289
290
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
291
  }
291
292
  const HOOK_NAMES$1 = ["pre-commit", "pre-push", "post-tag"];
@@ -342,6 +343,7 @@ function areHooksInstalled(cwd = process.cwd()) {
342
343
  }
343
344
  function generateHookScript(hookName) {
344
345
  return `#!/bin/sh
346
+ # versionguard
345
347
  # VersionGuard ${hookName} hook
346
348
  # --no-install prevents accidentally downloading an unscoped package
347
349
  # if @codluv/versionguard is not installed locally
@@ -353,6 +355,428 @@ if [ $status -ne 0 ]; then
353
355
  fi
354
356
  `;
355
357
  }
358
+ class GitTagSource {
359
+ name = "git-tag";
360
+ manifestFile = "";
361
+ exists(cwd) {
362
+ try {
363
+ execFileSync("git", ["rev-parse", "--git-dir"], {
364
+ cwd,
365
+ stdio: ["pipe", "pipe", "ignore"]
366
+ });
367
+ return true;
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+ getVersion(cwd) {
373
+ try {
374
+ const tag = execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
375
+ cwd,
376
+ encoding: "utf-8",
377
+ stdio: ["pipe", "pipe", "ignore"]
378
+ }).trim();
379
+ return tag.replace(/^v/, "");
380
+ } catch {
381
+ throw new Error("No git tags found. Create a tag first (e.g., git tag v0.1.0)");
382
+ }
383
+ }
384
+ setVersion(version, cwd) {
385
+ const tagName = `v${version}`;
386
+ execFileSync("git", ["tag", "-a", tagName, "-m", `Release ${version}`], {
387
+ cwd,
388
+ stdio: ["pipe", "pipe", "ignore"]
389
+ });
390
+ }
391
+ }
392
+ class JsonVersionSource {
393
+ name;
394
+ manifestFile;
395
+ versionPath;
396
+ constructor(manifestFile = "package.json", versionPath = "version") {
397
+ this.name = manifestFile;
398
+ this.manifestFile = manifestFile;
399
+ this.versionPath = versionPath;
400
+ }
401
+ exists(cwd) {
402
+ return fs.existsSync(path.join(cwd, this.manifestFile));
403
+ }
404
+ getVersion(cwd) {
405
+ const filePath = path.join(cwd, this.manifestFile);
406
+ if (!fs.existsSync(filePath)) {
407
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
408
+ }
409
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
410
+ const version = getNestedValue$1(content, this.versionPath);
411
+ if (typeof version !== "string" || version.length === 0) {
412
+ throw new Error(`No version field in ${this.manifestFile}`);
413
+ }
414
+ return version;
415
+ }
416
+ setVersion(version, cwd) {
417
+ const filePath = path.join(cwd, this.manifestFile);
418
+ if (!fs.existsSync(filePath)) {
419
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
420
+ }
421
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
422
+ setNestedValue(content, this.versionPath, version);
423
+ fs.writeFileSync(filePath, `${JSON.stringify(content, null, 2)}
424
+ `, "utf-8");
425
+ }
426
+ }
427
+ function getNestedValue$1(obj, dotPath) {
428
+ let current = obj;
429
+ for (const key of dotPath.split(".")) {
430
+ if (current === null || typeof current !== "object") {
431
+ return void 0;
432
+ }
433
+ current = current[key];
434
+ }
435
+ return current;
436
+ }
437
+ function setNestedValue(obj, dotPath, value) {
438
+ const keys = dotPath.split(".");
439
+ let current = obj;
440
+ for (let i = 0; i < keys.length - 1; i++) {
441
+ const key = keys[i];
442
+ if (typeof current[key] !== "object" || current[key] === null) {
443
+ current[key] = {};
444
+ }
445
+ current = current[key];
446
+ }
447
+ current[keys[keys.length - 1]] = value;
448
+ }
449
+ class RegexVersionSource {
450
+ name;
451
+ manifestFile;
452
+ versionRegex;
453
+ constructor(manifestFile, versionRegex) {
454
+ this.name = manifestFile;
455
+ this.manifestFile = manifestFile;
456
+ try {
457
+ this.versionRegex = new RegExp(versionRegex, "m");
458
+ } catch (err) {
459
+ throw new Error(`Invalid version regex for ${manifestFile}: ${err.message}`);
460
+ }
461
+ if (!/\((?!\?)/.test(versionRegex)) {
462
+ throw new Error(`Version regex for ${manifestFile} must contain at least one capture group`);
463
+ }
464
+ }
465
+ exists(cwd) {
466
+ return fs.existsSync(path.join(cwd, this.manifestFile));
467
+ }
468
+ getVersion(cwd) {
469
+ const filePath = path.join(cwd, this.manifestFile);
470
+ if (!fs.existsSync(filePath)) {
471
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
472
+ }
473
+ const content = fs.readFileSync(filePath, "utf-8");
474
+ const match = content.match(this.versionRegex);
475
+ if (!match?.[1]) {
476
+ throw new Error(`No version match found in ${this.manifestFile}`);
477
+ }
478
+ return match[1];
479
+ }
480
+ setVersion(version, cwd) {
481
+ const filePath = path.join(cwd, this.manifestFile);
482
+ if (!fs.existsSync(filePath)) {
483
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
484
+ }
485
+ const content = fs.readFileSync(filePath, "utf-8");
486
+ const match = this.versionRegex.exec(content);
487
+ if (!match || match.index === void 0) {
488
+ throw new Error(`No version match found in ${this.manifestFile}`);
489
+ }
490
+ const captureStart = match.index + match[0].indexOf(match[1]);
491
+ const captureEnd = captureStart + match[1].length;
492
+ const updated = content.slice(0, captureStart) + version + content.slice(captureEnd);
493
+ fs.writeFileSync(filePath, updated, "utf-8");
494
+ }
495
+ }
496
+ class TomlVersionSource {
497
+ name;
498
+ manifestFile;
499
+ versionPath;
500
+ constructor(manifestFile = "Cargo.toml", versionPath = "package.version") {
501
+ this.name = manifestFile;
502
+ this.manifestFile = manifestFile;
503
+ this.versionPath = versionPath;
504
+ }
505
+ exists(cwd) {
506
+ return fs.existsSync(path.join(cwd, this.manifestFile));
507
+ }
508
+ getVersion(cwd) {
509
+ const filePath = path.join(cwd, this.manifestFile);
510
+ if (!fs.existsSync(filePath)) {
511
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
512
+ }
513
+ const content = fs.readFileSync(filePath, "utf-8");
514
+ const parsed = parse$2(content);
515
+ const version = getNestedValue(parsed, this.versionPath);
516
+ if (typeof version !== "string" || version.length === 0) {
517
+ throw new Error(`No version field at '${this.versionPath}' in ${this.manifestFile}`);
518
+ }
519
+ return version;
520
+ }
521
+ setVersion(version, cwd) {
522
+ const filePath = path.join(cwd, this.manifestFile);
523
+ if (!fs.existsSync(filePath)) {
524
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
525
+ }
526
+ const content = fs.readFileSync(filePath, "utf-8");
527
+ const sectionKey = this.getSectionKey();
528
+ const updated = replaceTomlVersion(content, sectionKey, version);
529
+ if (updated === content) {
530
+ throw new Error(`Could not find version field to update in ${this.manifestFile}`);
531
+ }
532
+ fs.writeFileSync(filePath, updated, "utf-8");
533
+ }
534
+ getSectionKey() {
535
+ const parts = this.versionPath.split(".");
536
+ if (parts.length === 1) {
537
+ return { section: "", key: parts[0] };
538
+ }
539
+ return {
540
+ section: parts.slice(0, -1).join("."),
541
+ key: parts[parts.length - 1]
542
+ };
543
+ }
544
+ }
545
+ function getNestedValue(obj, dotPath) {
546
+ let current = obj;
547
+ for (const key of dotPath.split(".")) {
548
+ if (current === null || typeof current !== "object") {
549
+ return void 0;
550
+ }
551
+ current = current[key];
552
+ }
553
+ return current;
554
+ }
555
+ function replaceTomlVersion(content, target, newVersion) {
556
+ const lines = content.split("\n");
557
+ const sectionHeader = target.section ? `[${target.section}]` : null;
558
+ let inSection = sectionHeader === null;
559
+ const versionRegex = new RegExp(`^(\\s*${escapeRegExp$1(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`);
560
+ for (let i = 0; i < lines.length; i++) {
561
+ const trimmed = lines[i].trim();
562
+ if (sectionHeader !== null) {
563
+ if (trimmed === sectionHeader) {
564
+ inSection = true;
565
+ continue;
566
+ }
567
+ if (inSection && trimmed.startsWith("[") && trimmed !== sectionHeader) {
568
+ inSection = false;
569
+ continue;
570
+ }
571
+ }
572
+ if (inSection) {
573
+ const match = lines[i].match(versionRegex);
574
+ if (match) {
575
+ lines[i] = lines[i].replace(versionRegex, `$1$2${newVersion}$4`);
576
+ return lines.join("\n");
577
+ }
578
+ }
579
+ }
580
+ return content;
581
+ }
582
+ function escapeRegExp$1(value) {
583
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
584
+ }
585
+ class VersionFileSource {
586
+ name;
587
+ manifestFile;
588
+ constructor(manifestFile = "VERSION") {
589
+ this.name = manifestFile;
590
+ this.manifestFile = manifestFile;
591
+ }
592
+ exists(cwd) {
593
+ return fs.existsSync(path.join(cwd, this.manifestFile));
594
+ }
595
+ getVersion(cwd) {
596
+ const filePath = path.join(cwd, this.manifestFile);
597
+ if (!fs.existsSync(filePath)) {
598
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
599
+ }
600
+ const version = fs.readFileSync(filePath, "utf-8").trim();
601
+ if (version.length === 0) {
602
+ throw new Error(`${this.manifestFile} is empty`);
603
+ }
604
+ return version;
605
+ }
606
+ setVersion(version, cwd) {
607
+ const filePath = path.join(cwd, this.manifestFile);
608
+ if (!fs.existsSync(filePath)) {
609
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
610
+ }
611
+ fs.writeFileSync(filePath, `${version}
612
+ `, "utf-8");
613
+ }
614
+ }
615
+ class YamlVersionSource {
616
+ name;
617
+ manifestFile;
618
+ versionKey;
619
+ constructor(manifestFile = "pubspec.yaml", versionKey = "version") {
620
+ this.name = manifestFile;
621
+ this.manifestFile = manifestFile;
622
+ this.versionKey = versionKey;
623
+ }
624
+ exists(cwd) {
625
+ return fs.existsSync(path.join(cwd, this.manifestFile));
626
+ }
627
+ getVersion(cwd) {
628
+ const filePath = path.join(cwd, this.manifestFile);
629
+ if (!fs.existsSync(filePath)) {
630
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
631
+ }
632
+ const content = fs.readFileSync(filePath, "utf-8");
633
+ const parsed = yaml.load(content);
634
+ if (!parsed || typeof parsed !== "object") {
635
+ throw new Error(`Failed to parse ${this.manifestFile}`);
636
+ }
637
+ const version = parsed[this.versionKey];
638
+ if (typeof version !== "string" || version.length === 0) {
639
+ if (typeof version === "number") {
640
+ return String(version);
641
+ }
642
+ throw new Error(`No version field in ${this.manifestFile}`);
643
+ }
644
+ return version;
645
+ }
646
+ setVersion(version, cwd) {
647
+ const filePath = path.join(cwd, this.manifestFile);
648
+ if (!fs.existsSync(filePath)) {
649
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
650
+ }
651
+ const content = fs.readFileSync(filePath, "utf-8");
652
+ const regex = new RegExp(`^(${escapeRegExp(this.versionKey)}:\\s*)(["']?)(.+?)\\2\\s*$`, "m");
653
+ const updated = content.replace(regex, `$1$2${version}$2`);
654
+ if (updated === content) {
655
+ throw new Error(`Could not find version field to update in ${this.manifestFile}`);
656
+ }
657
+ fs.writeFileSync(filePath, updated, "utf-8");
658
+ }
659
+ }
660
+ function escapeRegExp(value) {
661
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
662
+ }
663
+ const VALID_SOURCES = /* @__PURE__ */ new Set([
664
+ "auto",
665
+ "package.json",
666
+ "composer.json",
667
+ "Cargo.toml",
668
+ "pyproject.toml",
669
+ "pubspec.yaml",
670
+ "pom.xml",
671
+ "VERSION",
672
+ "git-tag",
673
+ "custom"
674
+ ]);
675
+ const DETECTION_TABLE = [
676
+ {
677
+ file: "package.json",
678
+ source: "package.json",
679
+ factory: () => new JsonVersionSource("package.json", "version")
680
+ },
681
+ {
682
+ file: "Cargo.toml",
683
+ source: "Cargo.toml",
684
+ factory: () => new TomlVersionSource("Cargo.toml", "package.version")
685
+ },
686
+ {
687
+ file: "pyproject.toml",
688
+ source: "pyproject.toml",
689
+ factory: () => new TomlVersionSource("pyproject.toml", "project.version")
690
+ },
691
+ {
692
+ file: "pubspec.yaml",
693
+ source: "pubspec.yaml",
694
+ factory: () => new YamlVersionSource("pubspec.yaml", "version")
695
+ },
696
+ {
697
+ file: "composer.json",
698
+ source: "composer.json",
699
+ factory: () => new JsonVersionSource("composer.json", "version")
700
+ },
701
+ {
702
+ // H-002: Use regex that skips <parent> blocks for pom.xml
703
+ file: "pom.xml",
704
+ source: "pom.xml",
705
+ factory: () => new RegexVersionSource("pom.xml", "<project[^>]*>[\\s\\S]*?<version>([^<]+)</version>")
706
+ },
707
+ { file: "VERSION", source: "VERSION", factory: () => new VersionFileSource("VERSION") }
708
+ ];
709
+ function assertPathContained(manifestFile, cwd) {
710
+ const resolved = path.resolve(cwd, manifestFile);
711
+ const root = path.resolve(cwd);
712
+ if (!resolved.startsWith(`${root}${path.sep}`) && resolved !== root) {
713
+ throw new Error(`Manifest path "${manifestFile}" resolves outside the project directory`);
714
+ }
715
+ }
716
+ function createProvider(source, config, cwd) {
717
+ if (!VALID_SOURCES.has(source)) {
718
+ throw new Error(
719
+ `Invalid manifest source "${source}". Valid sources: ${[...VALID_SOURCES].join(", ")}`
720
+ );
721
+ }
722
+ switch (source) {
723
+ case "package.json":
724
+ return new JsonVersionSource("package.json", config.path ?? "version");
725
+ case "composer.json":
726
+ return new JsonVersionSource("composer.json", config.path ?? "version");
727
+ case "Cargo.toml":
728
+ return new TomlVersionSource("Cargo.toml", config.path ?? "package.version");
729
+ case "pyproject.toml":
730
+ return new TomlVersionSource("pyproject.toml", config.path ?? "project.version");
731
+ case "pubspec.yaml":
732
+ return new YamlVersionSource("pubspec.yaml", config.path ?? "version");
733
+ case "pom.xml":
734
+ return new RegexVersionSource(
735
+ "pom.xml",
736
+ config.regex ?? "<project[^>]*>[\\s\\S]*?<version>([^<]+)</version>"
737
+ );
738
+ case "VERSION":
739
+ return new VersionFileSource(config.path ?? "VERSION");
740
+ case "git-tag":
741
+ return new GitTagSource();
742
+ case "custom": {
743
+ if (!config.regex) {
744
+ throw new Error("Custom manifest source requires a 'regex' field in manifest config");
745
+ }
746
+ if (!config.path) {
747
+ throw new Error(
748
+ "Custom manifest source requires a 'path' field (manifest filename) in manifest config"
749
+ );
750
+ }
751
+ assertPathContained(config.path, cwd);
752
+ return new RegexVersionSource(config.path, config.regex);
753
+ }
754
+ default:
755
+ throw new Error(`Unknown manifest source: ${source}`);
756
+ }
757
+ }
758
+ function resolveVersionSource(config, cwd = process.cwd()) {
759
+ if (config.source !== "auto") {
760
+ return createProvider(config.source, config, cwd);
761
+ }
762
+ for (const entry of DETECTION_TABLE) {
763
+ const provider = entry.factory();
764
+ if (provider.exists(cwd)) {
765
+ return provider;
766
+ }
767
+ }
768
+ return new JsonVersionSource("package.json", "version");
769
+ }
770
+ function detectManifests(cwd = process.cwd()) {
771
+ const detected = [];
772
+ for (const entry of DETECTION_TABLE) {
773
+ const provider = entry.factory();
774
+ if (provider.exists(cwd)) {
775
+ detected.push(entry.source);
776
+ }
777
+ }
778
+ return detected;
779
+ }
356
780
  function getPackageJsonPath(cwd = process.cwd()) {
357
781
  return path.join(cwd, "package.json");
358
782
  }
@@ -367,18 +791,30 @@ function writePackageJson(pkg, cwd = process.cwd()) {
367
791
  fs.writeFileSync(getPackageJsonPath(cwd), `${JSON.stringify(pkg, null, 2)}
368
792
  `, "utf-8");
369
793
  }
370
- function getPackageVersion(cwd = process.cwd()) {
794
+ function getPackageVersion(cwd = process.cwd(), manifest) {
795
+ if (manifest) {
796
+ const provider = resolveVersionSource(manifest, cwd);
797
+ return provider.getVersion(cwd);
798
+ }
371
799
  const pkg = readPackageJson(cwd);
372
800
  if (typeof pkg.version !== "string" || pkg.version.length === 0) {
373
801
  throw new Error("No version field in package.json");
374
802
  }
375
803
  return pkg.version;
376
804
  }
377
- function setPackageVersion(version, cwd = process.cwd()) {
805
+ function setPackageVersion(version, cwd = process.cwd(), manifest) {
806
+ if (manifest) {
807
+ const provider = resolveVersionSource(manifest, cwd);
808
+ provider.setVersion(version, cwd);
809
+ return;
810
+ }
378
811
  const pkg = readPackageJson(cwd);
379
812
  pkg.version = version;
380
813
  writePackageJson(pkg, cwd);
381
814
  }
815
+ function getVersionSource(manifest, cwd = process.cwd()) {
816
+ return resolveVersionSource(manifest, cwd);
817
+ }
382
818
  const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
383
819
  function parse(version) {
384
820
  const match = version.match(SEMVER_REGEX);
@@ -637,6 +1073,9 @@ const DEFAULT_CONFIG = {
637
1073
  preventFutureDates: true
638
1074
  }
639
1075
  },
1076
+ manifest: {
1077
+ source: "auto"
1078
+ },
640
1079
  sync: {
641
1080
  files: ["README.md", "CHANGELOG.md"],
642
1081
  patterns: [
@@ -896,7 +1335,7 @@ function getCalVerFeedback(version, calverConfig, previousVersion) {
896
1335
  });
897
1336
  suggestions.push({
898
1337
  message: `Expected format: ${format2}`,
899
- fix: `Update package.json to use current date: "${getCurrentVersion(format2)}"`,
1338
+ fix: `Update version to current date: "${getCurrentVersion(format2)}"`,
900
1339
  autoFixable: true
901
1340
  });
902
1341
  return { valid: false, errors, suggestions, canAutoFix: true };
@@ -1003,7 +1442,7 @@ function getChangelogFeedback(hasEntry, version, latestChangelogVersion) {
1003
1442
  }
1004
1443
  if (latestChangelogVersion && latestChangelogVersion !== version) {
1005
1444
  suggestions.push({
1006
- message: `CHANGELOG.md latest entry is ${latestChangelogVersion}, but package.json is ${version}`,
1445
+ message: `CHANGELOG.md latest entry is ${latestChangelogVersion}, but manifest version is ${version}`,
1007
1446
  fix: `Make sure versions are in sync`,
1008
1447
  autoFixable: false
1009
1448
  });
@@ -1014,7 +1453,7 @@ function getTagFeedback(tagVersion, packageVersion, hasUnsyncedFiles) {
1014
1453
  const suggestions = [];
1015
1454
  if (tagVersion !== packageVersion) {
1016
1455
  suggestions.push({
1017
- message: `Git tag "${tagVersion}" doesn't match package.json "${packageVersion}"`,
1456
+ message: `Git tag "${tagVersion}" doesn't match manifest version "${packageVersion}"`,
1018
1457
  fix: `Delete tag and recreate: git tag -d ${tagVersion} && git tag ${packageVersion}`,
1019
1458
  autoFixable: false
1020
1459
  });
@@ -1028,25 +1467,43 @@ function getTagFeedback(tagVersion, packageVersion, hasUnsyncedFiles) {
1028
1467
  }
1029
1468
  return suggestions;
1030
1469
  }
1031
- function fixPackageVersion(targetVersion, cwd = process.cwd()) {
1032
- const packagePath = path.join(cwd, "package.json");
1033
- if (!fs.existsSync(packagePath)) {
1034
- return { fixed: false, message: "package.json not found" };
1470
+ function fixPackageVersion(targetVersion, cwd = process.cwd(), manifest) {
1471
+ if (!manifest) {
1472
+ const packagePath = path.join(cwd, "package.json");
1473
+ if (!fs.existsSync(packagePath)) {
1474
+ return { fixed: false, message: "package.json not found" };
1475
+ }
1476
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1477
+ const oldVersion2 = typeof pkg.version === "string" ? pkg.version : void 0;
1478
+ if (oldVersion2 === targetVersion) {
1479
+ return { fixed: false, message: `Already at version ${targetVersion}` };
1480
+ }
1481
+ setPackageVersion(targetVersion, cwd);
1482
+ return {
1483
+ fixed: true,
1484
+ message: `Updated package.json from ${oldVersion2} to ${targetVersion}`,
1485
+ file: packagePath
1486
+ };
1487
+ }
1488
+ const provider = getVersionSource(manifest, cwd);
1489
+ let oldVersion;
1490
+ try {
1491
+ oldVersion = provider.getVersion(cwd);
1492
+ } catch {
1493
+ return { fixed: false, message: "Version source not found" };
1035
1494
  }
1036
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1037
- const oldVersion = typeof pkg.version === "string" ? pkg.version : void 0;
1038
1495
  if (oldVersion === targetVersion) {
1039
1496
  return { fixed: false, message: `Already at version ${targetVersion}` };
1040
1497
  }
1041
- setPackageVersion(targetVersion, cwd);
1498
+ provider.setVersion(targetVersion, cwd);
1042
1499
  return {
1043
1500
  fixed: true,
1044
- message: `Updated package.json from ${oldVersion} to ${targetVersion}`,
1045
- file: packagePath
1501
+ message: `Updated version from ${oldVersion} to ${targetVersion}`,
1502
+ file: provider.manifestFile ? path.join(cwd, provider.manifestFile) : void 0
1046
1503
  };
1047
1504
  }
1048
1505
  function fixSyncIssues(config, cwd = process.cwd()) {
1049
- const version = getPackageVersion(cwd);
1506
+ const version = getPackageVersion(cwd, config.manifest);
1050
1507
  const results = syncVersion(version, config.sync, cwd).filter((result) => result.updated).map((result) => ({
1051
1508
  fixed: true,
1052
1509
  message: `Updated ${path.relative(cwd, result.file)} (${result.changes.length} changes)`,
@@ -1102,9 +1559,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1102
1559
  }
1103
1560
  function fixAll(config, targetVersion, cwd = process.cwd()) {
1104
1561
  const results = [];
1105
- const version = targetVersion || getPackageVersion(cwd);
1106
- if (targetVersion && targetVersion !== getPackageVersion(cwd)) {
1107
- results.push(fixPackageVersion(targetVersion, cwd));
1562
+ const version = targetVersion || getPackageVersion(cwd, config.manifest);
1563
+ if (targetVersion && targetVersion !== getPackageVersion(cwd, config.manifest)) {
1564
+ results.push(fixPackageVersion(targetVersion, cwd, config.manifest));
1108
1565
  }
1109
1566
  const syncResults = fixSyncIssues(config, cwd);
1110
1567
  results.push(...syncResults);
@@ -1165,6 +1622,15 @@ function checkHooksPathOverride(cwd) {
1165
1622
  encoding: "utf-8"
1166
1623
  }).trim();
1167
1624
  if (hooksPath) {
1625
+ const resolved = path.resolve(cwd, hooksPath);
1626
+ const huskyDir = path.resolve(cwd, ".husky");
1627
+ if (resolved === huskyDir || resolved.startsWith(`${huskyDir}${path.sep}`)) {
1628
+ return {
1629
+ code: "HOOKS_PATH_HUSKY",
1630
+ severity: "warning",
1631
+ message: `Husky detected — core.hooksPath is set to "${hooksPath}". Hooks in .git/hooks/ are bypassed. Add versionguard validate to your .husky/pre-commit manually or use a tool like forge-ts that manages .husky/ hooks cooperatively.`
1632
+ };
1633
+ }
1168
1634
  return {
1169
1635
  code: "HOOKS_PATH_OVERRIDE",
1170
1636
  severity: "error",
@@ -1315,7 +1781,7 @@ function createTag(version, message, autoFix = true, config, cwd = process.cwd()
1315
1781
  actions
1316
1782
  };
1317
1783
  }
1318
- const packageVersion = getPackageVersion(cwd);
1784
+ const packageVersion = getPackageVersion(cwd, config.manifest);
1319
1785
  const shouldAutoFix = autoFix;
1320
1786
  const preflightError = getTagPreflightError(config, cwd, version, shouldAutoFix);
1321
1787
  if (preflightError) {
@@ -1328,7 +1794,7 @@ function createTag(version, message, autoFix = true, config, cwd = process.cwd()
1328
1794
  if (version !== packageVersion && !autoFix) {
1329
1795
  return {
1330
1796
  success: false,
1331
- message: `Version mismatch: package.json is ${packageVersion}, tag is ${version}`,
1797
+ message: `Version mismatch: manifest version is ${packageVersion}, tag is ${version}`,
1332
1798
  actions: []
1333
1799
  };
1334
1800
  }
@@ -1388,15 +1854,15 @@ function handlePostTag(config, cwd = process.cwd()) {
1388
1854
  actions
1389
1855
  };
1390
1856
  }
1391
- const packageVersion = getPackageVersion(cwd);
1857
+ const packageVersion = getPackageVersion(cwd, config.manifest);
1392
1858
  if (tag.version !== packageVersion) {
1393
1859
  return {
1394
1860
  success: false,
1395
- message: `Tag version ${tag.version} doesn't match package.json ${packageVersion}`,
1861
+ message: `Tag version ${tag.version} doesn't match manifest version ${packageVersion}`,
1396
1862
  actions: [
1397
1863
  "To fix: delete tag and recreate with correct version",
1398
1864
  ` git tag -d ${tag.name}`,
1399
- ` npm version ${tag.version}`,
1865
+ ` Update manifest to ${tag.version}`,
1400
1866
  ` git tag ${tag.name}`
1401
1867
  ]
1402
1868
  };
@@ -1427,7 +1893,7 @@ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false
1427
1893
  if (hasDirtyWorktree(cwd)) {
1428
1894
  return "Working tree must be clean before creating or validating release tags";
1429
1895
  }
1430
- const version = expectedVersion ?? getPackageVersion(cwd);
1896
+ const version = expectedVersion ?? getPackageVersion(cwd, config.manifest);
1431
1897
  const versionResult = config.versioning.type === "semver" ? validate$1(version) : validate$2(
1432
1898
  version,
1433
1899
  config.versioning.calver?.format ?? "YYYY.MM.PATCH",
@@ -1521,7 +1987,7 @@ function validate(config, cwd = process.cwd()) {
1521
1987
  const errors = [];
1522
1988
  let version;
1523
1989
  try {
1524
- version = getPackageVersion(cwd);
1990
+ version = getPackageVersion(cwd, config.manifest);
1525
1991
  } catch (err) {
1526
1992
  return {
1527
1993
  valid: false,
@@ -1592,7 +2058,7 @@ function doctor(config, cwd = process.cwd()) {
1592
2058
  };
1593
2059
  }
1594
2060
  function sync(config, cwd = process.cwd()) {
1595
- const version = getPackageVersion(cwd);
2061
+ const version = getPackageVersion(cwd, config.manifest);
1596
2062
  syncVersion(version, config.sync, cwd);
1597
2063
  }
1598
2064
  function canBump(currentVersion, newVersion, config) {
@@ -1641,30 +2107,39 @@ function isWorktreeClean(cwd) {
1641
2107
  }
1642
2108
  }
1643
2109
  export {
1644
- fixPackageVersion as A,
1645
- getAllTags as B,
1646
- getLatestTag as C,
1647
- getTagFeedback as D,
1648
- semver as E,
1649
- suggestTagMessage as F,
1650
- sync as G,
1651
- syncVersion as H,
1652
- validateChangelog as I,
1653
- validateTagForPush as J,
1654
- validateVersion as K,
1655
- getPackageVersion as a,
1656
- getVersionFeedback as b,
1657
- getSyncFeedback as c,
1658
- getChangelogFeedback as d,
1659
- doctor as e,
1660
- fixAll as f,
2110
+ fixChangelog as A,
2111
+ fixPackageVersion as B,
2112
+ getAllTags as C,
2113
+ getLatestTag as D,
2114
+ getTagFeedback as E,
2115
+ getVersionSource as F,
2116
+ GitTagSource as G,
2117
+ resolveVersionSource as H,
2118
+ semver as I,
2119
+ JsonVersionSource as J,
2120
+ suggestTagMessage as K,
2121
+ sync as L,
2122
+ syncVersion as M,
2123
+ validateChangelog as N,
2124
+ validateTagForPush as O,
2125
+ validateVersion as P,
2126
+ RegexVersionSource as R,
2127
+ TomlVersionSource as T,
2128
+ VersionFileSource as V,
2129
+ YamlVersionSource as Y,
2130
+ installHooks as a,
2131
+ getPackageVersion as b,
2132
+ getVersionFeedback as c,
2133
+ getSyncFeedback as d,
2134
+ getChangelogFeedback as e,
2135
+ doctor as f,
1661
2136
  getConfig as g,
1662
2137
  handlePostTag as h,
1663
2138
  initConfig as i,
1664
- fixSyncIssues as j,
1665
- setPackageVersion as k,
1666
- createTag as l,
1667
- installHooks as m,
2139
+ fixAll as j,
2140
+ fixSyncIssues as k,
2141
+ setPackageVersion as l,
2142
+ createTag as m,
1668
2143
  areHooksInstalled as n,
1669
2144
  calver as o,
1670
2145
  canBump as p,
@@ -1677,6 +2152,6 @@ export {
1677
2152
  checkHookIntegrity as w,
1678
2153
  checkHooksPathOverride as x,
1679
2154
  checkHuskyBypass as y,
1680
- fixChangelog as z
2155
+ detectManifests as z
1681
2156
  };
1682
- //# sourceMappingURL=index-BwE_OaV3.js.map
2157
+ //# sourceMappingURL=index-BrZJDWya.js.map