@codluv/versionguard 0.1.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.
@@ -0,0 +1,1568 @@
1
+ import * as childProcess from "node:child_process";
2
+ import { execSync } from "node:child_process";
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs";
5
+ import { globSync } from "glob";
6
+ import { fileURLToPath } from "node:url";
7
+ import * as yaml from "js-yaml";
8
+ function parseFormat(calverFormat) {
9
+ const parts = calverFormat.split(".");
10
+ const result = {
11
+ year: parts[0],
12
+ month: parts[1]
13
+ };
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";
21
+ }
22
+ return result;
23
+ }
24
+ function tokenPattern(token) {
25
+ switch (token) {
26
+ case "YYYY":
27
+ return "(\\d{4})";
28
+ case "YY":
29
+ return "(\\d{2})";
30
+ case "0M":
31
+ case "0D":
32
+ return "(\\d{2})";
33
+ case "MM":
34
+ case "DD":
35
+ case "M":
36
+ case "D":
37
+ return "(\\d{1,2})";
38
+ case "PATCH":
39
+ return "(\\d+)";
40
+ default:
41
+ throw new Error(`Unsupported CalVer token: ${token}`);
42
+ }
43
+ }
44
+ function getRegexForFormat(calverFormat) {
45
+ const tokens = calverFormat.split(".");
46
+ const pattern = tokens.map(tokenPattern).join("\\.");
47
+ return new RegExp(`^${pattern}$`);
48
+ }
49
+ function parse$1(version, calverFormat) {
50
+ const match = version.match(getRegexForFormat(calverFormat));
51
+ if (!match) {
52
+ return null;
53
+ }
54
+ 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;
58
+ let day;
59
+ let patch;
60
+ if (definition.day) {
61
+ day = Number.parseInt(match[cursor], 10);
62
+ cursor += 1;
63
+ }
64
+ if (definition.patch) {
65
+ patch = Number.parseInt(match[cursor], 10);
66
+ }
67
+ return {
68
+ year,
69
+ month,
70
+ day,
71
+ patch,
72
+ format: calverFormat,
73
+ raw: version
74
+ };
75
+ }
76
+ function validate$2(version, calverFormat, preventFutureDates = true) {
77
+ const errors = [];
78
+ const parsed = parse$1(version, calverFormat);
79
+ if (!parsed) {
80
+ return {
81
+ valid: false,
82
+ errors: [
83
+ {
84
+ message: `Invalid CalVer format: "${version}". Expected format: ${calverFormat}`,
85
+ severity: "error"
86
+ }
87
+ ]
88
+ };
89
+ }
90
+ if (parsed.month < 1 || parsed.month > 12) {
91
+ errors.push({
92
+ message: `Invalid month: ${parsed.month}. Must be between 1 and 12.`,
93
+ severity: "error"
94
+ });
95
+ }
96
+ if (parsed.day !== void 0) {
97
+ if (parsed.day < 1 || parsed.day > 31) {
98
+ errors.push({
99
+ message: `Invalid day: ${parsed.day}. Must be between 1 and 31.`,
100
+ severity: "error"
101
+ });
102
+ } else {
103
+ const daysInMonth = new Date(parsed.year, parsed.month, 0).getDate();
104
+ if (parsed.day > daysInMonth) {
105
+ errors.push({
106
+ message: `Invalid day: ${parsed.day}. ${parsed.year}-${String(parsed.month).padStart(2, "0")} has only ${daysInMonth} days.`,
107
+ severity: "error"
108
+ });
109
+ }
110
+ }
111
+ }
112
+ if (preventFutureDates) {
113
+ const now = /* @__PURE__ */ new Date();
114
+ const currentYear = now.getFullYear();
115
+ const currentMonth = now.getMonth() + 1;
116
+ const currentDay = now.getDate();
117
+ if (parsed.year > currentYear) {
118
+ errors.push({
119
+ message: `Future year not allowed: ${parsed.year}. Current year is ${currentYear}.`,
120
+ severity: "error"
121
+ });
122
+ } else if (parsed.year === currentYear && parsed.month > currentMonth) {
123
+ errors.push({
124
+ message: `Future month not allowed: ${parsed.year}.${parsed.month}. Current month is ${currentMonth}.`,
125
+ severity: "error"
126
+ });
127
+ } else if (parsed.year === currentYear && parsed.month === currentMonth && parsed.day !== void 0 && parsed.day > currentDay) {
128
+ errors.push({
129
+ message: `Future day not allowed: ${parsed.year}.${parsed.month}.${parsed.day}. Current day is ${currentDay}.`,
130
+ severity: "error"
131
+ });
132
+ }
133
+ }
134
+ return {
135
+ valid: errors.length === 0,
136
+ errors,
137
+ version: { type: "calver", version: parsed }
138
+ };
139
+ }
140
+ 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");
146
+ }
147
+ return String(value);
148
+ }
149
+ 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);
154
+ }
155
+ if (tokens.includes("PATCH")) {
156
+ values.push(version.patch ?? 0);
157
+ }
158
+ return tokens.map((token, index) => formatToken(token, values[index])).join(".");
159
+ }
160
+ function getCurrentVersion(calverFormat, now = /* @__PURE__ */ new Date()) {
161
+ const definition = parseFormat(calverFormat);
162
+ const currentDay = now.getDate();
163
+ const base = {
164
+ year: now.getFullYear(),
165
+ month: now.getMonth() + 1,
166
+ day: definition.day ? currentDay : void 0,
167
+ patch: definition.patch ? 0 : void 0
168
+ };
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}` : "");
172
+ }
173
+ function compare$1(a, b, calverFormat) {
174
+ const left = parse$1(a, calverFormat);
175
+ const right = parse$1(b, calverFormat);
176
+ if (!left || !right) {
177
+ throw new Error(`Invalid CalVer comparison between "${a}" and "${b}"`);
178
+ }
179
+ for (const key of ["year", "month", "day", "patch"]) {
180
+ const leftValue = left[key] ?? 0;
181
+ const rightValue = right[key] ?? 0;
182
+ if (leftValue !== rightValue) {
183
+ return leftValue > rightValue ? 1 : -1;
184
+ }
185
+ }
186
+ return 0;
187
+ }
188
+ function increment$1(version, calverFormat) {
189
+ const parsed = parse$1(version, calverFormat);
190
+ if (!parsed) {
191
+ throw new Error(`Invalid CalVer version: ${version}`);
192
+ }
193
+ const definition = parseFormat(calverFormat);
194
+ const next = {
195
+ ...parsed
196
+ };
197
+ if (definition.patch) {
198
+ const patch = parsed.patch ?? 0;
199
+ next.patch = patch + 1;
200
+ } else {
201
+ next.patch = 0;
202
+ next.format = `${calverFormat}.PATCH`;
203
+ }
204
+ return format$1(next);
205
+ }
206
+ function getNextVersions(currentVersion, calverFormat) {
207
+ return [getCurrentVersion(calverFormat), increment$1(currentVersion, calverFormat)];
208
+ }
209
+ const calver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
210
+ __proto__: null,
211
+ compare: compare$1,
212
+ format: format$1,
213
+ getCurrentVersion,
214
+ getNextVersions,
215
+ getRegexForFormat,
216
+ increment: increment$1,
217
+ parse: parse$1,
218
+ parseFormat,
219
+ validate: validate$2
220
+ }, Symbol.toStringTag, { value: "Module" }));
221
+ const CHANGELOG_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
222
+ function validateChangelog(changelogPath, version, strict = true, requireEntry = true) {
223
+ if (!fs.existsSync(changelogPath)) {
224
+ return {
225
+ valid: !requireEntry,
226
+ errors: requireEntry ? [`Changelog not found: ${changelogPath}`] : [],
227
+ hasEntryForVersion: false
228
+ };
229
+ }
230
+ const errors = [];
231
+ const content = fs.readFileSync(changelogPath, "utf-8");
232
+ if (!content.startsWith("# Changelog")) {
233
+ errors.push('Changelog must start with "# Changelog"');
234
+ }
235
+ if (!content.includes("## [Unreleased]")) {
236
+ errors.push("Changelog must have an [Unreleased] section");
237
+ }
238
+ const versionHeader = `## [${version}]`;
239
+ const hasEntryForVersion = content.includes(versionHeader);
240
+ if (requireEntry && !hasEntryForVersion) {
241
+ errors.push(`Changelog must have an entry for version ${version}`);
242
+ }
243
+ if (strict) {
244
+ if (!content.includes("[Unreleased]:")) {
245
+ errors.push("Changelog should include compare links at the bottom");
246
+ }
247
+ const versionHeaderMatch = content.match(
248
+ new RegExp(`## \\[${escapeRegExp(version)}\\] - ([^\r
249
+ ]+)`)
250
+ );
251
+ if (requireEntry && hasEntryForVersion) {
252
+ if (!versionHeaderMatch) {
253
+ errors.push(`Version ${version} entry must use "## [${version}] - YYYY-MM-DD" format`);
254
+ } else if (!CHANGELOG_DATE_REGEX.test(versionHeaderMatch[1])) {
255
+ errors.push(`Version ${version} entry date must use YYYY-MM-DD format`);
256
+ }
257
+ }
258
+ }
259
+ return {
260
+ valid: errors.length === 0,
261
+ errors,
262
+ hasEntryForVersion
263
+ };
264
+ }
265
+ function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
266
+ if (!fs.existsSync(changelogPath)) {
267
+ throw new Error(`Changelog not found: ${changelogPath}`);
268
+ }
269
+ const content = fs.readFileSync(changelogPath, "utf-8");
270
+ if (content.includes(`## [${version}]`)) {
271
+ return;
272
+ }
273
+ const block = `## [${version}] - ${date}
274
+
275
+ ### Added
276
+
277
+ - Describe changes here.
278
+
279
+ `;
280
+ const unreleasedMatch = content.match(/## \[Unreleased\]\r?\n(?:\r?\n)?/);
281
+ if (!unreleasedMatch || unreleasedMatch.index === void 0) {
282
+ throw new Error("Changelog must have an [Unreleased] section");
283
+ }
284
+ const insertIndex = unreleasedMatch.index + unreleasedMatch[0].length;
285
+ const updated = `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
286
+ fs.writeFileSync(changelogPath, updated, "utf-8");
287
+ }
288
+ function escapeRegExp(value) {
289
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
+ }
291
+ const HOOK_NAMES = ["pre-commit", "pre-push", "post-tag"];
292
+ function installHooks(config, cwd = process.cwd()) {
293
+ const gitDir = findGitDir(cwd);
294
+ if (!gitDir) {
295
+ throw new Error("Not a git repository. Run `git init` first.");
296
+ }
297
+ const hooksDir = path.join(gitDir, "hooks");
298
+ fs.mkdirSync(hooksDir, { recursive: true });
299
+ for (const hookName of HOOK_NAMES) {
300
+ if (config.hooks[hookName]) {
301
+ const hookPath = path.join(hooksDir, hookName);
302
+ fs.writeFileSync(hookPath, generateHookScript(hookName), { encoding: "utf-8", mode: 493 });
303
+ }
304
+ }
305
+ }
306
+ function uninstallHooks(cwd = process.cwd()) {
307
+ const gitDir = findGitDir(cwd);
308
+ if (!gitDir) {
309
+ return;
310
+ }
311
+ const hooksDir = path.join(gitDir, "hooks");
312
+ for (const hookName of HOOK_NAMES) {
313
+ const hookPath = path.join(hooksDir, hookName);
314
+ if (fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard")) {
315
+ fs.unlinkSync(hookPath);
316
+ }
317
+ }
318
+ }
319
+ function findGitDir(cwd) {
320
+ let current = cwd;
321
+ while (true) {
322
+ const gitPath = path.join(current, ".git");
323
+ if (fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory()) {
324
+ return gitPath;
325
+ }
326
+ const parent = path.dirname(current);
327
+ if (parent === current) {
328
+ return null;
329
+ }
330
+ current = parent;
331
+ }
332
+ }
333
+ function areHooksInstalled(cwd = process.cwd()) {
334
+ const gitDir = findGitDir(cwd);
335
+ if (!gitDir) {
336
+ return false;
337
+ }
338
+ return HOOK_NAMES.every((hookName) => {
339
+ const hookPath = path.join(gitDir, "hooks", hookName);
340
+ return fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard");
341
+ });
342
+ }
343
+ function generateHookScript(hookName) {
344
+ return `#!/bin/sh
345
+ # VersionGuard ${hookName} hook
346
+ npx versionguard validate --hook=${hookName}
347
+ status=$?
348
+ if [ $status -ne 0 ]; then
349
+ echo "VersionGuard validation failed."
350
+ exit $status
351
+ fi
352
+ `;
353
+ }
354
+ function getPackageJsonPath(cwd = process.cwd()) {
355
+ return path.join(cwd, "package.json");
356
+ }
357
+ function readPackageJson(cwd = process.cwd()) {
358
+ const packagePath = getPackageJsonPath(cwd);
359
+ if (!fs.existsSync(packagePath)) {
360
+ throw new Error(`package.json not found in ${cwd}`);
361
+ }
362
+ return JSON.parse(fs.readFileSync(packagePath, "utf-8"));
363
+ }
364
+ function writePackageJson(pkg, cwd = process.cwd()) {
365
+ fs.writeFileSync(getPackageJsonPath(cwd), `${JSON.stringify(pkg, null, 2)}
366
+ `, "utf-8");
367
+ }
368
+ function getPackageVersion(cwd = process.cwd()) {
369
+ const pkg = readPackageJson(cwd);
370
+ if (typeof pkg.version !== "string" || pkg.version.length === 0) {
371
+ throw new Error("No version field in package.json");
372
+ }
373
+ return pkg.version;
374
+ }
375
+ function setPackageVersion(version, cwd = process.cwd()) {
376
+ const pkg = readPackageJson(cwd);
377
+ pkg.version = version;
378
+ writePackageJson(pkg, cwd);
379
+ }
380
+ 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-]+)*))?$/;
381
+ function parse(version) {
382
+ const match = version.match(SEMVER_REGEX);
383
+ if (!match) {
384
+ return null;
385
+ }
386
+ return {
387
+ major: Number.parseInt(match[1], 10),
388
+ minor: Number.parseInt(match[2], 10),
389
+ patch: Number.parseInt(match[3], 10),
390
+ prerelease: match[4] ? match[4].split(".") : [],
391
+ build: match[5] ? match[5].split(".") : [],
392
+ raw: version
393
+ };
394
+ }
395
+ function getStructuralErrors(version) {
396
+ const errors = [];
397
+ if (version.startsWith("v")) {
398
+ errors.push({
399
+ message: `Version should not start with 'v': ${version}`,
400
+ severity: "error"
401
+ });
402
+ return errors;
403
+ }
404
+ const mainPart = version.split(/[+-]/, 1)[0];
405
+ const segments = mainPart.split(".");
406
+ if (segments.length === 3) {
407
+ const leadingZeroSegment = segments.find((segment) => /^0\d+$/.test(segment));
408
+ if (leadingZeroSegment) {
409
+ errors.push({
410
+ message: `Invalid SemVer: numeric segment "${leadingZeroSegment}" has a leading zero`,
411
+ severity: "error"
412
+ });
413
+ return errors;
414
+ }
415
+ }
416
+ const prerelease = version.match(/-([^+]+)/)?.[1];
417
+ if (prerelease) {
418
+ const invalidPrerelease = prerelease.split(".").find((segment) => /^0\d+$/.test(segment));
419
+ if (invalidPrerelease) {
420
+ errors.push({
421
+ message: `Invalid SemVer: prerelease identifier "${invalidPrerelease}" has a leading zero`,
422
+ severity: "error"
423
+ });
424
+ return errors;
425
+ }
426
+ }
427
+ errors.push({
428
+ message: `Invalid SemVer format: "${version}". Expected MAJOR.MINOR.PATCH[-prerelease][+build].`,
429
+ severity: "error"
430
+ });
431
+ return errors;
432
+ }
433
+ function validate$1(version) {
434
+ const parsed = parse(version);
435
+ if (!parsed) {
436
+ return {
437
+ valid: false,
438
+ errors: getStructuralErrors(version)
439
+ };
440
+ }
441
+ return {
442
+ valid: true,
443
+ errors: [],
444
+ version: { type: "semver", version: parsed }
445
+ };
446
+ }
447
+ function compare(a, b) {
448
+ const left = parse(a);
449
+ const right = parse(b);
450
+ if (!left || !right) {
451
+ throw new Error(`Invalid SemVer comparison between "${a}" and "${b}"`);
452
+ }
453
+ for (const key of ["major", "minor", "patch"]) {
454
+ if (left[key] !== right[key]) {
455
+ return left[key] > right[key] ? 1 : -1;
456
+ }
457
+ }
458
+ const leftHasPrerelease = left.prerelease.length > 0;
459
+ const rightHasPrerelease = right.prerelease.length > 0;
460
+ if (leftHasPrerelease && !rightHasPrerelease) {
461
+ return -1;
462
+ }
463
+ if (!leftHasPrerelease && rightHasPrerelease) {
464
+ return 1;
465
+ }
466
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
467
+ for (let index = 0; index < length; index += 1) {
468
+ const leftValue = left.prerelease[index];
469
+ const rightValue = right.prerelease[index];
470
+ if (leftValue === void 0) {
471
+ return -1;
472
+ }
473
+ if (rightValue === void 0) {
474
+ return 1;
475
+ }
476
+ const leftNumeric = /^\d+$/.test(leftValue) ? Number.parseInt(leftValue, 10) : null;
477
+ const rightNumeric = /^\d+$/.test(rightValue) ? Number.parseInt(rightValue, 10) : null;
478
+ if (leftNumeric !== null && rightNumeric !== null) {
479
+ if (leftNumeric !== rightNumeric) {
480
+ return leftNumeric > rightNumeric ? 1 : -1;
481
+ }
482
+ continue;
483
+ }
484
+ if (leftNumeric !== null) {
485
+ return -1;
486
+ }
487
+ if (rightNumeric !== null) {
488
+ return 1;
489
+ }
490
+ if (leftValue !== rightValue) {
491
+ return leftValue > rightValue ? 1 : -1;
492
+ }
493
+ }
494
+ return 0;
495
+ }
496
+ function gt(a, b) {
497
+ return compare(a, b) > 0;
498
+ }
499
+ function lt(a, b) {
500
+ return compare(a, b) < 0;
501
+ }
502
+ function eq(a, b) {
503
+ return compare(a, b) === 0;
504
+ }
505
+ function increment(version, release, prerelease) {
506
+ const parsed = parse(version);
507
+ if (!parsed) {
508
+ throw new Error(`Invalid SemVer version: ${version}`);
509
+ }
510
+ if (release === "major") {
511
+ return `${parsed.major + 1}.0.0${prerelease ? `-${prerelease}` : ""}`;
512
+ }
513
+ if (release === "minor") {
514
+ return `${parsed.major}.${parsed.minor + 1}.0${prerelease ? `-${prerelease}` : ""}`;
515
+ }
516
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}${prerelease ? `-${prerelease}` : ""}`;
517
+ }
518
+ function format(version) {
519
+ let output = `${version.major}.${version.minor}.${version.patch}`;
520
+ if (version.prerelease.length > 0) {
521
+ output += `-${version.prerelease.join(".")}`;
522
+ }
523
+ if (version.build.length > 0) {
524
+ output += `+${version.build.join(".")}`;
525
+ }
526
+ return output;
527
+ }
528
+ const semver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
529
+ __proto__: null,
530
+ compare,
531
+ eq,
532
+ format,
533
+ gt,
534
+ increment,
535
+ lt,
536
+ parse,
537
+ validate: validate$1
538
+ }, Symbol.toStringTag, { value: "Module" }));
539
+ function resolveFiles(patterns, cwd, ignore = []) {
540
+ return [
541
+ ...new Set(patterns.flatMap((pattern) => globSync(pattern, { cwd, absolute: true, ignore })))
542
+ ].sort();
543
+ }
544
+ function getLineNumber(content, offset) {
545
+ return content.slice(0, offset).split("\n").length;
546
+ }
547
+ function extractVersion(groups) {
548
+ return groups[1] ?? groups[0] ?? "";
549
+ }
550
+ function applyTemplate(template, groups, version) {
551
+ return template.replace(/\$(\d+)|\{\{version\}\}/g, (match, groupIndex) => {
552
+ if (match === "{{version}}") {
553
+ return version;
554
+ }
555
+ return groups[Number.parseInt(groupIndex ?? "0", 10) - 1] ?? "";
556
+ });
557
+ }
558
+ function stringifyCapture(value) {
559
+ return typeof value === "string" ? value : "";
560
+ }
561
+ function syncVersion(version, config, cwd = process.cwd()) {
562
+ return resolveFiles(config.files, cwd).map(
563
+ (filePath) => syncFile(filePath, version, config.patterns)
564
+ );
565
+ }
566
+ function syncFile(filePath, version, patterns) {
567
+ const original = fs.readFileSync(filePath, "utf-8");
568
+ let updatedContent = original;
569
+ const changes = [];
570
+ for (const pattern of patterns) {
571
+ const regex = new RegExp(pattern.regex, "gm");
572
+ updatedContent = updatedContent.replace(regex, (match, ...args) => {
573
+ const hasNamedGroups = typeof args.at(-1) === "object" && args.at(-1) !== null;
574
+ const offsetIndex = hasNamedGroups ? -3 : -2;
575
+ const offset = args.at(offsetIndex);
576
+ const groups = args.slice(0, offsetIndex).map((value) => stringifyCapture(value));
577
+ const found = extractVersion(groups);
578
+ if (found === "Unreleased") {
579
+ return match;
580
+ }
581
+ if (found !== version) {
582
+ changes.push({
583
+ line: getLineNumber(updatedContent, offset),
584
+ oldValue: found,
585
+ newValue: version
586
+ });
587
+ }
588
+ return applyTemplate(pattern.template, groups, version) || match;
589
+ });
590
+ }
591
+ const result = {
592
+ file: filePath,
593
+ updated: updatedContent !== original,
594
+ changes
595
+ };
596
+ if (result.updated) {
597
+ fs.writeFileSync(filePath, updatedContent, "utf-8");
598
+ }
599
+ return result;
600
+ }
601
+ function checkHardcodedVersions(expectedVersion, config, ignorePatterns, cwd = process.cwd()) {
602
+ const mismatches = [];
603
+ const files = resolveFiles(config.files, cwd, ignorePatterns);
604
+ for (const filePath of files) {
605
+ const content = fs.readFileSync(filePath, "utf-8");
606
+ for (const pattern of config.patterns) {
607
+ const regex = new RegExp(pattern.regex, "gm");
608
+ let match = regex.exec(content);
609
+ while (match) {
610
+ const found = extractVersion(match.slice(1));
611
+ if (found !== "Unreleased" && found !== expectedVersion) {
612
+ mismatches.push({
613
+ file: path.relative(cwd, filePath),
614
+ line: getLineNumber(content, match.index),
615
+ found
616
+ });
617
+ }
618
+ match = regex.exec(content);
619
+ }
620
+ }
621
+ }
622
+ return mismatches;
623
+ }
624
+ const CONFIG_FILE_NAMES = [
625
+ ".versionguard.yml",
626
+ ".versionguard.yaml",
627
+ "versionguard.yml",
628
+ "versionguard.yaml"
629
+ ];
630
+ const DEFAULT_CONFIG = {
631
+ versioning: {
632
+ type: "semver",
633
+ calver: {
634
+ format: "YYYY.MM.PATCH",
635
+ preventFutureDates: true
636
+ }
637
+ },
638
+ sync: {
639
+ files: ["README.md", "CHANGELOG.md"],
640
+ patterns: [
641
+ {
642
+ regex: `(version\\s*[=:]\\s*["'])(.+?)(["'])`,
643
+ template: "$1{{version}}$3"
644
+ },
645
+ {
646
+ regex: "(##\\s*\\[)(.+?)(\\])",
647
+ template: "$1{{version}}$3"
648
+ }
649
+ ]
650
+ },
651
+ changelog: {
652
+ enabled: true,
653
+ file: "CHANGELOG.md",
654
+ strict: true,
655
+ requireEntry: true
656
+ },
657
+ git: {
658
+ hooks: {
659
+ "pre-commit": true,
660
+ "pre-push": true,
661
+ "post-tag": true
662
+ },
663
+ enforceHooks: true
664
+ },
665
+ ignore: ["node_modules/**", "dist/**", ".git/**", "*.lock", "package-lock.json"]
666
+ };
667
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
668
+ function getDefaultConfig() {
669
+ return structuredClone(DEFAULT_CONFIG);
670
+ }
671
+ function findConfig(cwd = process.cwd()) {
672
+ for (const fileName of CONFIG_FILE_NAMES) {
673
+ const fullPath = path.join(cwd, fileName);
674
+ if (fs.existsSync(fullPath)) {
675
+ return fullPath;
676
+ }
677
+ }
678
+ return null;
679
+ }
680
+ function loadConfig(configPath) {
681
+ const content = fs.readFileSync(configPath, "utf-8");
682
+ const parsed = yaml.load(content);
683
+ if (parsed === void 0) {
684
+ return getDefaultConfig();
685
+ }
686
+ if (!isPlainObject(parsed)) {
687
+ throw new Error(`Config file must contain a YAML object: ${configPath}`);
688
+ }
689
+ return mergeDeep(getDefaultConfig(), parsed);
690
+ }
691
+ function getConfig(cwd = process.cwd()) {
692
+ const configPath = findConfig(cwd);
693
+ return configPath ? loadConfig(configPath) : getDefaultConfig();
694
+ }
695
+ function initConfig(cwd = process.cwd()) {
696
+ const configPath = path.join(cwd, ".versionguard.yml");
697
+ const existingConfigPath = findConfig(cwd);
698
+ if (existingConfigPath) {
699
+ throw new Error(`Config file already exists: ${existingConfigPath}`);
700
+ }
701
+ const examplePath = path.join(MODULE_DIR, "..", ".versionguard.yml.example");
702
+ const content = fs.existsSync(examplePath) ? fs.readFileSync(examplePath, "utf-8") : generateDefaultConfig();
703
+ fs.writeFileSync(configPath, content, "utf-8");
704
+ return configPath;
705
+ }
706
+ function generateDefaultConfig() {
707
+ return `# VersionGuard Configuration
708
+ versioning:
709
+ type: semver
710
+
711
+ sync:
712
+ files:
713
+ - "README.md"
714
+ - "CHANGELOG.md"
715
+ patterns:
716
+ - regex: '(version\\s*[=:]\\s*["''])(.+?)(["''])'
717
+ template: '$1{{version}}$3'
718
+ - regex: '(##\\s*\\[)(.+?)(\\])'
719
+ template: '$1{{version}}$3'
720
+
721
+ changelog:
722
+ enabled: true
723
+ file: "CHANGELOG.md"
724
+ strict: true
725
+ requireEntry: true
726
+
727
+ git:
728
+ hooks:
729
+ pre-commit: true
730
+ pre-push: true
731
+ post-tag: true
732
+ enforceHooks: true
733
+
734
+ ignore:
735
+ - "node_modules/**"
736
+ - "dist/**"
737
+ - ".git/**"
738
+ `;
739
+ }
740
+ function isPlainObject(value) {
741
+ return typeof value === "object" && value !== null && !Array.isArray(value);
742
+ }
743
+ function mergeDeep(target, source) {
744
+ if (!isPlainObject(target) || !isPlainObject(source)) {
745
+ return source ?? target;
746
+ }
747
+ const output = { ...target };
748
+ for (const [key, value] of Object.entries(source)) {
749
+ const current = output[key];
750
+ output[key] = isPlainObject(current) && isPlainObject(value) ? mergeDeep(current, value) : value;
751
+ }
752
+ return output;
753
+ }
754
+ function getVersionFeedback(version, config, previousVersion) {
755
+ if (config.versioning.type === "semver") {
756
+ return getSemVerFeedback(version, previousVersion);
757
+ }
758
+ return getCalVerFeedback(version, getCalVerConfig$2(config), previousVersion);
759
+ }
760
+ function getCalVerConfig$2(config) {
761
+ if (!config.versioning.calver) {
762
+ throw new Error('CalVer configuration is required when versioning.type is "calver"');
763
+ }
764
+ return config.versioning.calver;
765
+ }
766
+ function getSemVerFeedback(version, previousVersion) {
767
+ const errors = [];
768
+ const suggestions = [];
769
+ const parsed = parse(version);
770
+ if (!parsed) {
771
+ const validation = validate$1(version);
772
+ if (version.startsWith("v")) {
773
+ const cleanVersion = version.slice(1);
774
+ errors.push({
775
+ message: `Version should not start with 'v': ${version}`,
776
+ severity: "error"
777
+ });
778
+ suggestions.push({
779
+ message: `Remove the 'v' prefix`,
780
+ fix: `npm version ${cleanVersion}`,
781
+ autoFixable: true
782
+ });
783
+ } else if (version.split(".").length === 2) {
784
+ errors.push({
785
+ message: `Version missing patch number: ${version}`,
786
+ severity: "error"
787
+ });
788
+ suggestions.push({
789
+ message: `Add patch number (e.g., ${version}.0)`,
790
+ fix: `npm version ${version}.0`,
791
+ autoFixable: true
792
+ });
793
+ } else if (/^\d+\.\d+\.\d+\.\d+$/.test(version)) {
794
+ errors.push({
795
+ message: `Version has too many segments: ${version}`,
796
+ severity: "error"
797
+ });
798
+ suggestions.push({
799
+ message: `Use only 3 segments (MAJOR.MINOR.PATCH)`,
800
+ autoFixable: false
801
+ });
802
+ } else if (validation.errors.some((error) => error.message.includes("leading zero"))) {
803
+ errors.push(...validation.errors);
804
+ suggestions.push({
805
+ message: `Remove leading zeros (e.g., 1.2.3 instead of 01.02.03)`,
806
+ autoFixable: false
807
+ });
808
+ } else {
809
+ errors.push(
810
+ ...validation.errors.length > 0 ? validation.errors : [
811
+ {
812
+ message: `Invalid SemVer format: ${version}`,
813
+ severity: "error"
814
+ }
815
+ ]
816
+ );
817
+ suggestions.push({
818
+ message: `Use format: MAJOR.MINOR.PATCH (e.g., 1.0.0)`,
819
+ autoFixable: false
820
+ });
821
+ }
822
+ return {
823
+ valid: false,
824
+ errors,
825
+ suggestions,
826
+ canAutoFix: suggestions.some((s) => s.autoFixable)
827
+ };
828
+ }
829
+ if (previousVersion) {
830
+ const prevParsed = parse(previousVersion);
831
+ if (prevParsed) {
832
+ const comparison = compare(version, previousVersion);
833
+ if (comparison < 0) {
834
+ errors.push({
835
+ message: `Version ${version} is older than previous ${previousVersion}`,
836
+ severity: "error"
837
+ });
838
+ suggestions.push({
839
+ message: `Version must be greater than ${previousVersion}`,
840
+ fix: `npm version ${increment(previousVersion, "patch")}`,
841
+ autoFixable: true
842
+ });
843
+ } else if (comparison === 0) {
844
+ errors.push({
845
+ message: `Version ${version} is the same as previous`,
846
+ severity: "error"
847
+ });
848
+ suggestions.push({
849
+ message: `Bump the version`,
850
+ fix: `npm version ${increment(previousVersion, "patch")}`,
851
+ autoFixable: true
852
+ });
853
+ } else {
854
+ const majorJump = parsed.major - prevParsed.major;
855
+ const minorJump = parsed.minor - prevParsed.minor;
856
+ const patchJump = parsed.patch - prevParsed.patch;
857
+ if (majorJump > 1) {
858
+ suggestions.push({
859
+ message: `⚠️ Major version jumped by ${majorJump} (from ${previousVersion} to ${version})`,
860
+ autoFixable: false
861
+ });
862
+ }
863
+ if (minorJump > 10) {
864
+ suggestions.push({
865
+ message: `⚠️ Minor version jumped by ${minorJump} - did you mean to do a major bump?`,
866
+ autoFixable: false
867
+ });
868
+ }
869
+ if (patchJump > 20) {
870
+ suggestions.push({
871
+ message: `⚠️ Patch version jumped by ${patchJump} - consider a minor bump instead`,
872
+ autoFixable: false
873
+ });
874
+ }
875
+ }
876
+ }
877
+ }
878
+ return {
879
+ valid: errors.length === 0,
880
+ errors,
881
+ suggestions,
882
+ canAutoFix: suggestions.some((s) => s.autoFixable)
883
+ };
884
+ }
885
+ function getCalVerFeedback(version, calverConfig, previousVersion) {
886
+ const errors = [];
887
+ const suggestions = [];
888
+ const { format: format2, preventFutureDates } = calverConfig;
889
+ const parsed = parse$1(version, format2);
890
+ if (!parsed) {
891
+ errors.push({
892
+ message: `Invalid CalVer format: ${version}`,
893
+ severity: "error"
894
+ });
895
+ suggestions.push({
896
+ message: `Expected format: ${format2}`,
897
+ fix: `Update package.json to use current date: "${getCurrentVersion(format2)}"`,
898
+ autoFixable: true
899
+ });
900
+ return { valid: false, errors, suggestions, canAutoFix: true };
901
+ }
902
+ const validation = validate$2(version, format2, preventFutureDates);
903
+ errors.push(...validation.errors);
904
+ const now = /* @__PURE__ */ new Date();
905
+ if (validation.errors.some((error) => error.message.startsWith("Invalid month:"))) {
906
+ suggestions.push({
907
+ message: `Month must be between 1-12`,
908
+ autoFixable: false
909
+ });
910
+ }
911
+ if (validation.errors.some((error) => error.message.startsWith("Invalid day:"))) {
912
+ suggestions.push({
913
+ message: `Day must be valid for the selected month`,
914
+ autoFixable: false
915
+ });
916
+ }
917
+ if (preventFutureDates && parsed.year > now.getFullYear()) {
918
+ suggestions.push({
919
+ message: `Use current year (${now.getFullYear()}) or a past year`,
920
+ fix: `npm version ${formatCalVerVersion({ ...parsed, year: now.getFullYear() })}`,
921
+ autoFixable: true
922
+ });
923
+ }
924
+ if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month > now.getMonth() + 1) {
925
+ suggestions.push({
926
+ message: `Current month is ${now.getMonth() + 1}`,
927
+ fix: `npm version ${formatCalVerVersion({ ...parsed, month: now.getMonth() + 1 })}`,
928
+ autoFixable: true
929
+ });
930
+ }
931
+ if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month === now.getMonth() + 1 && parsed.day !== void 0 && parsed.day > now.getDate()) {
932
+ suggestions.push({
933
+ message: `Current day is ${now.getDate()}`,
934
+ fix: `npm version ${formatCalVerVersion({ ...parsed, day: now.getDate() })}`,
935
+ autoFixable: true
936
+ });
937
+ }
938
+ if (previousVersion) {
939
+ const prevParsed = parse$1(previousVersion, format2);
940
+ if (prevParsed) {
941
+ if (compare$1(version, previousVersion, format2) <= 0) {
942
+ errors.push({
943
+ message: `Version ${version} is not newer than previous ${previousVersion}`,
944
+ severity: "error"
945
+ });
946
+ suggestions.push({
947
+ message: `CalVer must increase over time`,
948
+ fix: `npm version ${increment$1(previousVersion, format2)}`,
949
+ autoFixable: true
950
+ });
951
+ }
952
+ }
953
+ }
954
+ return {
955
+ valid: errors.length === 0,
956
+ errors,
957
+ suggestions,
958
+ canAutoFix: suggestions.some((s) => s.autoFixable)
959
+ };
960
+ }
961
+ function formatCalVerVersion(version) {
962
+ return format$1({
963
+ ...version,
964
+ raw: version.raw || ""
965
+ });
966
+ }
967
+ function getSyncFeedback(file, foundVersion, expectedVersion) {
968
+ const suggestions = [
969
+ {
970
+ message: `${file} has version "${foundVersion}" but should be "${expectedVersion}"`,
971
+ fix: `npx versionguard sync`,
972
+ autoFixable: true
973
+ }
974
+ ];
975
+ if (file.endsWith(".md")) {
976
+ suggestions.push({
977
+ message: `For markdown files, check headers like "## [${expectedVersion}]"`,
978
+ autoFixable: false
979
+ });
980
+ }
981
+ if (file.endsWith(".ts") || file.endsWith(".js")) {
982
+ suggestions.push({
983
+ message: `For code files, check constants like "export const VERSION = '${expectedVersion}'"`,
984
+ autoFixable: false
985
+ });
986
+ }
987
+ return suggestions;
988
+ }
989
+ function getChangelogFeedback(hasEntry, version, latestChangelogVersion) {
990
+ const suggestions = [];
991
+ if (!hasEntry) {
992
+ suggestions.push({
993
+ message: `CHANGELOG.md is missing entry for version ${version}`,
994
+ fix: `npx versionguard fix`,
995
+ autoFixable: true
996
+ });
997
+ suggestions.push({
998
+ message: `Or manually add: "## [${version}] - YYYY-MM-DD" under [Unreleased]`,
999
+ autoFixable: false
1000
+ });
1001
+ }
1002
+ if (latestChangelogVersion && latestChangelogVersion !== version) {
1003
+ suggestions.push({
1004
+ message: `CHANGELOG.md latest entry is ${latestChangelogVersion}, but package.json is ${version}`,
1005
+ fix: `Make sure versions are in sync`,
1006
+ autoFixable: false
1007
+ });
1008
+ }
1009
+ return suggestions;
1010
+ }
1011
+ function getTagFeedback(tagVersion, packageVersion, hasUnsyncedFiles) {
1012
+ const suggestions = [];
1013
+ if (tagVersion !== packageVersion) {
1014
+ suggestions.push({
1015
+ message: `Git tag "${tagVersion}" doesn't match package.json "${packageVersion}"`,
1016
+ fix: `Delete tag and recreate: git tag -d ${tagVersion} && git tag ${packageVersion}`,
1017
+ autoFixable: false
1018
+ });
1019
+ }
1020
+ if (hasUnsyncedFiles) {
1021
+ suggestions.push({
1022
+ message: `Files are out of sync with version ${packageVersion}`,
1023
+ fix: `npx versionguard sync`,
1024
+ autoFixable: true
1025
+ });
1026
+ }
1027
+ return suggestions;
1028
+ }
1029
+ function fixPackageVersion(targetVersion, cwd = process.cwd()) {
1030
+ const packagePath = path.join(cwd, "package.json");
1031
+ if (!fs.existsSync(packagePath)) {
1032
+ return { fixed: false, message: "package.json not found" };
1033
+ }
1034
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1035
+ const oldVersion = typeof pkg.version === "string" ? pkg.version : void 0;
1036
+ if (oldVersion === targetVersion) {
1037
+ return { fixed: false, message: `Already at version ${targetVersion}` };
1038
+ }
1039
+ setPackageVersion(targetVersion, cwd);
1040
+ return {
1041
+ fixed: true,
1042
+ message: `Updated package.json from ${oldVersion} to ${targetVersion}`,
1043
+ file: packagePath
1044
+ };
1045
+ }
1046
+ function fixSyncIssues(config, cwd = process.cwd()) {
1047
+ const version = getPackageVersion(cwd);
1048
+ const results = syncVersion(version, config.sync, cwd).filter((result) => result.updated).map((result) => ({
1049
+ fixed: true,
1050
+ message: `Updated ${path.relative(cwd, result.file)} (${result.changes.length} changes)`,
1051
+ file: result.file
1052
+ }));
1053
+ if (results.length === 0) {
1054
+ results.push({ fixed: false, message: "All files already in sync" });
1055
+ }
1056
+ return results;
1057
+ }
1058
+ function fixChangelog(version, config, cwd = process.cwd()) {
1059
+ const changelogPath = path.join(cwd, config.changelog.file);
1060
+ if (!fs.existsSync(changelogPath)) {
1061
+ createInitialChangelog(changelogPath, version);
1062
+ return {
1063
+ fixed: true,
1064
+ message: `Created ${config.changelog.file} with entry for ${version}`,
1065
+ file: changelogPath
1066
+ };
1067
+ }
1068
+ const content = fs.readFileSync(changelogPath, "utf-8");
1069
+ if (content.includes(`## [${version}]`)) {
1070
+ return { fixed: false, message: `Changelog already has entry for ${version}` };
1071
+ }
1072
+ addVersionEntry(changelogPath, version);
1073
+ return {
1074
+ fixed: true,
1075
+ message: `Added entry for ${version} to ${config.changelog.file}`,
1076
+ file: changelogPath
1077
+ };
1078
+ }
1079
+ function createInitialChangelog(changelogPath, version) {
1080
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1081
+ const content = `# Changelog
1082
+
1083
+ All notable changes to this project will be documented in this file.
1084
+
1085
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1086
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1087
+
1088
+ ## [Unreleased]
1089
+
1090
+ ## [${version}] - ${today}
1091
+
1092
+ ### Added
1093
+
1094
+ - Initial release
1095
+
1096
+ [Unreleased]: https://github.com/yourorg/project/compare/v${version}...HEAD
1097
+ [${version}]: https://github.com/yourorg/project/releases/tag/v${version}
1098
+ `;
1099
+ fs.writeFileSync(changelogPath, content, "utf-8");
1100
+ }
1101
+ function fixAll(config, targetVersion, cwd = process.cwd()) {
1102
+ const results = [];
1103
+ const version = targetVersion || getPackageVersion(cwd);
1104
+ if (targetVersion && targetVersion !== getPackageVersion(cwd)) {
1105
+ results.push(fixPackageVersion(targetVersion, cwd));
1106
+ }
1107
+ const syncResults = fixSyncIssues(config, cwd);
1108
+ results.push(...syncResults);
1109
+ if (config.changelog.enabled) {
1110
+ const changelogResult = fixChangelog(version, config, cwd);
1111
+ if (changelogResult.fixed) {
1112
+ results.push(changelogResult);
1113
+ }
1114
+ }
1115
+ return results;
1116
+ }
1117
+ function suggestNextVersion(currentVersion, config, changeType) {
1118
+ const suggestions = [];
1119
+ if (config.versioning.type === "semver") {
1120
+ if (!changeType || changeType === "auto" || changeType === "patch") {
1121
+ suggestions.push({
1122
+ version: increment(currentVersion, "patch"),
1123
+ reason: "Patch - bug fixes, small changes"
1124
+ });
1125
+ }
1126
+ if (!changeType || changeType === "auto" || changeType === "minor") {
1127
+ suggestions.push({
1128
+ version: increment(currentVersion, "minor"),
1129
+ reason: "Minor - new features, backwards compatible"
1130
+ });
1131
+ }
1132
+ if (!changeType || changeType === "auto" || changeType === "major") {
1133
+ suggestions.push({
1134
+ version: increment(currentVersion, "major"),
1135
+ reason: "Major - breaking changes"
1136
+ });
1137
+ }
1138
+ } else {
1139
+ const format2 = getCalVerConfig$1(config).format;
1140
+ const currentCal = getCurrentVersion(format2);
1141
+ suggestions.push({
1142
+ version: currentCal,
1143
+ reason: "Current date - new release today"
1144
+ });
1145
+ suggestions.push({
1146
+ version: increment$1(currentVersion, format2),
1147
+ reason: "Increment patch - additional release today"
1148
+ });
1149
+ }
1150
+ return suggestions;
1151
+ }
1152
+ function getCalVerConfig$1(config) {
1153
+ if (!config.versioning.calver) {
1154
+ throw new Error('CalVer configuration is required when versioning.type is "calver"');
1155
+ }
1156
+ return config.versioning.calver;
1157
+ }
1158
+ function runGit(cwd, args, encoding) {
1159
+ return childProcess.execFileSync("git", args, {
1160
+ cwd,
1161
+ encoding,
1162
+ stdio: ["pipe", "pipe", "ignore"]
1163
+ });
1164
+ }
1165
+ function runGitText(cwd, args) {
1166
+ return runGit(cwd, args, "utf-8");
1167
+ }
1168
+ function getLatestTag(cwd = process.cwd()) {
1169
+ try {
1170
+ const result = runGitText(cwd, ["describe", "--tags", "--abbrev=0"]);
1171
+ const tagName = result.trim();
1172
+ const version = tagName.replace(/^v/, "");
1173
+ const dateResult = runGitText(cwd, ["log", "-1", "--format=%ai", tagName]);
1174
+ const messageResult = runGitText(cwd, ["tag", "-l", tagName, "--format=%(contents)"]);
1175
+ const message = messageResult.trim();
1176
+ return {
1177
+ name: tagName,
1178
+ version,
1179
+ message: message.length > 0 ? message : void 0,
1180
+ date: new Date(dateResult.trim())
1181
+ };
1182
+ } catch {
1183
+ return null;
1184
+ }
1185
+ }
1186
+ function getAllTags(cwd = process.cwd()) {
1187
+ try {
1188
+ const result = runGitText(cwd, ["tag", "--list"]);
1189
+ return result.trim().split("\n").filter(Boolean).map((name) => ({
1190
+ name,
1191
+ version: name.replace(/^v/, ""),
1192
+ date: /* @__PURE__ */ new Date()
1193
+ // Would need individual lookup for accurate dates
1194
+ }));
1195
+ } catch {
1196
+ return [];
1197
+ }
1198
+ }
1199
+ function createTag(version, message, autoFix = true, config, cwd = process.cwd()) {
1200
+ const actions = [];
1201
+ try {
1202
+ if (!config) {
1203
+ return {
1204
+ success: false,
1205
+ message: "VersionGuard config is required to create tags safely",
1206
+ actions
1207
+ };
1208
+ }
1209
+ const packageVersion = getPackageVersion(cwd);
1210
+ const shouldAutoFix = autoFix;
1211
+ const preflightError = getTagPreflightError(config, cwd, version, shouldAutoFix);
1212
+ if (preflightError) {
1213
+ return {
1214
+ success: false,
1215
+ message: preflightError,
1216
+ actions
1217
+ };
1218
+ }
1219
+ if (version !== packageVersion && !autoFix) {
1220
+ return {
1221
+ success: false,
1222
+ message: `Version mismatch: package.json is ${packageVersion}, tag is ${version}`,
1223
+ actions: []
1224
+ };
1225
+ }
1226
+ if (autoFix) {
1227
+ const fixResults = version !== packageVersion ? fixAll(config, version, cwd) : fixAll(config, void 0, cwd);
1228
+ for (const result of fixResults) {
1229
+ if (result.fixed) {
1230
+ actions.push(result.message);
1231
+ }
1232
+ }
1233
+ if (fixResults.some((result) => result.fixed)) {
1234
+ runGit(cwd, ["add", "-A"]);
1235
+ runGit(cwd, ["commit", "--no-verify", "-m", `chore(release): ${version}`]);
1236
+ actions.push("Committed version changes");
1237
+ }
1238
+ }
1239
+ const tagName = `v${version}`;
1240
+ const tagMessage = message || `Release ${version}`;
1241
+ if (getAllTags(cwd).some((tag) => tag.name === tagName)) {
1242
+ return {
1243
+ success: false,
1244
+ message: `Tag ${tagName} already exists`,
1245
+ actions
1246
+ };
1247
+ }
1248
+ runGit(cwd, ["tag", "-a", tagName, "-m", tagMessage]);
1249
+ actions.push(`Created tag ${tagName}`);
1250
+ return {
1251
+ success: true,
1252
+ message: `Successfully created tag ${tagName}`,
1253
+ actions
1254
+ };
1255
+ } catch (err) {
1256
+ return {
1257
+ success: false,
1258
+ message: `Failed to create tag: ${err.message}`,
1259
+ actions
1260
+ };
1261
+ }
1262
+ }
1263
+ function handlePostTag(config, cwd = process.cwd()) {
1264
+ const actions = [];
1265
+ try {
1266
+ const preflightError = getTagPreflightError(config, cwd);
1267
+ if (preflightError) {
1268
+ return {
1269
+ success: false,
1270
+ message: preflightError,
1271
+ actions
1272
+ };
1273
+ }
1274
+ const tag = getLatestTag(cwd);
1275
+ if (!tag) {
1276
+ return {
1277
+ success: false,
1278
+ message: "No tag found",
1279
+ actions
1280
+ };
1281
+ }
1282
+ const packageVersion = getPackageVersion(cwd);
1283
+ if (tag.version !== packageVersion) {
1284
+ return {
1285
+ success: false,
1286
+ message: `Tag version ${tag.version} doesn't match package.json ${packageVersion}`,
1287
+ actions: [
1288
+ "To fix: delete tag and recreate with correct version",
1289
+ ` git tag -d ${tag.name}`,
1290
+ ` npm version ${tag.version}`,
1291
+ ` git tag ${tag.name}`
1292
+ ]
1293
+ };
1294
+ }
1295
+ const syncResults = fixAll(config, packageVersion, cwd);
1296
+ for (const result of syncResults) {
1297
+ if (result.fixed) {
1298
+ actions.push(result.message);
1299
+ }
1300
+ }
1301
+ return {
1302
+ success: true,
1303
+ message: `Post-tag workflow completed for ${tag.name}`,
1304
+ actions
1305
+ };
1306
+ } catch (err) {
1307
+ return {
1308
+ success: false,
1309
+ message: `Post-tag workflow failed: ${err.message}`,
1310
+ actions
1311
+ };
1312
+ }
1313
+ }
1314
+ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false) {
1315
+ if (config.git.enforceHooks && !areHooksInstalled(cwd)) {
1316
+ return "Git hooks must be installed before creating or validating release tags";
1317
+ }
1318
+ if (hasDirtyWorktree(cwd)) {
1319
+ return "Working tree must be clean before creating or validating release tags";
1320
+ }
1321
+ const version = expectedVersion ?? getPackageVersion(cwd);
1322
+ const versionResult = config.versioning.type === "semver" ? validate$1(version) : validate$2(
1323
+ version,
1324
+ config.versioning.calver?.format ?? "YYYY.MM.PATCH",
1325
+ config.versioning.calver?.preventFutureDates ?? true
1326
+ );
1327
+ if (!versionResult.valid) {
1328
+ return versionResult.errors[0]?.message ?? `Invalid version: ${version}`;
1329
+ }
1330
+ if (allowAutoFix) {
1331
+ return null;
1332
+ }
1333
+ const mismatches = checkHardcodedVersions(version, config.sync, config.ignore, cwd);
1334
+ if (mismatches.length > 0) {
1335
+ const mismatch = mismatches[0];
1336
+ return `Version mismatch in ${mismatch.file}:${mismatch.line} - found "${mismatch.found}" but expected "${version}"`;
1337
+ }
1338
+ const changelogResult = validateChangelog(
1339
+ path.join(cwd, config.changelog.file),
1340
+ version,
1341
+ config.changelog.strict,
1342
+ config.changelog.requireEntry
1343
+ );
1344
+ if (!changelogResult.valid) {
1345
+ return changelogResult.errors[0] ?? "Changelog validation failed";
1346
+ }
1347
+ return null;
1348
+ }
1349
+ function hasDirtyWorktree(cwd) {
1350
+ try {
1351
+ return runGitText(cwd, ["status", "--porcelain"]).trim().length > 0;
1352
+ } catch {
1353
+ return true;
1354
+ }
1355
+ }
1356
+ function validateTagForPush(tagName, cwd = process.cwd()) {
1357
+ try {
1358
+ runGit(cwd, ["rev-parse", tagName]);
1359
+ try {
1360
+ runGit(cwd, ["ls-remote", "--tags", "origin", tagName]);
1361
+ const localHash = runGitText(cwd, ["rev-parse", tagName]).trim();
1362
+ const remoteOutput = runGitText(cwd, ["ls-remote", "--tags", "origin", tagName]).trim();
1363
+ if (remoteOutput && !remoteOutput.includes(localHash)) {
1364
+ return {
1365
+ valid: false,
1366
+ message: `Tag ${tagName} exists on remote with different commit`,
1367
+ fix: `Delete remote tag first: git push origin :refs/tags/${tagName}`
1368
+ };
1369
+ }
1370
+ } catch {
1371
+ return { valid: true, message: `Tag ${tagName} is valid for push` };
1372
+ }
1373
+ return { valid: true, message: `Tag ${tagName} is valid for push` };
1374
+ } catch {
1375
+ return {
1376
+ valid: false,
1377
+ message: `Tag ${tagName} not found locally`,
1378
+ fix: `Create tag: git tag ${tagName}`
1379
+ };
1380
+ }
1381
+ }
1382
+ function suggestTagMessage(version, cwd = process.cwd()) {
1383
+ try {
1384
+ const changelogPath = path.join(cwd, "CHANGELOG.md");
1385
+ if (fs.existsSync(changelogPath)) {
1386
+ const content = fs.readFileSync(changelogPath, "utf-8");
1387
+ const versionRegex = new RegExp(
1388
+ `## \\[${version}\\].*?\\n(.*?)(?=\\n## \\[|\\n\\n## |$)`,
1389
+ "s"
1390
+ );
1391
+ const match = content.match(versionRegex);
1392
+ if (match) {
1393
+ const bulletMatch = match[1].match(/- (.+)/);
1394
+ if (bulletMatch) {
1395
+ return `Release ${version}: ${bulletMatch[1].trim()}`;
1396
+ }
1397
+ }
1398
+ }
1399
+ } catch {
1400
+ return `Release ${version}`;
1401
+ }
1402
+ return `Release ${version}`;
1403
+ }
1404
+ function validateVersion(version, config) {
1405
+ if (config.versioning.type === "semver") {
1406
+ return validate$1(version);
1407
+ }
1408
+ const calverConfig = getCalVerConfig(config);
1409
+ return validate$2(version, calverConfig.format, calverConfig.preventFutureDates);
1410
+ }
1411
+ function validate(config, cwd = process.cwd()) {
1412
+ const errors = [];
1413
+ let version;
1414
+ try {
1415
+ version = getPackageVersion(cwd);
1416
+ } catch (err) {
1417
+ return {
1418
+ valid: false,
1419
+ version: "",
1420
+ versionValid: false,
1421
+ syncValid: false,
1422
+ changelogValid: false,
1423
+ errors: [err.message]
1424
+ };
1425
+ }
1426
+ const versionResult = validateVersion(version, config);
1427
+ if (!versionResult.valid) {
1428
+ errors.push(...versionResult.errors.map((error) => error.message));
1429
+ }
1430
+ const hardcoded = checkHardcodedVersions(version, config.sync, config.ignore, cwd);
1431
+ if (hardcoded.length > 0) {
1432
+ for (const mismatch of hardcoded) {
1433
+ errors.push(
1434
+ `Version mismatch in ${mismatch.file}:${mismatch.line} - found "${mismatch.found}" but expected "${version}"`
1435
+ );
1436
+ }
1437
+ }
1438
+ let changelogValid = true;
1439
+ if (config.changelog.enabled) {
1440
+ const changelogPath = path.join(cwd, config.changelog.file);
1441
+ const changelogResult = validateChangelog(
1442
+ changelogPath,
1443
+ version,
1444
+ config.changelog.strict,
1445
+ config.changelog.requireEntry
1446
+ );
1447
+ if (!changelogResult.valid) {
1448
+ changelogValid = false;
1449
+ errors.push(...changelogResult.errors);
1450
+ }
1451
+ }
1452
+ return {
1453
+ valid: errors.length === 0,
1454
+ version,
1455
+ versionValid: versionResult.valid,
1456
+ syncValid: hardcoded.length === 0,
1457
+ changelogValid,
1458
+ errors
1459
+ };
1460
+ }
1461
+ function doctor(config, cwd = process.cwd()) {
1462
+ const validation = validate(config, cwd);
1463
+ const gitRepository = findGitDir(cwd) !== null;
1464
+ const hooksInstalled = gitRepository ? areHooksInstalled(cwd) : false;
1465
+ const worktreeClean = gitRepository ? isWorktreeClean(cwd) : true;
1466
+ const errors = [...validation.errors];
1467
+ if (gitRepository && config.git.enforceHooks && !hooksInstalled) {
1468
+ errors.push("Git hooks are not installed");
1469
+ }
1470
+ if (gitRepository && !worktreeClean) {
1471
+ errors.push("Working tree is not clean");
1472
+ }
1473
+ return {
1474
+ ready: errors.length === 0,
1475
+ version: validation.version,
1476
+ versionValid: validation.versionValid,
1477
+ syncValid: validation.syncValid,
1478
+ changelogValid: validation.changelogValid,
1479
+ gitRepository,
1480
+ hooksInstalled,
1481
+ worktreeClean,
1482
+ errors
1483
+ };
1484
+ }
1485
+ function sync(config, cwd = process.cwd()) {
1486
+ const version = getPackageVersion(cwd);
1487
+ syncVersion(version, config.sync, cwd);
1488
+ }
1489
+ function canBump(currentVersion, newVersion, config) {
1490
+ const currentValid = validateVersion(currentVersion, config);
1491
+ const newValid = validateVersion(newVersion, config);
1492
+ if (!currentValid.valid) {
1493
+ return { canBump: false, error: `Current version is invalid: ${currentVersion}` };
1494
+ }
1495
+ if (!newValid.valid) {
1496
+ return { canBump: false, error: `New version is invalid: ${newVersion}` };
1497
+ }
1498
+ if (config.versioning.type === "semver") {
1499
+ if (!gt(newVersion, currentVersion)) {
1500
+ return {
1501
+ canBump: false,
1502
+ error: `New version ${newVersion} must be greater than current ${currentVersion}`
1503
+ };
1504
+ }
1505
+ } else {
1506
+ const calverConfig = getCalVerConfig(config);
1507
+ const currentParsed = parse$1(currentVersion, calverConfig.format);
1508
+ const newParsed = parse$1(newVersion, calverConfig.format);
1509
+ if (!currentParsed || !newParsed) {
1510
+ return { canBump: false, error: "Failed to parse CalVer versions" };
1511
+ }
1512
+ if (compare$1(newVersion, currentVersion, calverConfig.format) <= 0) {
1513
+ return {
1514
+ canBump: false,
1515
+ error: `New CalVer ${newVersion} must be newer than current ${currentVersion}`
1516
+ };
1517
+ }
1518
+ }
1519
+ return { canBump: true };
1520
+ }
1521
+ function getCalVerConfig(config) {
1522
+ if (!config.versioning.calver) {
1523
+ throw new Error('CalVer configuration is required when versioning.type is "calver"');
1524
+ }
1525
+ return config.versioning.calver;
1526
+ }
1527
+ function isWorktreeClean(cwd) {
1528
+ try {
1529
+ return execSync("git status --porcelain", { cwd, encoding: "utf-8" }).trim().length === 0;
1530
+ } catch {
1531
+ return false;
1532
+ }
1533
+ }
1534
+ export {
1535
+ suggestTagMessage as A,
1536
+ sync as B,
1537
+ syncVersion as C,
1538
+ validateChangelog as D,
1539
+ validateTagForPush as E,
1540
+ validateVersion as F,
1541
+ getPackageVersion as a,
1542
+ getVersionFeedback as b,
1543
+ getSyncFeedback as c,
1544
+ getChangelogFeedback as d,
1545
+ doctor as e,
1546
+ fixAll as f,
1547
+ getConfig as g,
1548
+ handlePostTag as h,
1549
+ initConfig as i,
1550
+ fixSyncIssues as j,
1551
+ setPackageVersion as k,
1552
+ createTag as l,
1553
+ installHooks as m,
1554
+ areHooksInstalled as n,
1555
+ calver as o,
1556
+ canBump as p,
1557
+ checkHardcodedVersions as q,
1558
+ fixChangelog as r,
1559
+ suggestNextVersion as s,
1560
+ fixPackageVersion as t,
1561
+ uninstallHooks as u,
1562
+ validate as v,
1563
+ getAllTags as w,
1564
+ getLatestTag as x,
1565
+ getTagFeedback as y,
1566
+ semver as z
1567
+ };
1568
+ //# sourceMappingURL=index-DPBYoIRi.js.map