@codluv/versionguard 0.2.0 → 0.4.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 (47) hide show
  1. package/dist/calver.d.ts +65 -22
  2. package/dist/calver.d.ts.map +1 -1
  3. package/dist/chunks/{index-BwE_OaV3.js → index-B3R60bYJ.js} +913 -138
  4. package/dist/chunks/index-B3R60bYJ.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +258 -22
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/feedback/index.d.ts +1 -1
  10. package/dist/feedback/index.d.ts.map +1 -1
  11. package/dist/fix/index.d.ts +7 -2
  12. package/dist/fix/index.d.ts.map +1 -1
  13. package/dist/guard.d.ts +45 -1
  14. package/dist/guard.d.ts.map +1 -1
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +33 -23
  19. package/dist/init-wizard.d.ts +49 -0
  20. package/dist/init-wizard.d.ts.map +1 -0
  21. package/dist/project.d.ts +54 -10
  22. package/dist/project.d.ts.map +1 -1
  23. package/dist/sources/git-tag.d.ts +52 -0
  24. package/dist/sources/git-tag.d.ts.map +1 -0
  25. package/dist/sources/index.d.ts +15 -0
  26. package/dist/sources/index.d.ts.map +1 -0
  27. package/dist/sources/json.d.ts +53 -0
  28. package/dist/sources/json.d.ts.map +1 -0
  29. package/dist/sources/provider.d.ts +25 -0
  30. package/dist/sources/provider.d.ts.map +1 -0
  31. package/dist/sources/regex.d.ts +56 -0
  32. package/dist/sources/regex.d.ts.map +1 -0
  33. package/dist/sources/resolve.d.ts +54 -0
  34. package/dist/sources/resolve.d.ts.map +1 -0
  35. package/dist/sources/toml.d.ts +60 -0
  36. package/dist/sources/toml.d.ts.map +1 -0
  37. package/dist/sources/utils.d.ts +73 -0
  38. package/dist/sources/utils.d.ts.map +1 -0
  39. package/dist/sources/version-file.d.ts +51 -0
  40. package/dist/sources/version-file.d.ts.map +1 -0
  41. package/dist/sources/yaml.d.ts +53 -0
  42. package/dist/sources/yaml.d.ts.map +1 -0
  43. package/dist/tag/index.d.ts.map +1 -1
  44. package/dist/types.d.ts +138 -5
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +4 -2
  47. package/dist/chunks/index-BwE_OaV3.js.map +0 -1
@@ -1,42 +1,88 @@
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";
9
+ const VALID_TOKENS = /* @__PURE__ */ new Set([
10
+ "YYYY",
11
+ "YY",
12
+ "0Y",
13
+ "MM",
14
+ "M",
15
+ "0M",
16
+ "WW",
17
+ "0W",
18
+ "DD",
19
+ "D",
20
+ "0D",
21
+ "MICRO",
22
+ "PATCH"
23
+ ]);
24
+ const YEAR_TOKENS = /* @__PURE__ */ new Set(["YYYY", "YY", "0Y"]);
25
+ const MONTH_TOKENS = /* @__PURE__ */ new Set(["MM", "M", "0M"]);
26
+ const WEEK_TOKENS = /* @__PURE__ */ new Set(["WW", "0W"]);
27
+ const DAY_TOKENS = /* @__PURE__ */ new Set(["DD", "D", "0D"]);
28
+ const COUNTER_TOKENS = /* @__PURE__ */ new Set(["MICRO", "PATCH"]);
29
+ function isValidCalVerFormat(formatStr) {
30
+ const tokens = formatStr.split(".");
31
+ if (tokens.length < 2) return false;
32
+ if (!tokens.every((t) => VALID_TOKENS.has(t))) return false;
33
+ if (!YEAR_TOKENS.has(tokens[0])) return false;
34
+ const hasWeek = tokens.some((t) => WEEK_TOKENS.has(t));
35
+ const hasMonthOrDay = tokens.some((t) => MONTH_TOKENS.has(t) || DAY_TOKENS.has(t));
36
+ if (hasWeek && hasMonthOrDay) return false;
37
+ const counterIndex = tokens.findIndex((t) => COUNTER_TOKENS.has(t));
38
+ if (counterIndex !== -1 && counterIndex !== tokens.length - 1) return false;
39
+ return true;
40
+ }
8
41
  function parseFormat(calverFormat) {
9
- const parts = calverFormat.split(".");
42
+ const tokens = calverFormat.split(".");
10
43
  const result = {
11
- year: parts[0],
12
- month: parts[1]
44
+ year: tokens[0]
13
45
  };
14
- if (parts[2] === "PATCH") {
15
- result.patch = "PATCH";
16
- } else if (parts[2]) {
17
- result.day = parts[2];
18
- }
19
- if (parts[3] === "PATCH") {
20
- result.patch = "PATCH";
46
+ for (let i = 1; i < tokens.length; i++) {
47
+ const token = tokens[i];
48
+ if (MONTH_TOKENS.has(token)) {
49
+ result.month = token;
50
+ } else if (WEEK_TOKENS.has(token)) {
51
+ result.week = token;
52
+ } else if (DAY_TOKENS.has(token)) {
53
+ result.day = token;
54
+ } else if (COUNTER_TOKENS.has(token)) {
55
+ result.counter = token;
56
+ }
21
57
  }
22
58
  return result;
23
59
  }
60
+ const MODIFIER_PATTERN = "(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?";
24
61
  function tokenPattern(token) {
25
62
  switch (token) {
26
63
  case "YYYY":
27
- return "(\\d{4})";
64
+ return "([1-9]\\d{3})";
28
65
  case "YY":
29
- return "(\\d{2})";
30
- case "0M":
31
- case "0D":
32
- return "(\\d{2})";
66
+ return "(\\d{1,3})";
67
+ case "0Y":
68
+ return "(\\d{2,3})";
33
69
  case "MM":
34
- case "DD":
35
70
  case "M":
71
+ return "([1-9]|1[0-2])";
72
+ case "0M":
73
+ return "(0[1-9]|1[0-2])";
74
+ case "WW":
75
+ return "([1-9]|[1-4]\\d|5[0-3])";
76
+ case "0W":
77
+ return "(0[1-9]|[1-4]\\d|5[0-3])";
78
+ case "DD":
36
79
  case "D":
37
- return "(\\d{1,2})";
80
+ return "([1-9]|[12]\\d|3[01])";
81
+ case "0D":
82
+ return "(0[1-9]|[12]\\d|3[01])";
83
+ case "MICRO":
38
84
  case "PATCH":
39
- return "(\\d+)";
85
+ return "(0|[1-9]\\d*)";
40
86
  default:
41
87
  throw new Error(`Unsupported CalVer token: ${token}`);
42
88
  }
@@ -44,7 +90,7 @@ function tokenPattern(token) {
44
90
  function getRegexForFormat(calverFormat) {
45
91
  const tokens = calverFormat.split(".");
46
92
  const pattern = tokens.map(tokenPattern).join("\\.");
47
- return new RegExp(`^${pattern}$`);
93
+ return new RegExp(`^${pattern}${MODIFIER_PATTERN}$`);
48
94
  }
49
95
  function parse$1(version, calverFormat) {
50
96
  const match = version.match(getRegexForFormat(calverFormat));
@@ -52,28 +98,44 @@ function parse$1(version, calverFormat) {
52
98
  return null;
53
99
  }
54
100
  const definition = parseFormat(calverFormat);
55
- const year = definition.year === "YYYY" ? Number.parseInt(match[1], 10) : 2e3 + Number.parseInt(match[1], 10);
56
- const month = Number.parseInt(match[2], 10);
57
- let cursor = 3;
101
+ const yearToken = definition.year;
102
+ let year = Number.parseInt(match[1], 10);
103
+ if (yearToken === "YY" || yearToken === "0Y") {
104
+ year = 2e3 + year;
105
+ }
106
+ let cursor = 2;
107
+ let month;
58
108
  let day;
59
109
  let patch;
110
+ if (definition.month) {
111
+ month = Number.parseInt(match[cursor], 10);
112
+ cursor += 1;
113
+ }
114
+ if (definition.week) {
115
+ month = Number.parseInt(match[cursor], 10);
116
+ cursor += 1;
117
+ }
60
118
  if (definition.day) {
61
119
  day = Number.parseInt(match[cursor], 10);
62
120
  cursor += 1;
63
121
  }
64
- if (definition.patch) {
122
+ if (definition.counter) {
65
123
  patch = Number.parseInt(match[cursor], 10);
124
+ cursor += 1;
66
125
  }
126
+ const modifierGroup = match[cursor];
127
+ const modifier = modifierGroup || void 0;
67
128
  return {
68
129
  year,
69
- month,
130
+ month: month ?? 1,
70
131
  day,
71
132
  patch,
133
+ modifier,
72
134
  format: calverFormat,
73
135
  raw: version
74
136
  };
75
137
  }
76
- function validate$2(version, calverFormat, preventFutureDates = true) {
138
+ function validate$2(version, calverFormat, preventFutureDates = true, schemeRules) {
77
139
  const errors = [];
78
140
  const parsed = parse$1(version, calverFormat);
79
141
  if (!parsed) {
@@ -87,19 +149,26 @@ function validate$2(version, calverFormat, preventFutureDates = true) {
87
149
  ]
88
150
  };
89
151
  }
90
- if (parsed.month < 1 || parsed.month > 12) {
152
+ const definition = parseFormat(calverFormat);
153
+ if (definition.month && (parsed.month < 1 || parsed.month > 12)) {
91
154
  errors.push({
92
155
  message: `Invalid month: ${parsed.month}. Must be between 1 and 12.`,
93
156
  severity: "error"
94
157
  });
95
158
  }
159
+ if (definition.week && (parsed.month < 1 || parsed.month > 53)) {
160
+ errors.push({
161
+ message: `Invalid week: ${parsed.month}. Must be between 1 and 53.`,
162
+ severity: "error"
163
+ });
164
+ }
96
165
  if (parsed.day !== void 0) {
97
166
  if (parsed.day < 1 || parsed.day > 31) {
98
167
  errors.push({
99
168
  message: `Invalid day: ${parsed.day}. Must be between 1 and 31.`,
100
169
  severity: "error"
101
170
  });
102
- } else {
171
+ } else if (definition.month) {
103
172
  const daysInMonth = new Date(parsed.year, parsed.month, 0).getDate();
104
173
  if (parsed.day > daysInMonth) {
105
174
  errors.push({
@@ -119,56 +188,83 @@ function validate$2(version, calverFormat, preventFutureDates = true) {
119
188
  message: `Future year not allowed: ${parsed.year}. Current year is ${currentYear}.`,
120
189
  severity: "error"
121
190
  });
122
- } else if (parsed.year === currentYear && parsed.month > currentMonth) {
191
+ } else if (definition.month && parsed.year === currentYear && parsed.month > currentMonth) {
123
192
  errors.push({
124
193
  message: `Future month not allowed: ${parsed.year}.${parsed.month}. Current month is ${currentMonth}.`,
125
194
  severity: "error"
126
195
  });
127
- } else if (parsed.year === currentYear && parsed.month === currentMonth && parsed.day !== void 0 && parsed.day > currentDay) {
196
+ } else if (definition.month && parsed.year === currentYear && parsed.month === currentMonth && parsed.day !== void 0 && parsed.day > currentDay) {
128
197
  errors.push({
129
198
  message: `Future day not allowed: ${parsed.year}.${parsed.month}.${parsed.day}. Current day is ${currentDay}.`,
130
199
  severity: "error"
131
200
  });
132
201
  }
133
202
  }
203
+ if (parsed.modifier && schemeRules?.allowedModifiers) {
204
+ const baseModifier = parsed.modifier.replace(/[\d.]+$/, "") || parsed.modifier;
205
+ if (!schemeRules.allowedModifiers.includes(baseModifier)) {
206
+ errors.push({
207
+ message: `Modifier "${parsed.modifier}" is not allowed. Allowed: ${schemeRules.allowedModifiers.join(", ")}`,
208
+ severity: "error"
209
+ });
210
+ }
211
+ }
212
+ if (schemeRules?.maxNumericSegments) {
213
+ const segmentCount = calverFormat.split(".").length;
214
+ if (segmentCount > schemeRules.maxNumericSegments) {
215
+ errors.push({
216
+ message: `Format has ${segmentCount} segments, convention recommends ${schemeRules.maxNumericSegments} or fewer`,
217
+ severity: "warning"
218
+ });
219
+ }
220
+ }
134
221
  return {
135
- valid: errors.length === 0,
222
+ valid: errors.filter((e) => e.severity === "error").length === 0,
136
223
  errors,
137
224
  version: { type: "calver", version: parsed }
138
225
  };
139
226
  }
140
227
  function formatToken(token, value) {
141
- if (token === "0M" || token === "0D") {
142
- return String(value).padStart(2, "0");
143
- }
144
- if (token === "YY") {
145
- return String(value % 100).padStart(2, "0");
228
+ switch (token) {
229
+ case "0M":
230
+ case "0D":
231
+ case "0W":
232
+ case "0Y":
233
+ return String(token === "0Y" ? value % 100 : value).padStart(2, "0");
234
+ case "YY":
235
+ return String(value % 100).padStart(2, "0");
236
+ default:
237
+ return String(value);
146
238
  }
147
- return String(value);
148
239
  }
149
240
  function format$1(version) {
150
- const tokens = version.format.split(".");
151
- const values = [version.year, version.month];
152
- if (tokens.includes("DD") || tokens.includes("D") || tokens.includes("0D")) {
153
- values.push(version.day ?? 1);
241
+ const definition = parseFormat(version.format);
242
+ const parts = [formatToken(definition.year, version.year)];
243
+ if (definition.month) {
244
+ parts.push(formatToken(definition.month, version.month));
245
+ }
246
+ if (definition.week) {
247
+ parts.push(formatToken(definition.week, version.month));
154
248
  }
155
- if (tokens.includes("PATCH")) {
156
- values.push(version.patch ?? 0);
249
+ if (definition.day) {
250
+ parts.push(formatToken(definition.day, version.day ?? 1));
251
+ }
252
+ if (definition.counter) {
253
+ parts.push(formatToken(definition.counter, version.patch ?? 0));
157
254
  }
158
- return tokens.map((token, index) => formatToken(token, values[index])).join(".");
255
+ const base = parts.join(".");
256
+ return version.modifier ? `${base}-${version.modifier}` : base;
159
257
  }
160
258
  function getCurrentVersion(calverFormat, now = /* @__PURE__ */ new Date()) {
161
259
  const definition = parseFormat(calverFormat);
162
- const currentDay = now.getDate();
163
260
  const base = {
164
261
  year: now.getFullYear(),
165
262
  month: now.getMonth() + 1,
166
- day: definition.day ? currentDay : void 0,
167
- patch: definition.patch ? 0 : void 0
263
+ day: definition.day ? now.getDate() : void 0,
264
+ patch: definition.counter ? 0 : void 0,
265
+ format: calverFormat
168
266
  };
169
- const day = base.day ?? currentDay;
170
- const patch = base.patch ?? 0;
171
- return formatToken(definition.year, base.year).concat(`.${formatToken(definition.month, base.month)}`).concat(definition.day ? `.${formatToken(definition.day, day)}` : "").concat(definition.patch ? `.${patch}` : "");
267
+ return format$1(base);
172
268
  }
173
269
  function compare$1(a, b, calverFormat) {
174
270
  const left = parse$1(a, calverFormat);
@@ -194,12 +290,11 @@ function increment$1(version, calverFormat) {
194
290
  const next = {
195
291
  ...parsed
196
292
  };
197
- if (definition.patch) {
198
- const patch = parsed.patch ?? 0;
199
- next.patch = patch + 1;
293
+ if (definition.counter) {
294
+ next.patch = (parsed.patch ?? 0) + 1;
200
295
  } else {
201
296
  next.patch = 0;
202
- next.format = `${calverFormat}.PATCH`;
297
+ next.format = `${calverFormat}.MICRO`;
203
298
  }
204
299
  return format$1(next);
205
300
  }
@@ -214,6 +309,7 @@ const calver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
214
309
  getNextVersions,
215
310
  getRegexForFormat,
216
311
  increment: increment$1,
312
+ isValidCalVerFormat,
217
313
  parse: parse$1,
218
314
  parseFormat,
219
315
  validate: validate$2
@@ -245,7 +341,7 @@ function validateChangelog(changelogPath, version, strict = true, requireEntry =
245
341
  errors.push("Changelog should include compare links at the bottom");
246
342
  }
247
343
  const versionHeaderMatch = content.match(
248
- new RegExp(`## \\[${escapeRegExp(version)}\\] - ([^\r
344
+ new RegExp(`## \\[${escapeRegExp$1(version)}\\] - ([^\r
249
345
  ]+)`)
250
346
  );
251
347
  if (requireEntry && hasEntryForVersion) {
@@ -285,7 +381,7 @@ function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Dat
285
381
  const updated = `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
286
382
  fs.writeFileSync(changelogPath, updated, "utf-8");
287
383
  }
288
- function escapeRegExp(value) {
384
+ function escapeRegExp$1(value) {
289
385
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
386
  }
291
387
  const HOOK_NAMES$1 = ["pre-commit", "pre-push", "post-tag"];
@@ -342,6 +438,7 @@ function areHooksInstalled(cwd = process.cwd()) {
342
438
  }
343
439
  function generateHookScript(hookName) {
344
440
  return `#!/bin/sh
441
+ # versionguard
345
442
  # VersionGuard ${hookName} hook
346
443
  # --no-install prevents accidentally downloading an unscoped package
347
444
  # if @codluv/versionguard is not installed locally
@@ -353,6 +450,632 @@ if [ $status -ne 0 ]; then
353
450
  fi
354
451
  `;
355
452
  }
453
+ class GitTagSource {
454
+ /** Human-readable provider name. */
455
+ name = "git-tag";
456
+ /** Empty string since git-tag has no manifest file. */
457
+ manifestFile = "";
458
+ /**
459
+ * Returns `true` when `cwd` is inside a Git repository.
460
+ *
461
+ * @param cwd - Project directory to check.
462
+ * @returns Whether a Git repository is found.
463
+ */
464
+ exists(cwd) {
465
+ try {
466
+ execFileSync("git", ["rev-parse", "--git-dir"], {
467
+ cwd,
468
+ stdio: ["pipe", "pipe", "ignore"]
469
+ });
470
+ return true;
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+ /**
476
+ * Reads the version string from the latest Git tag.
477
+ *
478
+ * @param cwd - Project directory containing the Git repository.
479
+ * @returns The version string extracted from the latest version tag.
480
+ */
481
+ getVersion(cwd) {
482
+ try {
483
+ const tag = this.describeVersionTag(cwd);
484
+ return tag.replace(/^v/, "");
485
+ } catch {
486
+ throw new Error("No version tags found. Create a tag first (e.g., git tag v0.1.0)");
487
+ }
488
+ }
489
+ /**
490
+ * Creates a new annotated Git tag for the given version.
491
+ *
492
+ * @param version - Version string to tag.
493
+ * @param cwd - Project directory containing the Git repository.
494
+ */
495
+ setVersion(version, cwd) {
496
+ const prefix = this.detectPrefix(cwd);
497
+ const tagName = `${prefix}${version}`;
498
+ execFileSync("git", ["tag", "-a", tagName, "-m", `Release ${version}`], {
499
+ cwd,
500
+ stdio: ["pipe", "pipe", "ignore"]
501
+ });
502
+ }
503
+ /** Try version-like tag patterns, fall back to any tag. */
504
+ describeVersionTag(cwd) {
505
+ try {
506
+ return execFileSync("git", ["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"], {
507
+ cwd,
508
+ encoding: "utf-8",
509
+ stdio: ["pipe", "pipe", "ignore"]
510
+ }).trim();
511
+ } catch {
512
+ }
513
+ try {
514
+ return execFileSync("git", ["describe", "--tags", "--abbrev=0", "--match", "[0-9]*"], {
515
+ cwd,
516
+ encoding: "utf-8",
517
+ stdio: ["pipe", "pipe", "ignore"]
518
+ }).trim();
519
+ } catch {
520
+ throw new Error("No version tags found");
521
+ }
522
+ }
523
+ /** Detect whether existing tags use a `v` prefix or not. */
524
+ detectPrefix(cwd) {
525
+ try {
526
+ const tag = this.describeVersionTag(cwd);
527
+ return tag.startsWith("v") ? "v" : "";
528
+ } catch {
529
+ return "v";
530
+ }
531
+ }
532
+ }
533
+ function getNestedValue(obj, dotPath) {
534
+ let current = obj;
535
+ for (const key of dotPath.split(".")) {
536
+ if (current === null || typeof current !== "object") {
537
+ return void 0;
538
+ }
539
+ current = current[key];
540
+ }
541
+ return current;
542
+ }
543
+ function setNestedValue(obj, dotPath, value) {
544
+ const keys = dotPath.split(".");
545
+ let current = obj;
546
+ for (let i = 0; i < keys.length - 1; i++) {
547
+ const next = current[keys[i]];
548
+ if (typeof next !== "object" || next === null) {
549
+ throw new Error(`Missing intermediate key '${keys.slice(0, i + 1).join(".")}' in manifest`);
550
+ }
551
+ current = next;
552
+ }
553
+ current[keys[keys.length - 1]] = value;
554
+ }
555
+ function escapeRegExp(value) {
556
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
557
+ }
558
+ class JsonVersionSource {
559
+ /** Human-readable provider name. */
560
+ name;
561
+ /** Filename of the JSON manifest (e.g. `'package.json'`). */
562
+ manifestFile;
563
+ /** Dotted key path to the version field within the JSON document. */
564
+ versionPath;
565
+ /**
566
+ * Creates a new JSON version source.
567
+ *
568
+ * @param manifestFile - JSON manifest filename.
569
+ * @param versionPath - Dotted key path to the version field.
570
+ */
571
+ constructor(manifestFile = "package.json", versionPath = "version") {
572
+ this.name = manifestFile;
573
+ this.manifestFile = manifestFile;
574
+ this.versionPath = versionPath;
575
+ }
576
+ /**
577
+ * Returns `true` when the manifest file exists in `cwd`.
578
+ *
579
+ * @param cwd - Project directory to check.
580
+ * @returns Whether the manifest file exists.
581
+ */
582
+ exists(cwd) {
583
+ return fs.existsSync(path.join(cwd, this.manifestFile));
584
+ }
585
+ /**
586
+ * Reads the version string from the JSON manifest.
587
+ *
588
+ * @param cwd - Project directory containing the manifest.
589
+ * @returns The version string extracted from the manifest.
590
+ */
591
+ getVersion(cwd) {
592
+ const filePath = path.join(cwd, this.manifestFile);
593
+ if (!fs.existsSync(filePath)) {
594
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
595
+ }
596
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
597
+ const version = getNestedValue(content, this.versionPath);
598
+ if (typeof version !== "string" || version.length === 0) {
599
+ throw new Error(`No version field in ${this.manifestFile}`);
600
+ }
601
+ return version;
602
+ }
603
+ /**
604
+ * Writes a version string to the JSON manifest, preserving indentation.
605
+ *
606
+ * @param version - Version string to write.
607
+ * @param cwd - Project directory containing the manifest.
608
+ */
609
+ setVersion(version, cwd) {
610
+ const filePath = path.join(cwd, this.manifestFile);
611
+ if (!fs.existsSync(filePath)) {
612
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
613
+ }
614
+ const raw = fs.readFileSync(filePath, "utf-8");
615
+ const indentMatch = raw.match(/^(\s+)"/m);
616
+ const indent = indentMatch?.[1]?.length ?? 2;
617
+ const content = JSON.parse(raw);
618
+ setNestedValue(content, this.versionPath, version);
619
+ fs.writeFileSync(filePath, `${JSON.stringify(content, null, indent)}
620
+ `, "utf-8");
621
+ }
622
+ }
623
+ class RegexVersionSource {
624
+ /** Human-readable provider name. */
625
+ name;
626
+ /** Filename of the source manifest (e.g. `'setup.py'`). */
627
+ manifestFile;
628
+ /** Compiled regex used to locate the version string. */
629
+ versionRegex;
630
+ /**
631
+ * Creates a new regex version source.
632
+ *
633
+ * @param manifestFile - Source manifest filename.
634
+ * @param versionRegex - Regex string with at least one capture group for the version.
635
+ */
636
+ constructor(manifestFile, versionRegex) {
637
+ this.name = manifestFile;
638
+ this.manifestFile = manifestFile;
639
+ try {
640
+ this.versionRegex = new RegExp(versionRegex, "m");
641
+ } catch (err) {
642
+ throw new Error(`Invalid version regex for ${manifestFile}: ${err.message}`);
643
+ }
644
+ if (!/\((?!\?)/.test(versionRegex)) {
645
+ throw new Error(`Version regex for ${manifestFile} must contain at least one capture group`);
646
+ }
647
+ }
648
+ /**
649
+ * Returns `true` when the manifest file exists in `cwd`.
650
+ *
651
+ * @param cwd - Project directory to check.
652
+ * @returns Whether the manifest file exists.
653
+ */
654
+ exists(cwd) {
655
+ return fs.existsSync(path.join(cwd, this.manifestFile));
656
+ }
657
+ /**
658
+ * Reads the version string from the source manifest using regex extraction.
659
+ *
660
+ * @param cwd - Project directory containing the manifest.
661
+ * @returns The version string captured by group 1 of the regex.
662
+ */
663
+ getVersion(cwd) {
664
+ const filePath = path.join(cwd, this.manifestFile);
665
+ if (!fs.existsSync(filePath)) {
666
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
667
+ }
668
+ const content = fs.readFileSync(filePath, "utf-8");
669
+ const match = content.match(this.versionRegex);
670
+ if (!match?.[1]) {
671
+ throw new Error(`No version match found in ${this.manifestFile}`);
672
+ }
673
+ return match[1];
674
+ }
675
+ /**
676
+ * Writes a version string to the source manifest using position-based replacement.
677
+ *
678
+ * @param version - Version string to write.
679
+ * @param cwd - Project directory containing the manifest.
680
+ */
681
+ setVersion(version, cwd) {
682
+ const filePath = path.join(cwd, this.manifestFile);
683
+ if (!fs.existsSync(filePath)) {
684
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
685
+ }
686
+ const content = fs.readFileSync(filePath, "utf-8");
687
+ const match = this.versionRegex.exec(content);
688
+ if (!match || match.index === void 0) {
689
+ throw new Error(`No version match found in ${this.manifestFile}`);
690
+ }
691
+ const captureStart = match.index + match[0].indexOf(match[1]);
692
+ const captureEnd = captureStart + match[1].length;
693
+ const updated = content.slice(0, captureStart) + version + content.slice(captureEnd);
694
+ fs.writeFileSync(filePath, updated, "utf-8");
695
+ }
696
+ }
697
+ class TomlVersionSource {
698
+ /** Human-readable provider name. */
699
+ name;
700
+ /** Filename of the TOML manifest (e.g. `'Cargo.toml'`). */
701
+ manifestFile;
702
+ /** Dotted key path to the version field within the TOML document. */
703
+ versionPath;
704
+ /**
705
+ * Creates a new TOML version source.
706
+ *
707
+ * @param manifestFile - TOML manifest filename.
708
+ * @param versionPath - Dotted key path to the version field.
709
+ */
710
+ constructor(manifestFile = "Cargo.toml", versionPath = "package.version") {
711
+ this.name = manifestFile;
712
+ this.manifestFile = manifestFile;
713
+ this.versionPath = versionPath;
714
+ }
715
+ /**
716
+ * Returns `true` when the manifest file exists in `cwd`.
717
+ *
718
+ * @param cwd - Project directory to check.
719
+ * @returns Whether the manifest file exists.
720
+ */
721
+ exists(cwd) {
722
+ return fs.existsSync(path.join(cwd, this.manifestFile));
723
+ }
724
+ /**
725
+ * Reads the version string from the TOML manifest.
726
+ *
727
+ * @param cwd - Project directory containing the manifest.
728
+ * @returns The version string extracted from the manifest.
729
+ */
730
+ getVersion(cwd) {
731
+ const filePath = path.join(cwd, this.manifestFile);
732
+ if (!fs.existsSync(filePath)) {
733
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
734
+ }
735
+ const content = fs.readFileSync(filePath, "utf-8");
736
+ const parsed = parse$2(content);
737
+ const version = getNestedValue(parsed, this.versionPath);
738
+ if (typeof version !== "string" || version.length === 0) {
739
+ throw new Error(`No version field at '${this.versionPath}' in ${this.manifestFile}`);
740
+ }
741
+ return version;
742
+ }
743
+ /**
744
+ * Writes a version string to the TOML manifest, preserving formatting.
745
+ *
746
+ * @param version - Version string to write.
747
+ * @param cwd - Project directory containing the manifest.
748
+ */
749
+ setVersion(version, cwd) {
750
+ const filePath = path.join(cwd, this.manifestFile);
751
+ if (!fs.existsSync(filePath)) {
752
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
753
+ }
754
+ const content = fs.readFileSync(filePath, "utf-8");
755
+ const sectionKey = this.getSectionKey();
756
+ const updated = replaceTomlVersion(content, sectionKey, version);
757
+ if (updated === content) {
758
+ throw new Error(`Could not find version field to update in ${this.manifestFile}`);
759
+ }
760
+ fs.writeFileSync(filePath, updated, "utf-8");
761
+ }
762
+ /**
763
+ * Splits the dotted version path into a TOML section name and key name.
764
+ *
765
+ * @returns An object with `section` and `key` components.
766
+ */
767
+ getSectionKey() {
768
+ const parts = this.versionPath.split(".");
769
+ if (parts.length === 1) {
770
+ return { section: "", key: parts[0] };
771
+ }
772
+ return {
773
+ section: parts.slice(0, -1).join("."),
774
+ key: parts[parts.length - 1]
775
+ };
776
+ }
777
+ }
778
+ function replaceTomlVersion(content, target, newVersion) {
779
+ const result = replaceInSection(content, target, newVersion);
780
+ if (result !== content) return result;
781
+ if (target.section) {
782
+ const dottedRegex = new RegExp(
783
+ `^(\\s*${escapeRegExp(target.section)}\\.${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`,
784
+ "m"
785
+ );
786
+ const dottedResult = content.replace(dottedRegex, `$1$2${newVersion}$4`);
787
+ if (dottedResult !== content) return dottedResult;
788
+ }
789
+ if (target.section) {
790
+ const inlineRegex = new RegExp(
791
+ `^(\\s*${escapeRegExp(target.section)}\\s*=\\s*\\{[^}]*${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`,
792
+ "m"
793
+ );
794
+ const inlineResult = content.replace(inlineRegex, `$1$2${newVersion}$4`);
795
+ if (inlineResult !== content) return inlineResult;
796
+ }
797
+ return content;
798
+ }
799
+ function replaceInSection(content, target, newVersion) {
800
+ const lines = content.split("\n");
801
+ const sectionHeader = target.section ? `[${target.section}]` : null;
802
+ let inSection = sectionHeader === null;
803
+ const versionRegex = new RegExp(`^(\\s*${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`);
804
+ for (let i = 0; i < lines.length; i++) {
805
+ const trimmed = lines[i].trim();
806
+ if (sectionHeader !== null) {
807
+ if (trimmed === sectionHeader) {
808
+ inSection = true;
809
+ continue;
810
+ }
811
+ if (inSection && trimmed.startsWith("[") && trimmed !== sectionHeader) {
812
+ inSection = false;
813
+ continue;
814
+ }
815
+ }
816
+ if (inSection) {
817
+ const match = lines[i].match(versionRegex);
818
+ if (match) {
819
+ lines[i] = lines[i].replace(versionRegex, `$1$2${newVersion}$4`);
820
+ return lines.join("\n");
821
+ }
822
+ }
823
+ }
824
+ return content;
825
+ }
826
+ class VersionFileSource {
827
+ /** Human-readable provider name. */
828
+ name;
829
+ /** Filename of the version file (e.g. `'VERSION'`). */
830
+ manifestFile;
831
+ /**
832
+ * Creates a new plain text version file source.
833
+ *
834
+ * @param manifestFile - Version filename.
835
+ */
836
+ constructor(manifestFile = "VERSION") {
837
+ this.name = manifestFile;
838
+ this.manifestFile = manifestFile;
839
+ }
840
+ /**
841
+ * Returns `true` when the version file exists in `cwd`.
842
+ *
843
+ * @param cwd - Project directory to check.
844
+ * @returns Whether the version file exists.
845
+ */
846
+ exists(cwd) {
847
+ return fs.existsSync(path.join(cwd, this.manifestFile));
848
+ }
849
+ /**
850
+ * Reads the version string from the plain text version file.
851
+ *
852
+ * @param cwd - Project directory containing the version file.
853
+ * @returns The version string from the first line of the file.
854
+ */
855
+ getVersion(cwd) {
856
+ const filePath = path.join(cwd, this.manifestFile);
857
+ if (!fs.existsSync(filePath)) {
858
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
859
+ }
860
+ const raw = fs.readFileSync(filePath, "utf-8");
861
+ if (raw.includes("\0")) {
862
+ throw new Error(`${this.manifestFile} appears to be a binary file`);
863
+ }
864
+ const version = raw.split("\n")[0].trim();
865
+ if (version.length === 0) {
866
+ throw new Error(`${this.manifestFile} is empty`);
867
+ }
868
+ return version;
869
+ }
870
+ /**
871
+ * Writes a version string to the plain text version file.
872
+ *
873
+ * @param version - Version string to write.
874
+ * @param cwd - Project directory containing the version file.
875
+ */
876
+ setVersion(version, cwd) {
877
+ const filePath = path.join(cwd, this.manifestFile);
878
+ if (!fs.existsSync(filePath)) {
879
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
880
+ }
881
+ fs.writeFileSync(filePath, `${version}
882
+ `, "utf-8");
883
+ }
884
+ }
885
+ class YamlVersionSource {
886
+ /** Human-readable provider name. */
887
+ name;
888
+ /** Filename of the YAML manifest (e.g. `'pubspec.yaml'`). */
889
+ manifestFile;
890
+ /** Dotted key path to the version field within the YAML document. */
891
+ versionKey;
892
+ /**
893
+ * Creates a new YAML version source.
894
+ *
895
+ * @param manifestFile - YAML manifest filename.
896
+ * @param versionKey - Dotted key path to the version field.
897
+ */
898
+ constructor(manifestFile = "pubspec.yaml", versionKey = "version") {
899
+ this.name = manifestFile;
900
+ this.manifestFile = manifestFile;
901
+ this.versionKey = versionKey;
902
+ }
903
+ /**
904
+ * Returns `true` when the manifest file exists in `cwd`.
905
+ *
906
+ * @param cwd - Project directory to check.
907
+ * @returns Whether the manifest file exists.
908
+ */
909
+ exists(cwd) {
910
+ return fs.existsSync(path.join(cwd, this.manifestFile));
911
+ }
912
+ /**
913
+ * Reads the version string from the YAML manifest.
914
+ *
915
+ * @param cwd - Project directory containing the manifest.
916
+ * @returns The version string extracted from the manifest.
917
+ */
918
+ getVersion(cwd) {
919
+ const filePath = path.join(cwd, this.manifestFile);
920
+ if (!fs.existsSync(filePath)) {
921
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
922
+ }
923
+ const content = fs.readFileSync(filePath, "utf-8");
924
+ const parsed = yaml.load(content);
925
+ if (!parsed || typeof parsed !== "object") {
926
+ throw new Error(`Failed to parse ${this.manifestFile}`);
927
+ }
928
+ const version = getNestedValue(parsed, this.versionKey);
929
+ if (typeof version !== "string" || version.length === 0) {
930
+ if (typeof version === "number") {
931
+ return String(version);
932
+ }
933
+ throw new Error(`No version field in ${this.manifestFile}`);
934
+ }
935
+ return version;
936
+ }
937
+ /**
938
+ * Writes a version string to the YAML manifest, preserving formatting.
939
+ *
940
+ * @param version - Version string to write.
941
+ * @param cwd - Project directory containing the manifest.
942
+ */
943
+ setVersion(version, cwd) {
944
+ const filePath = path.join(cwd, this.manifestFile);
945
+ if (!fs.existsSync(filePath)) {
946
+ throw new Error(`${this.manifestFile} not found in ${cwd}`);
947
+ }
948
+ const keyParts = this.versionKey.split(".");
949
+ const leafKey = keyParts[keyParts.length - 1];
950
+ const content = fs.readFileSync(filePath, "utf-8");
951
+ const regex = new RegExp(`^(\\s*${escapeRegExp(leafKey)}:\\s*)(["']?)(.+?)\\2\\s*$`, "m");
952
+ const updated = content.replace(regex, `$1$2${version}$2`);
953
+ if (updated === content) {
954
+ throw new Error(`Could not find version field to update in ${this.manifestFile}`);
955
+ }
956
+ fs.writeFileSync(filePath, updated, "utf-8");
957
+ }
958
+ }
959
+ const VALID_SOURCES = /* @__PURE__ */ new Set([
960
+ "auto",
961
+ "package.json",
962
+ "composer.json",
963
+ "Cargo.toml",
964
+ "pyproject.toml",
965
+ "pubspec.yaml",
966
+ "pom.xml",
967
+ "VERSION",
968
+ "git-tag",
969
+ "custom"
970
+ ]);
971
+ const DETECTION_TABLE = [
972
+ {
973
+ file: "package.json",
974
+ source: "package.json",
975
+ factory: () => new JsonVersionSource("package.json", "version")
976
+ },
977
+ {
978
+ file: "Cargo.toml",
979
+ source: "Cargo.toml",
980
+ factory: () => new TomlVersionSource("Cargo.toml", "package.version")
981
+ },
982
+ {
983
+ file: "pyproject.toml",
984
+ source: "pyproject.toml",
985
+ factory: () => new TomlVersionSource("pyproject.toml", "project.version")
986
+ },
987
+ {
988
+ file: "pubspec.yaml",
989
+ source: "pubspec.yaml",
990
+ factory: () => new YamlVersionSource("pubspec.yaml", "version")
991
+ },
992
+ {
993
+ file: "composer.json",
994
+ source: "composer.json",
995
+ factory: () => new JsonVersionSource("composer.json", "version")
996
+ },
997
+ {
998
+ // H-002: Use regex that skips <parent> blocks for pom.xml
999
+ file: "pom.xml",
1000
+ source: "pom.xml",
1001
+ factory: () => new RegexVersionSource("pom.xml", "<project[^>]*>[\\s\\S]*?<version>([^<]+)</version>")
1002
+ },
1003
+ { file: "VERSION", source: "VERSION", factory: () => new VersionFileSource("VERSION") }
1004
+ ];
1005
+ function assertPathContained(manifestFile, cwd) {
1006
+ const resolved = path.resolve(cwd, manifestFile);
1007
+ const root = path.resolve(cwd);
1008
+ if (!resolved.startsWith(`${root}${path.sep}`) && resolved !== root) {
1009
+ throw new Error(`Manifest path "${manifestFile}" resolves outside the project directory`);
1010
+ }
1011
+ }
1012
+ function createProvider(source, config, cwd) {
1013
+ if (!VALID_SOURCES.has(source)) {
1014
+ throw new Error(
1015
+ `Invalid manifest source "${source}". Valid sources: ${[...VALID_SOURCES].join(", ")}`
1016
+ );
1017
+ }
1018
+ switch (source) {
1019
+ case "package.json":
1020
+ return new JsonVersionSource("package.json", config.path ?? "version");
1021
+ case "composer.json":
1022
+ return new JsonVersionSource("composer.json", config.path ?? "version");
1023
+ case "Cargo.toml":
1024
+ return new TomlVersionSource("Cargo.toml", config.path ?? "package.version");
1025
+ case "pyproject.toml":
1026
+ return new TomlVersionSource("pyproject.toml", config.path ?? "project.version");
1027
+ case "pubspec.yaml":
1028
+ return new YamlVersionSource("pubspec.yaml", config.path ?? "version");
1029
+ case "pom.xml":
1030
+ return new RegexVersionSource(
1031
+ "pom.xml",
1032
+ config.regex ?? "<project[^>]*>[\\s\\S]*?<version>([^<]+)</version>"
1033
+ );
1034
+ case "VERSION":
1035
+ return new VersionFileSource(config.path ?? "VERSION");
1036
+ case "git-tag":
1037
+ return new GitTagSource();
1038
+ case "custom": {
1039
+ if (!config.regex) {
1040
+ throw new Error("Custom manifest source requires a 'regex' field in manifest config");
1041
+ }
1042
+ if (!config.path) {
1043
+ throw new Error(
1044
+ "Custom manifest source requires a 'path' field (manifest filename) in manifest config"
1045
+ );
1046
+ }
1047
+ assertPathContained(config.path, cwd);
1048
+ return new RegexVersionSource(config.path, config.regex);
1049
+ }
1050
+ default:
1051
+ throw new Error(`Unknown manifest source: ${source}`);
1052
+ }
1053
+ }
1054
+ function resolveVersionSource(config, cwd = process.cwd()) {
1055
+ if (config.source !== "auto") {
1056
+ return createProvider(config.source, config, cwd);
1057
+ }
1058
+ for (const entry of DETECTION_TABLE) {
1059
+ const provider = entry.factory();
1060
+ if (provider.exists(cwd)) {
1061
+ return provider;
1062
+ }
1063
+ }
1064
+ const supported = DETECTION_TABLE.map((e) => e.file).join(", ");
1065
+ throw new Error(
1066
+ `No supported manifest file found in ${cwd}. Looked for: ${supported}. Set manifest.source explicitly in .versionguard.yml or create a supported manifest file.`
1067
+ );
1068
+ }
1069
+ function detectManifests(cwd = process.cwd()) {
1070
+ const detected = [];
1071
+ for (const entry of DETECTION_TABLE) {
1072
+ const provider = entry.factory();
1073
+ if (provider.exists(cwd)) {
1074
+ detected.push(entry.source);
1075
+ }
1076
+ }
1077
+ return detected;
1078
+ }
356
1079
  function getPackageJsonPath(cwd = process.cwd()) {
357
1080
  return path.join(cwd, "package.json");
358
1081
  }
@@ -367,18 +1090,30 @@ function writePackageJson(pkg, cwd = process.cwd()) {
367
1090
  fs.writeFileSync(getPackageJsonPath(cwd), `${JSON.stringify(pkg, null, 2)}
368
1091
  `, "utf-8");
369
1092
  }
370
- function getPackageVersion(cwd = process.cwd()) {
1093
+ function getPackageVersion(cwd = process.cwd(), manifest) {
1094
+ if (manifest) {
1095
+ const provider = resolveVersionSource(manifest, cwd);
1096
+ return provider.getVersion(cwd);
1097
+ }
371
1098
  const pkg = readPackageJson(cwd);
372
1099
  if (typeof pkg.version !== "string" || pkg.version.length === 0) {
373
1100
  throw new Error("No version field in package.json");
374
1101
  }
375
1102
  return pkg.version;
376
1103
  }
377
- function setPackageVersion(version, cwd = process.cwd()) {
1104
+ function setPackageVersion(version, cwd = process.cwd(), manifest) {
1105
+ if (manifest) {
1106
+ const provider = resolveVersionSource(manifest, cwd);
1107
+ provider.setVersion(version, cwd);
1108
+ return;
1109
+ }
378
1110
  const pkg = readPackageJson(cwd);
379
1111
  pkg.version = version;
380
1112
  writePackageJson(pkg, cwd);
381
1113
  }
1114
+ function getVersionSource(manifest, cwd = process.cwd()) {
1115
+ return resolveVersionSource(manifest, cwd);
1116
+ }
382
1117
  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
1118
  function parse(version) {
384
1119
  const match = version.match(SEMVER_REGEX);
@@ -623,6 +1358,12 @@ function checkHardcodedVersions(expectedVersion, config, ignorePatterns, cwd = p
623
1358
  }
624
1359
  return mismatches;
625
1360
  }
1361
+ function getCalVerConfig(config) {
1362
+ if (!config.versioning.calver) {
1363
+ throw new Error('CalVer configuration is required when versioning.type is "calver"');
1364
+ }
1365
+ return config.versioning.calver;
1366
+ }
626
1367
  const CONFIG_FILE_NAMES = [
627
1368
  ".versionguard.yml",
628
1369
  ".versionguard.yaml",
@@ -632,11 +1373,19 @@ const CONFIG_FILE_NAMES = [
632
1373
  const DEFAULT_CONFIG = {
633
1374
  versioning: {
634
1375
  type: "semver",
1376
+ schemeRules: {
1377
+ maxNumericSegments: 3,
1378
+ allowedModifiers: ["dev", "alpha", "beta", "rc"]
1379
+ },
635
1380
  calver: {
636
1381
  format: "YYYY.MM.PATCH",
637
- preventFutureDates: true
1382
+ preventFutureDates: true,
1383
+ strictMutualExclusion: true
638
1384
  }
639
1385
  },
1386
+ manifest: {
1387
+ source: "auto"
1388
+ },
640
1389
  sync: {
641
1390
  files: ["README.md", "CHANGELOG.md"],
642
1391
  patterns: [
@@ -757,13 +1506,7 @@ function getVersionFeedback(version, config, previousVersion) {
757
1506
  if (config.versioning.type === "semver") {
758
1507
  return getSemVerFeedback(version, previousVersion);
759
1508
  }
760
- return getCalVerFeedback(version, getCalVerConfig$2(config), previousVersion);
761
- }
762
- function getCalVerConfig$2(config) {
763
- if (!config.versioning.calver) {
764
- throw new Error('CalVer configuration is required when versioning.type is "calver"');
765
- }
766
- return config.versioning.calver;
1509
+ return getCalVerFeedback(version, getCalVerConfig(config), previousVersion);
767
1510
  }
768
1511
  function getSemVerFeedback(version, previousVersion) {
769
1512
  const errors = [];
@@ -779,7 +1522,7 @@ function getSemVerFeedback(version, previousVersion) {
779
1522
  });
780
1523
  suggestions.push({
781
1524
  message: `Remove the 'v' prefix`,
782
- fix: `npm version ${cleanVersion}`,
1525
+ fix: `npx versionguard fix --version ${cleanVersion}`,
783
1526
  autoFixable: true
784
1527
  });
785
1528
  } else if (version.split(".").length === 2) {
@@ -789,7 +1532,7 @@ function getSemVerFeedback(version, previousVersion) {
789
1532
  });
790
1533
  suggestions.push({
791
1534
  message: `Add patch number (e.g., ${version}.0)`,
792
- fix: `npm version ${version}.0`,
1535
+ fix: `npx versionguard fix --version ${version}.0`,
793
1536
  autoFixable: true
794
1537
  });
795
1538
  } else if (/^\d+\.\d+\.\d+\.\d+$/.test(version)) {
@@ -839,7 +1582,7 @@ function getSemVerFeedback(version, previousVersion) {
839
1582
  });
840
1583
  suggestions.push({
841
1584
  message: `Version must be greater than ${previousVersion}`,
842
- fix: `npm version ${increment(previousVersion, "patch")}`,
1585
+ fix: `npx versionguard fix --version ${increment(previousVersion, "patch")}`,
843
1586
  autoFixable: true
844
1587
  });
845
1588
  } else if (comparison === 0) {
@@ -849,7 +1592,7 @@ function getSemVerFeedback(version, previousVersion) {
849
1592
  });
850
1593
  suggestions.push({
851
1594
  message: `Bump the version`,
852
- fix: `npm version ${increment(previousVersion, "patch")}`,
1595
+ fix: `npx versionguard fix --version ${increment(previousVersion, "patch")}`,
853
1596
  autoFixable: true
854
1597
  });
855
1598
  } else {
@@ -896,7 +1639,7 @@ function getCalVerFeedback(version, calverConfig, previousVersion) {
896
1639
  });
897
1640
  suggestions.push({
898
1641
  message: `Expected format: ${format2}`,
899
- fix: `Update package.json to use current date: "${getCurrentVersion(format2)}"`,
1642
+ fix: `Update version to current date: "${getCurrentVersion(format2)}"`,
900
1643
  autoFixable: true
901
1644
  });
902
1645
  return { valid: false, errors, suggestions, canAutoFix: true };
@@ -919,21 +1662,21 @@ function getCalVerFeedback(version, calverConfig, previousVersion) {
919
1662
  if (preventFutureDates && parsed.year > now.getFullYear()) {
920
1663
  suggestions.push({
921
1664
  message: `Use current year (${now.getFullYear()}) or a past year`,
922
- fix: `npm version ${formatCalVerVersion({ ...parsed, year: now.getFullYear() })}`,
1665
+ fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, year: now.getFullYear() })}`,
923
1666
  autoFixable: true
924
1667
  });
925
1668
  }
926
1669
  if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month > now.getMonth() + 1) {
927
1670
  suggestions.push({
928
1671
  message: `Current month is ${now.getMonth() + 1}`,
929
- fix: `npm version ${formatCalVerVersion({ ...parsed, month: now.getMonth() + 1 })}`,
1672
+ fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, month: now.getMonth() + 1 })}`,
930
1673
  autoFixable: true
931
1674
  });
932
1675
  }
933
1676
  if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month === now.getMonth() + 1 && parsed.day !== void 0 && parsed.day > now.getDate()) {
934
1677
  suggestions.push({
935
1678
  message: `Current day is ${now.getDate()}`,
936
- fix: `npm version ${formatCalVerVersion({ ...parsed, day: now.getDate() })}`,
1679
+ fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, day: now.getDate() })}`,
937
1680
  autoFixable: true
938
1681
  });
939
1682
  }
@@ -947,7 +1690,7 @@ function getCalVerFeedback(version, calverConfig, previousVersion) {
947
1690
  });
948
1691
  suggestions.push({
949
1692
  message: `CalVer must increase over time`,
950
- fix: `npm version ${increment$1(previousVersion, format2)}`,
1693
+ fix: `npx versionguard fix --version ${increment$1(previousVersion, format2)}`,
951
1694
  autoFixable: true
952
1695
  });
953
1696
  }
@@ -1003,7 +1746,7 @@ function getChangelogFeedback(hasEntry, version, latestChangelogVersion) {
1003
1746
  }
1004
1747
  if (latestChangelogVersion && latestChangelogVersion !== version) {
1005
1748
  suggestions.push({
1006
- message: `CHANGELOG.md latest entry is ${latestChangelogVersion}, but package.json is ${version}`,
1749
+ message: `CHANGELOG.md latest entry is ${latestChangelogVersion}, but manifest version is ${version}`,
1007
1750
  fix: `Make sure versions are in sync`,
1008
1751
  autoFixable: false
1009
1752
  });
@@ -1014,7 +1757,7 @@ function getTagFeedback(tagVersion, packageVersion, hasUnsyncedFiles) {
1014
1757
  const suggestions = [];
1015
1758
  if (tagVersion !== packageVersion) {
1016
1759
  suggestions.push({
1017
- message: `Git tag "${tagVersion}" doesn't match package.json "${packageVersion}"`,
1760
+ message: `Git tag "${tagVersion}" doesn't match manifest version "${packageVersion}"`,
1018
1761
  fix: `Delete tag and recreate: git tag -d ${tagVersion} && git tag ${packageVersion}`,
1019
1762
  autoFixable: false
1020
1763
  });
@@ -1028,25 +1771,43 @@ function getTagFeedback(tagVersion, packageVersion, hasUnsyncedFiles) {
1028
1771
  }
1029
1772
  return suggestions;
1030
1773
  }
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" };
1774
+ function fixPackageVersion(targetVersion, cwd = process.cwd(), manifest) {
1775
+ if (!manifest) {
1776
+ const packagePath = path.join(cwd, "package.json");
1777
+ if (!fs.existsSync(packagePath)) {
1778
+ return { fixed: false, message: "package.json not found" };
1779
+ }
1780
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1781
+ const oldVersion2 = typeof pkg.version === "string" ? pkg.version : void 0;
1782
+ if (oldVersion2 === targetVersion) {
1783
+ return { fixed: false, message: `Already at version ${targetVersion}` };
1784
+ }
1785
+ setPackageVersion(targetVersion, cwd);
1786
+ return {
1787
+ fixed: true,
1788
+ message: `Updated package.json from ${oldVersion2} to ${targetVersion}`,
1789
+ file: packagePath
1790
+ };
1791
+ }
1792
+ const provider = getVersionSource(manifest, cwd);
1793
+ let oldVersion;
1794
+ try {
1795
+ oldVersion = provider.getVersion(cwd);
1796
+ } catch {
1797
+ return { fixed: false, message: "Version source not found" };
1035
1798
  }
1036
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1037
- const oldVersion = typeof pkg.version === "string" ? pkg.version : void 0;
1038
1799
  if (oldVersion === targetVersion) {
1039
1800
  return { fixed: false, message: `Already at version ${targetVersion}` };
1040
1801
  }
1041
- setPackageVersion(targetVersion, cwd);
1802
+ provider.setVersion(targetVersion, cwd);
1042
1803
  return {
1043
1804
  fixed: true,
1044
- message: `Updated package.json from ${oldVersion} to ${targetVersion}`,
1045
- file: packagePath
1805
+ message: `Updated version from ${oldVersion} to ${targetVersion}`,
1806
+ file: provider.manifestFile ? path.join(cwd, provider.manifestFile) : void 0
1046
1807
  };
1047
1808
  }
1048
1809
  function fixSyncIssues(config, cwd = process.cwd()) {
1049
- const version = getPackageVersion(cwd);
1810
+ const version = getPackageVersion(cwd, config.manifest);
1050
1811
  const results = syncVersion(version, config.sync, cwd).filter((result) => result.updated).map((result) => ({
1051
1812
  fixed: true,
1052
1813
  message: `Updated ${path.relative(cwd, result.file)} (${result.changes.length} changes)`,
@@ -1102,9 +1863,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1102
1863
  }
1103
1864
  function fixAll(config, targetVersion, cwd = process.cwd()) {
1104
1865
  const results = [];
1105
- const version = targetVersion || getPackageVersion(cwd);
1106
- if (targetVersion && targetVersion !== getPackageVersion(cwd)) {
1107
- results.push(fixPackageVersion(targetVersion, cwd));
1866
+ const version = targetVersion || getPackageVersion(cwd, config.manifest);
1867
+ if (targetVersion && targetVersion !== getPackageVersion(cwd, config.manifest)) {
1868
+ results.push(fixPackageVersion(targetVersion, cwd, config.manifest));
1108
1869
  }
1109
1870
  const syncResults = fixSyncIssues(config, cwd);
1110
1871
  results.push(...syncResults);
@@ -1138,7 +1899,7 @@ function suggestNextVersion(currentVersion, config, changeType) {
1138
1899
  });
1139
1900
  }
1140
1901
  } else {
1141
- const format2 = getCalVerConfig$1(config).format;
1902
+ const format2 = getCalVerConfig(config).format;
1142
1903
  const currentCal = getCurrentVersion(format2);
1143
1904
  suggestions.push({
1144
1905
  version: currentCal,
@@ -1151,12 +1912,6 @@ function suggestNextVersion(currentVersion, config, changeType) {
1151
1912
  }
1152
1913
  return suggestions;
1153
1914
  }
1154
- function getCalVerConfig$1(config) {
1155
- if (!config.versioning.calver) {
1156
- throw new Error('CalVer configuration is required when versioning.type is "calver"');
1157
- }
1158
- return config.versioning.calver;
1159
- }
1160
1915
  const HOOK_NAMES = ["pre-commit", "pre-push", "post-tag"];
1161
1916
  function checkHooksPathOverride(cwd) {
1162
1917
  try {
@@ -1165,6 +1920,15 @@ function checkHooksPathOverride(cwd) {
1165
1920
  encoding: "utf-8"
1166
1921
  }).trim();
1167
1922
  if (hooksPath) {
1923
+ const resolved = path.resolve(cwd, hooksPath);
1924
+ const huskyDir = path.resolve(cwd, ".husky");
1925
+ if (resolved === huskyDir || resolved.startsWith(`${huskyDir}${path.sep}`)) {
1926
+ return {
1927
+ code: "HOOKS_PATH_HUSKY",
1928
+ severity: "warning",
1929
+ 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.`
1930
+ };
1931
+ }
1168
1932
  return {
1169
1933
  code: "HOOKS_PATH_OVERRIDE",
1170
1934
  severity: "error",
@@ -1315,7 +2079,7 @@ function createTag(version, message, autoFix = true, config, cwd = process.cwd()
1315
2079
  actions
1316
2080
  };
1317
2081
  }
1318
- const packageVersion = getPackageVersion(cwd);
2082
+ const packageVersion = getPackageVersion(cwd, config.manifest);
1319
2083
  const shouldAutoFix = autoFix;
1320
2084
  const preflightError = getTagPreflightError(config, cwd, version, shouldAutoFix);
1321
2085
  if (preflightError) {
@@ -1328,7 +2092,7 @@ function createTag(version, message, autoFix = true, config, cwd = process.cwd()
1328
2092
  if (version !== packageVersion && !autoFix) {
1329
2093
  return {
1330
2094
  success: false,
1331
- message: `Version mismatch: package.json is ${packageVersion}, tag is ${version}`,
2095
+ message: `Version mismatch: manifest version is ${packageVersion}, tag is ${version}`,
1332
2096
  actions: []
1333
2097
  };
1334
2098
  }
@@ -1388,15 +2152,15 @@ function handlePostTag(config, cwd = process.cwd()) {
1388
2152
  actions
1389
2153
  };
1390
2154
  }
1391
- const packageVersion = getPackageVersion(cwd);
2155
+ const packageVersion = getPackageVersion(cwd, config.manifest);
1392
2156
  if (tag.version !== packageVersion) {
1393
2157
  return {
1394
2158
  success: false,
1395
- message: `Tag version ${tag.version} doesn't match package.json ${packageVersion}`,
2159
+ message: `Tag version ${tag.version} doesn't match manifest version ${packageVersion}`,
1396
2160
  actions: [
1397
2161
  "To fix: delete tag and recreate with correct version",
1398
2162
  ` git tag -d ${tag.name}`,
1399
- ` npm version ${tag.version}`,
2163
+ ` Update manifest to ${tag.version}`,
1400
2164
  ` git tag ${tag.name}`
1401
2165
  ]
1402
2166
  };
@@ -1427,11 +2191,12 @@ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false
1427
2191
  if (hasDirtyWorktree(cwd)) {
1428
2192
  return "Working tree must be clean before creating or validating release tags";
1429
2193
  }
1430
- const version = expectedVersion ?? getPackageVersion(cwd);
2194
+ const version = expectedVersion ?? getPackageVersion(cwd, config.manifest);
1431
2195
  const versionResult = config.versioning.type === "semver" ? validate$1(version) : validate$2(
1432
2196
  version,
1433
2197
  config.versioning.calver?.format ?? "YYYY.MM.PATCH",
1434
- config.versioning.calver?.preventFutureDates ?? true
2198
+ config.versioning.calver?.preventFutureDates ?? true,
2199
+ config.versioning.schemeRules
1435
2200
  );
1436
2201
  if (!versionResult.valid) {
1437
2202
  return versionResult.errors[0]?.message ?? `Invalid version: ${version}`;
@@ -1515,13 +2280,18 @@ function validateVersion(version, config) {
1515
2280
  return validate$1(version);
1516
2281
  }
1517
2282
  const calverConfig = getCalVerConfig(config);
1518
- return validate$2(version, calverConfig.format, calverConfig.preventFutureDates);
2283
+ return validate$2(
2284
+ version,
2285
+ calverConfig.format,
2286
+ calverConfig.preventFutureDates,
2287
+ config.versioning.schemeRules
2288
+ );
1519
2289
  }
1520
2290
  function validate(config, cwd = process.cwd()) {
1521
2291
  const errors = [];
1522
2292
  let version;
1523
2293
  try {
1524
- version = getPackageVersion(cwd);
2294
+ version = getPackageVersion(cwd, config.manifest);
1525
2295
  } catch (err) {
1526
2296
  return {
1527
2297
  valid: false,
@@ -1592,7 +2362,7 @@ function doctor(config, cwd = process.cwd()) {
1592
2362
  };
1593
2363
  }
1594
2364
  function sync(config, cwd = process.cwd()) {
1595
- const version = getPackageVersion(cwd);
2365
+ const version = getPackageVersion(cwd, config.manifest);
1596
2366
  syncVersion(version, config.sync, cwd);
1597
2367
  }
1598
2368
  function canBump(currentVersion, newVersion, config) {
@@ -1627,12 +2397,6 @@ function canBump(currentVersion, newVersion, config) {
1627
2397
  }
1628
2398
  return { canBump: true };
1629
2399
  }
1630
- function getCalVerConfig(config) {
1631
- if (!config.versioning.calver) {
1632
- throw new Error('CalVer configuration is required when versioning.type is "calver"');
1633
- }
1634
- return config.versioning.calver;
1635
- }
1636
2400
  function isWorktreeClean(cwd) {
1637
2401
  try {
1638
2402
  return execSync("git status --porcelain", { cwd, encoding: "utf-8" }).trim().length === 0;
@@ -1641,30 +2405,41 @@ function isWorktreeClean(cwd) {
1641
2405
  }
1642
2406
  }
1643
2407
  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,
2408
+ fixChangelog as A,
2409
+ fixPackageVersion as B,
2410
+ getAllTags as C,
2411
+ getCalVerConfig as D,
2412
+ getLatestTag as E,
2413
+ getTagFeedback as F,
2414
+ GitTagSource as G,
2415
+ getVersionSource as H,
2416
+ initConfig as I,
2417
+ JsonVersionSource as J,
2418
+ resolveVersionSource as K,
2419
+ semver as L,
2420
+ suggestTagMessage as M,
2421
+ sync as N,
2422
+ syncVersion as O,
2423
+ validateChangelog as P,
2424
+ validateTagForPush as Q,
2425
+ RegexVersionSource as R,
2426
+ validateVersion as S,
2427
+ TomlVersionSource as T,
2428
+ VersionFileSource as V,
2429
+ YamlVersionSource as Y,
2430
+ installHooks as a,
2431
+ getPackageVersion as b,
2432
+ getVersionFeedback as c,
2433
+ getSyncFeedback as d,
2434
+ getChangelogFeedback as e,
2435
+ doctor as f,
1661
2436
  getConfig as g,
1662
2437
  handlePostTag as h,
1663
- initConfig as i,
1664
- fixSyncIssues as j,
1665
- setPackageVersion as k,
1666
- createTag as l,
1667
- installHooks as m,
2438
+ isValidCalVerFormat as i,
2439
+ fixAll as j,
2440
+ fixSyncIssues as k,
2441
+ setPackageVersion as l,
2442
+ createTag as m,
1668
2443
  areHooksInstalled as n,
1669
2444
  calver as o,
1670
2445
  canBump as p,
@@ -1677,6 +2452,6 @@ export {
1677
2452
  checkHookIntegrity as w,
1678
2453
  checkHooksPathOverride as x,
1679
2454
  checkHuskyBypass as y,
1680
- fixChangelog as z
2455
+ detectManifests as z
1681
2456
  };
1682
- //# sourceMappingURL=index-BwE_OaV3.js.map
2457
+ //# sourceMappingURL=index-B3R60bYJ.js.map