@codluv/versionguard 0.1.1 → 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.
Files changed (56) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/dist/calver.d.ts +0 -0
  4. package/dist/calver.d.ts.map +0 -0
  5. package/dist/changelog.d.ts +0 -0
  6. package/dist/changelog.d.ts.map +0 -0
  7. package/dist/chunks/{index-C6jrxye7.js → index-BrZJDWya.js} +645 -58
  8. package/dist/chunks/index-BrZJDWya.js.map +1 -0
  9. package/dist/cli.d.ts +0 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +61 -19
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.d.ts +0 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/feedback/index.d.ts +0 -0
  16. package/dist/feedback/index.d.ts.map +0 -0
  17. package/dist/fix/index.d.ts +2 -2
  18. package/dist/fix/index.d.ts.map +1 -1
  19. package/dist/guard.d.ts +104 -0
  20. package/dist/guard.d.ts.map +1 -0
  21. package/dist/hooks.d.ts +0 -0
  22. package/dist/hooks.d.ts.map +1 -1
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +37 -23
  26. package/dist/index.js.map +0 -0
  27. package/dist/project.d.ts +33 -10
  28. package/dist/project.d.ts.map +1 -1
  29. package/dist/semver.d.ts +0 -0
  30. package/dist/semver.d.ts.map +0 -0
  31. package/dist/sources/git-tag.d.ts +23 -0
  32. package/dist/sources/git-tag.d.ts.map +1 -0
  33. package/dist/sources/index.d.ts +15 -0
  34. package/dist/sources/index.d.ts.map +1 -0
  35. package/dist/sources/json.d.ts +22 -0
  36. package/dist/sources/json.d.ts.map +1 -0
  37. package/dist/sources/provider.d.ts +25 -0
  38. package/dist/sources/provider.d.ts.map +1 -0
  39. package/dist/sources/regex.d.ts +26 -0
  40. package/dist/sources/regex.d.ts.map +1 -0
  41. package/dist/sources/resolve.d.ts +35 -0
  42. package/dist/sources/resolve.d.ts.map +1 -0
  43. package/dist/sources/toml.d.ts +25 -0
  44. package/dist/sources/toml.d.ts.map +1 -0
  45. package/dist/sources/version-file.d.ts +24 -0
  46. package/dist/sources/version-file.d.ts.map +1 -0
  47. package/dist/sources/yaml.d.ts +24 -0
  48. package/dist/sources/yaml.d.ts.map +1 -0
  49. package/dist/sync.d.ts +0 -0
  50. package/dist/sync.d.ts.map +0 -0
  51. package/dist/tag/index.d.ts +0 -0
  52. package/dist/tag/index.d.ts.map +0 -0
  53. package/dist/types.d.ts +49 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/package.json +7 -7
  56. package/dist/chunks/index-C6jrxye7.js.map +0 -1
@@ -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,10 +286,10 @@ 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
- const HOOK_NAMES = ["pre-commit", "pre-push", "post-tag"];
292
+ const HOOK_NAMES$1 = ["pre-commit", "pre-push", "post-tag"];
292
293
  function installHooks(config, cwd = process.cwd()) {
293
294
  const gitDir = findGitDir(cwd);
294
295
  if (!gitDir) {
@@ -296,7 +297,7 @@ function installHooks(config, cwd = process.cwd()) {
296
297
  }
297
298
  const hooksDir = path.join(gitDir, "hooks");
298
299
  fs.mkdirSync(hooksDir, { recursive: true });
299
- for (const hookName of HOOK_NAMES) {
300
+ for (const hookName of HOOK_NAMES$1) {
300
301
  if (config.hooks[hookName]) {
301
302
  const hookPath = path.join(hooksDir, hookName);
302
303
  fs.writeFileSync(hookPath, generateHookScript(hookName), { encoding: "utf-8", mode: 493 });
@@ -309,7 +310,7 @@ function uninstallHooks(cwd = process.cwd()) {
309
310
  return;
310
311
  }
311
312
  const hooksDir = path.join(gitDir, "hooks");
312
- for (const hookName of HOOK_NAMES) {
313
+ for (const hookName of HOOK_NAMES$1) {
313
314
  const hookPath = path.join(hooksDir, hookName);
314
315
  if (fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard")) {
315
316
  fs.unlinkSync(hookPath);
@@ -335,13 +336,14 @@ function areHooksInstalled(cwd = process.cwd()) {
335
336
  if (!gitDir) {
336
337
  return false;
337
338
  }
338
- return HOOK_NAMES.every((hookName) => {
339
+ return HOOK_NAMES$1.every((hookName) => {
339
340
  const hookPath = path.join(gitDir, "hooks", hookName);
340
341
  return fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard");
341
342
  });
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);
@@ -1157,6 +1614,122 @@ function getCalVerConfig$1(config) {
1157
1614
  }
1158
1615
  return config.versioning.calver;
1159
1616
  }
1617
+ const HOOK_NAMES = ["pre-commit", "pre-push", "post-tag"];
1618
+ function checkHooksPathOverride(cwd) {
1619
+ try {
1620
+ const hooksPath = execSync("git config core.hooksPath", {
1621
+ cwd,
1622
+ encoding: "utf-8"
1623
+ }).trim();
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
+ }
1634
+ return {
1635
+ code: "HOOKS_PATH_OVERRIDE",
1636
+ severity: "error",
1637
+ message: `git core.hooksPath is set to "${hooksPath}" — hooks in .git/hooks/ are bypassed`,
1638
+ fix: "git config --unset core.hooksPath"
1639
+ };
1640
+ }
1641
+ } catch {
1642
+ }
1643
+ return null;
1644
+ }
1645
+ function checkHuskyBypass() {
1646
+ if (process.env.HUSKY === "0") {
1647
+ return {
1648
+ code: "HUSKY_BYPASS",
1649
+ severity: "error",
1650
+ message: "HUSKY=0 is set — git hooks are disabled via environment variable",
1651
+ fix: "unset HUSKY"
1652
+ };
1653
+ }
1654
+ return null;
1655
+ }
1656
+ function checkHookIntegrity(config, cwd) {
1657
+ const warnings = [];
1658
+ const gitDir = findGitDir(cwd);
1659
+ if (!gitDir) {
1660
+ return warnings;
1661
+ }
1662
+ const hooksDir = path.join(gitDir, "hooks");
1663
+ for (const hookName of HOOK_NAMES) {
1664
+ if (!config.git.hooks[hookName]) {
1665
+ continue;
1666
+ }
1667
+ const hookPath = path.join(hooksDir, hookName);
1668
+ if (!fs.existsSync(hookPath)) {
1669
+ warnings.push({
1670
+ code: "HOOK_MISSING",
1671
+ severity: "error",
1672
+ message: `Required hook "${hookName}" is not installed`,
1673
+ fix: "npx versionguard hooks install"
1674
+ });
1675
+ continue;
1676
+ }
1677
+ const actual = fs.readFileSync(hookPath, "utf-8");
1678
+ const expected = generateHookScript(hookName);
1679
+ if (actual !== expected) {
1680
+ if (!actual.includes("versionguard")) {
1681
+ warnings.push({
1682
+ code: "HOOK_REPLACED",
1683
+ severity: "error",
1684
+ message: `Hook "${hookName}" has been replaced — versionguard invocation is missing`,
1685
+ fix: "npx versionguard hooks install"
1686
+ });
1687
+ } else {
1688
+ warnings.push({
1689
+ code: "HOOK_TAMPERED",
1690
+ severity: "warning",
1691
+ message: `Hook "${hookName}" has been modified from the expected template`,
1692
+ fix: "npx versionguard hooks install"
1693
+ });
1694
+ }
1695
+ }
1696
+ }
1697
+ return warnings;
1698
+ }
1699
+ function checkEnforceHooksPolicy(config) {
1700
+ const anyHookEnabled = HOOK_NAMES.some((name) => config.git.hooks[name]);
1701
+ if (anyHookEnabled && !config.git.enforceHooks) {
1702
+ return {
1703
+ code: "HOOKS_NOT_ENFORCED",
1704
+ severity: "warning",
1705
+ message: "Hooks are enabled but enforceHooks is false — missing hooks will not fail validation",
1706
+ fix: "Set git.enforceHooks: true in .versionguard.yml"
1707
+ };
1708
+ }
1709
+ return null;
1710
+ }
1711
+ function runGuardChecks(config, cwd) {
1712
+ const warnings = [];
1713
+ const hooksPathWarning = checkHooksPathOverride(cwd);
1714
+ if (hooksPathWarning) {
1715
+ warnings.push(hooksPathWarning);
1716
+ }
1717
+ const huskyWarning = checkHuskyBypass();
1718
+ if (huskyWarning) {
1719
+ warnings.push(huskyWarning);
1720
+ }
1721
+ const integrityWarnings = checkHookIntegrity(config, cwd);
1722
+ warnings.push(...integrityWarnings);
1723
+ const enforceWarning = checkEnforceHooksPolicy(config);
1724
+ if (enforceWarning) {
1725
+ warnings.push(enforceWarning);
1726
+ }
1727
+ const hasErrors = warnings.some((w) => w.severity === "error");
1728
+ return {
1729
+ safe: !hasErrors,
1730
+ warnings
1731
+ };
1732
+ }
1160
1733
  function runGit(cwd, args, encoding) {
1161
1734
  return childProcess.execFileSync("git", args, {
1162
1735
  cwd,
@@ -1208,7 +1781,7 @@ function createTag(version, message, autoFix = true, config, cwd = process.cwd()
1208
1781
  actions
1209
1782
  };
1210
1783
  }
1211
- const packageVersion = getPackageVersion(cwd);
1784
+ const packageVersion = getPackageVersion(cwd, config.manifest);
1212
1785
  const shouldAutoFix = autoFix;
1213
1786
  const preflightError = getTagPreflightError(config, cwd, version, shouldAutoFix);
1214
1787
  if (preflightError) {
@@ -1221,7 +1794,7 @@ function createTag(version, message, autoFix = true, config, cwd = process.cwd()
1221
1794
  if (version !== packageVersion && !autoFix) {
1222
1795
  return {
1223
1796
  success: false,
1224
- message: `Version mismatch: package.json is ${packageVersion}, tag is ${version}`,
1797
+ message: `Version mismatch: manifest version is ${packageVersion}, tag is ${version}`,
1225
1798
  actions: []
1226
1799
  };
1227
1800
  }
@@ -1281,15 +1854,15 @@ function handlePostTag(config, cwd = process.cwd()) {
1281
1854
  actions
1282
1855
  };
1283
1856
  }
1284
- const packageVersion = getPackageVersion(cwd);
1857
+ const packageVersion = getPackageVersion(cwd, config.manifest);
1285
1858
  if (tag.version !== packageVersion) {
1286
1859
  return {
1287
1860
  success: false,
1288
- message: `Tag version ${tag.version} doesn't match package.json ${packageVersion}`,
1861
+ message: `Tag version ${tag.version} doesn't match manifest version ${packageVersion}`,
1289
1862
  actions: [
1290
1863
  "To fix: delete tag and recreate with correct version",
1291
1864
  ` git tag -d ${tag.name}`,
1292
- ` npm version ${tag.version}`,
1865
+ ` Update manifest to ${tag.version}`,
1293
1866
  ` git tag ${tag.name}`
1294
1867
  ]
1295
1868
  };
@@ -1320,7 +1893,7 @@ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false
1320
1893
  if (hasDirtyWorktree(cwd)) {
1321
1894
  return "Working tree must be clean before creating or validating release tags";
1322
1895
  }
1323
- const version = expectedVersion ?? getPackageVersion(cwd);
1896
+ const version = expectedVersion ?? getPackageVersion(cwd, config.manifest);
1324
1897
  const versionResult = config.versioning.type === "semver" ? validate$1(version) : validate$2(
1325
1898
  version,
1326
1899
  config.versioning.calver?.format ?? "YYYY.MM.PATCH",
@@ -1414,7 +1987,7 @@ function validate(config, cwd = process.cwd()) {
1414
1987
  const errors = [];
1415
1988
  let version;
1416
1989
  try {
1417
- version = getPackageVersion(cwd);
1990
+ version = getPackageVersion(cwd, config.manifest);
1418
1991
  } catch (err) {
1419
1992
  return {
1420
1993
  valid: false,
@@ -1485,7 +2058,7 @@ function doctor(config, cwd = process.cwd()) {
1485
2058
  };
1486
2059
  }
1487
2060
  function sync(config, cwd = process.cwd()) {
1488
- const version = getPackageVersion(cwd);
2061
+ const version = getPackageVersion(cwd, config.manifest);
1489
2062
  syncVersion(version, config.sync, cwd);
1490
2063
  }
1491
2064
  function canBump(currentVersion, newVersion, config) {
@@ -1534,37 +2107,51 @@ function isWorktreeClean(cwd) {
1534
2107
  }
1535
2108
  }
1536
2109
  export {
1537
- suggestTagMessage as A,
1538
- sync as B,
1539
- syncVersion as C,
1540
- validateChangelog as D,
1541
- validateTagForPush as E,
1542
- validateVersion as F,
1543
- getPackageVersion as a,
1544
- getVersionFeedback as b,
1545
- getSyncFeedback as c,
1546
- getChangelogFeedback as d,
1547
- doctor as e,
1548
- 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,
1549
2136
  getConfig as g,
1550
2137
  handlePostTag as h,
1551
2138
  initConfig as i,
1552
- fixSyncIssues as j,
1553
- setPackageVersion as k,
1554
- createTag as l,
1555
- installHooks as m,
2139
+ fixAll as j,
2140
+ fixSyncIssues as k,
2141
+ setPackageVersion as l,
2142
+ createTag as m,
1556
2143
  areHooksInstalled as n,
1557
2144
  calver as o,
1558
2145
  canBump as p,
1559
- checkHardcodedVersions as q,
1560
- fixChangelog as r,
2146
+ checkEnforceHooksPolicy as q,
2147
+ runGuardChecks as r,
1561
2148
  suggestNextVersion as s,
1562
- fixPackageVersion as t,
2149
+ checkHardcodedVersions as t,
1563
2150
  uninstallHooks as u,
1564
2151
  validate as v,
1565
- getAllTags as w,
1566
- getLatestTag as x,
1567
- getTagFeedback as y,
1568
- semver as z
2152
+ checkHookIntegrity as w,
2153
+ checkHooksPathOverride as x,
2154
+ checkHuskyBypass as y,
2155
+ detectManifests as z
1569
2156
  };
1570
- //# sourceMappingURL=index-C6jrxye7.js.map
2157
+ //# sourceMappingURL=index-BrZJDWya.js.map