@codluv/versionguard 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2982 +0,0 @@
1
- import * as childProcess from "node:child_process";
2
- import { execFileSync, execSync } from "node:child_process";
3
- import * as path from "node:path";
4
- import * as fs from "node:fs";
5
- import { parse as parse$2 } from "smol-toml";
6
- import * as yaml from "js-yaml";
7
- import { globSync } from "glob";
8
- import { fileURLToPath } from "node:url";
9
- function validateModifier(modifier, schemeRules) {
10
- if (!modifier || !schemeRules?.allowedModifiers) return null;
11
- const baseModifier = modifier.replace(/[\d.]+$/, "") || modifier;
12
- if (!schemeRules.allowedModifiers.includes(baseModifier)) {
13
- return {
14
- message: `Modifier "${modifier}" is not allowed. Allowed: ${schemeRules.allowedModifiers.join(", ")}`,
15
- severity: "error"
16
- };
17
- }
18
- return null;
19
- }
20
- const VALID_TOKENS = /* @__PURE__ */ new Set([
21
- "YYYY",
22
- "YY",
23
- "0Y",
24
- "MM",
25
- "M",
26
- "0M",
27
- "WW",
28
- "0W",
29
- "DD",
30
- "D",
31
- "0D",
32
- "MICRO",
33
- "PATCH"
34
- ]);
35
- const YEAR_TOKENS = /* @__PURE__ */ new Set(["YYYY", "YY", "0Y"]);
36
- const MONTH_TOKENS = /* @__PURE__ */ new Set(["MM", "M", "0M"]);
37
- const WEEK_TOKENS = /* @__PURE__ */ new Set(["WW", "0W"]);
38
- const DAY_TOKENS = /* @__PURE__ */ new Set(["DD", "D", "0D"]);
39
- const COUNTER_TOKENS = /* @__PURE__ */ new Set(["MICRO", "PATCH"]);
40
- function isValidCalVerFormat(formatStr) {
41
- const tokens = formatStr.split(".");
42
- if (tokens.length < 2) return false;
43
- if (!tokens.every((t) => VALID_TOKENS.has(t))) return false;
44
- if (!YEAR_TOKENS.has(tokens[0])) return false;
45
- const hasWeek = tokens.some((t) => WEEK_TOKENS.has(t));
46
- const hasMonthOrDay = tokens.some((t) => MONTH_TOKENS.has(t) || DAY_TOKENS.has(t));
47
- if (hasWeek && hasMonthOrDay) return false;
48
- const counterIndex = tokens.findIndex((t) => COUNTER_TOKENS.has(t));
49
- if (counterIndex !== -1 && counterIndex !== tokens.length - 1) return false;
50
- return true;
51
- }
52
- function parseFormat(calverFormat) {
53
- const tokens = calverFormat.split(".");
54
- const result = {
55
- year: tokens[0]
56
- };
57
- for (let i = 1; i < tokens.length; i++) {
58
- const token = tokens[i];
59
- if (MONTH_TOKENS.has(token)) {
60
- result.month = token;
61
- } else if (WEEK_TOKENS.has(token)) {
62
- result.week = token;
63
- } else if (DAY_TOKENS.has(token)) {
64
- result.day = token;
65
- } else if (COUNTER_TOKENS.has(token)) {
66
- result.counter = token;
67
- }
68
- }
69
- return result;
70
- }
71
- const MODIFIER_PATTERN = "(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?";
72
- function tokenPattern(token) {
73
- switch (token) {
74
- case "YYYY":
75
- return "([1-9]\\d{3})";
76
- case "YY":
77
- return "(\\d{1,3})";
78
- case "0Y":
79
- return "(\\d{2,3})";
80
- case "MM":
81
- case "M":
82
- return "([1-9]|1[0-2])";
83
- case "0M":
84
- return "(0[1-9]|1[0-2])";
85
- case "WW":
86
- return "([1-9]|[1-4]\\d|5[0-3])";
87
- case "0W":
88
- return "(0[1-9]|[1-4]\\d|5[0-3])";
89
- case "DD":
90
- case "D":
91
- return "([1-9]|[12]\\d|3[01])";
92
- case "0D":
93
- return "(0[1-9]|[12]\\d|3[01])";
94
- case "MICRO":
95
- case "PATCH":
96
- return "(0|[1-9]\\d*)";
97
- default:
98
- throw new Error(`Unsupported CalVer token: ${token}`);
99
- }
100
- }
101
- function getRegexForFormat(calverFormat) {
102
- const tokens = calverFormat.split(".");
103
- const pattern = tokens.map(tokenPattern).join("\\.");
104
- return new RegExp(`^${pattern}${MODIFIER_PATTERN}$`);
105
- }
106
- function parse$1(version, calverFormat) {
107
- const match = version.match(getRegexForFormat(calverFormat));
108
- if (!match) {
109
- return null;
110
- }
111
- const definition = parseFormat(calverFormat);
112
- const yearToken = definition.year;
113
- let year = Number.parseInt(match[1], 10);
114
- if (yearToken === "YY" || yearToken === "0Y") {
115
- year = 2e3 + year;
116
- }
117
- let cursor = 2;
118
- let month;
119
- let day;
120
- let patch;
121
- if (definition.month) {
122
- month = Number.parseInt(match[cursor], 10);
123
- cursor += 1;
124
- }
125
- if (definition.week) {
126
- month = Number.parseInt(match[cursor], 10);
127
- cursor += 1;
128
- }
129
- if (definition.day) {
130
- day = Number.parseInt(match[cursor], 10);
131
- cursor += 1;
132
- }
133
- if (definition.counter) {
134
- patch = Number.parseInt(match[cursor], 10);
135
- cursor += 1;
136
- }
137
- const modifierGroup = match[cursor];
138
- const modifier = modifierGroup || void 0;
139
- return {
140
- year,
141
- month: month ?? 1,
142
- day,
143
- patch,
144
- modifier,
145
- format: calverFormat,
146
- raw: version
147
- };
148
- }
149
- function validate$2(version, calverFormat, preventFutureDates = true, schemeRules) {
150
- const errors = [];
151
- const parsed = parse$1(version, calverFormat);
152
- if (!parsed) {
153
- return {
154
- valid: false,
155
- errors: [
156
- {
157
- message: `Invalid CalVer format: "${version}". Expected format: ${calverFormat}`,
158
- severity: "error"
159
- }
160
- ]
161
- };
162
- }
163
- const definition = parseFormat(calverFormat);
164
- if (definition.month && (parsed.month < 1 || parsed.month > 12)) {
165
- errors.push({
166
- message: `Invalid month: ${parsed.month}. Must be between 1 and 12.`,
167
- severity: "error"
168
- });
169
- }
170
- if (definition.week && (parsed.month < 1 || parsed.month > 53)) {
171
- errors.push({
172
- message: `Invalid week: ${parsed.month}. Must be between 1 and 53.`,
173
- severity: "error"
174
- });
175
- }
176
- if (parsed.day !== void 0) {
177
- if (parsed.day < 1 || parsed.day > 31) {
178
- errors.push({
179
- message: `Invalid day: ${parsed.day}. Must be between 1 and 31.`,
180
- severity: "error"
181
- });
182
- } else if (definition.month) {
183
- const daysInMonth = new Date(parsed.year, parsed.month, 0).getDate();
184
- if (parsed.day > daysInMonth) {
185
- errors.push({
186
- message: `Invalid day: ${parsed.day}. ${parsed.year}-${String(parsed.month).padStart(2, "0")} has only ${daysInMonth} days.`,
187
- severity: "error"
188
- });
189
- }
190
- }
191
- }
192
- if (preventFutureDates) {
193
- const now = /* @__PURE__ */ new Date();
194
- const currentYear = now.getFullYear();
195
- const currentMonth = now.getMonth() + 1;
196
- const currentDay = now.getDate();
197
- if (parsed.year > currentYear) {
198
- errors.push({
199
- message: `Future year not allowed: ${parsed.year}. Current year is ${currentYear}.`,
200
- severity: "error"
201
- });
202
- } else if (definition.month && parsed.year === currentYear && parsed.month > currentMonth) {
203
- errors.push({
204
- message: `Future month not allowed: ${parsed.year}.${parsed.month}. Current month is ${currentMonth}.`,
205
- severity: "error"
206
- });
207
- } else if (definition.month && parsed.year === currentYear && parsed.month === currentMonth && parsed.day !== void 0 && parsed.day > currentDay) {
208
- errors.push({
209
- message: `Future day not allowed: ${parsed.year}.${parsed.month}.${parsed.day}. Current day is ${currentDay}.`,
210
- severity: "error"
211
- });
212
- }
213
- }
214
- if (parsed.modifier) {
215
- const modifierError = validateModifier(parsed.modifier, schemeRules);
216
- if (modifierError) {
217
- errors.push(modifierError);
218
- }
219
- }
220
- if (schemeRules?.maxNumericSegments) {
221
- const segmentCount = calverFormat.split(".").length;
222
- if (segmentCount > schemeRules.maxNumericSegments) {
223
- errors.push({
224
- message: `Format has ${segmentCount} segments, convention recommends ${schemeRules.maxNumericSegments} or fewer`,
225
- severity: "warning"
226
- });
227
- }
228
- }
229
- return {
230
- valid: errors.filter((e) => e.severity === "error").length === 0,
231
- errors,
232
- version: { type: "calver", version: parsed }
233
- };
234
- }
235
- function formatToken(token, value) {
236
- switch (token) {
237
- case "0M":
238
- case "0D":
239
- case "0W":
240
- case "0Y":
241
- return String(token === "0Y" ? value % 100 : value).padStart(2, "0");
242
- case "YY":
243
- return String(value % 100).padStart(2, "0");
244
- default:
245
- return String(value);
246
- }
247
- }
248
- function format$1(version) {
249
- const definition = parseFormat(version.format);
250
- const parts = [formatToken(definition.year, version.year)];
251
- if (definition.month) {
252
- parts.push(formatToken(definition.month, version.month));
253
- }
254
- if (definition.week) {
255
- parts.push(formatToken(definition.week, version.month));
256
- }
257
- if (definition.day) {
258
- parts.push(formatToken(definition.day, version.day ?? 1));
259
- }
260
- if (definition.counter) {
261
- parts.push(formatToken(definition.counter, version.patch ?? 0));
262
- }
263
- const base = parts.join(".");
264
- return version.modifier ? `${base}-${version.modifier}` : base;
265
- }
266
- function getCurrentVersion(calverFormat, now = /* @__PURE__ */ new Date()) {
267
- const definition = parseFormat(calverFormat);
268
- const base = {
269
- year: now.getFullYear(),
270
- month: now.getMonth() + 1,
271
- day: definition.day ? now.getDate() : void 0,
272
- patch: definition.counter ? 0 : void 0,
273
- format: calverFormat
274
- };
275
- return format$1(base);
276
- }
277
- function compare$1(a, b, calverFormat) {
278
- const left = parse$1(a, calverFormat);
279
- const right = parse$1(b, calverFormat);
280
- if (!left || !right) {
281
- throw new Error(`Invalid CalVer comparison between "${a}" and "${b}"`);
282
- }
283
- for (const key of ["year", "month", "day", "patch"]) {
284
- const leftValue = left[key] ?? 0;
285
- const rightValue = right[key] ?? 0;
286
- if (leftValue !== rightValue) {
287
- return leftValue > rightValue ? 1 : -1;
288
- }
289
- }
290
- return 0;
291
- }
292
- function increment$1(version, calverFormat) {
293
- const parsed = parse$1(version, calverFormat);
294
- if (!parsed) {
295
- throw new Error(`Invalid CalVer version: ${version}`);
296
- }
297
- const definition = parseFormat(calverFormat);
298
- const next = {
299
- ...parsed
300
- };
301
- if (definition.counter) {
302
- next.patch = (parsed.patch ?? 0) + 1;
303
- } else {
304
- next.patch = 0;
305
- next.format = `${calverFormat}.MICRO`;
306
- }
307
- return format$1(next);
308
- }
309
- function getNextVersions(currentVersion, calverFormat) {
310
- return [getCurrentVersion(calverFormat), increment$1(currentVersion, calverFormat)];
311
- }
312
- const calver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
313
- __proto__: null,
314
- compare: compare$1,
315
- format: format$1,
316
- getCurrentVersion,
317
- getNextVersions,
318
- getRegexForFormat,
319
- increment: increment$1,
320
- isValidCalVerFormat,
321
- parse: parse$1,
322
- parseFormat,
323
- validate: validate$2
324
- }, Symbol.toStringTag, { value: "Module" }));
325
- const CHANGELOG_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
326
- const KEEP_A_CHANGELOG_SECTIONS = [
327
- "Added",
328
- "Changed",
329
- "Deprecated",
330
- "Removed",
331
- "Fixed",
332
- "Security"
333
- ];
334
- function validateChangelog(changelogPath, version, strict = true, requireEntry = true, structure) {
335
- if (!fs.existsSync(changelogPath)) {
336
- return {
337
- valid: !requireEntry,
338
- errors: requireEntry ? [`Changelog not found: ${changelogPath}`] : [],
339
- hasEntryForVersion: false
340
- };
341
- }
342
- const errors = [];
343
- const content = fs.readFileSync(changelogPath, "utf-8");
344
- if (!content.startsWith("# Changelog")) {
345
- errors.push('Changelog must start with "# Changelog"');
346
- }
347
- if (!content.includes("## [Unreleased]")) {
348
- errors.push("Changelog must have an [Unreleased] section");
349
- }
350
- const versionHeader = `## [${version}]`;
351
- const hasEntryForVersion = content.includes(versionHeader);
352
- if (requireEntry && !hasEntryForVersion) {
353
- errors.push(`Changelog must have an entry for version ${version}`);
354
- }
355
- if (strict) {
356
- if (!content.includes("[Unreleased]:")) {
357
- errors.push("Changelog should include compare links at the bottom");
358
- }
359
- const versionHeaderMatch = content.match(
360
- new RegExp(`## \\[${escapeRegExp$1(version)}\\] - ([^\r
361
- ]+)`)
362
- );
363
- if (requireEntry && hasEntryForVersion) {
364
- if (!versionHeaderMatch) {
365
- errors.push(`Version ${version} entry must use "## [${version}] - YYYY-MM-DD" format`);
366
- } else if (!CHANGELOG_DATE_REGEX.test(versionHeaderMatch[1])) {
367
- errors.push(`Version ${version} entry date must use YYYY-MM-DD format`);
368
- }
369
- }
370
- }
371
- if (structure?.enforceStructure) {
372
- const allowed = structure.sections ?? KEEP_A_CHANGELOG_SECTIONS;
373
- const sectionErrors = validateSections(content, allowed);
374
- errors.push(...sectionErrors);
375
- }
376
- return {
377
- valid: errors.length === 0,
378
- errors,
379
- hasEntryForVersion
380
- };
381
- }
382
- function validateSections(content, allowed) {
383
- const errors = [];
384
- const lines = content.split("\n");
385
- for (let i = 0; i < lines.length; i++) {
386
- const sectionMatch = lines[i].match(/^### (.+)/);
387
- if (!sectionMatch) continue;
388
- const sectionName = sectionMatch[1].trim();
389
- if (!allowed.includes(sectionName)) {
390
- errors.push(
391
- `Invalid changelog section "### ${sectionName}" (line ${i + 1}). Allowed: ${allowed.join(", ")}`
392
- );
393
- }
394
- const nextContentLine = lines.slice(i + 1).find((l) => l.trim().length > 0);
395
- if (!nextContentLine || nextContentLine.startsWith("#")) {
396
- errors.push(`Empty changelog section "### ${sectionName}" (line ${i + 1})`);
397
- }
398
- }
399
- return errors;
400
- }
401
- function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
402
- if (!fs.existsSync(changelogPath)) {
403
- throw new Error(`Changelog not found: ${changelogPath}`);
404
- }
405
- const content = fs.readFileSync(changelogPath, "utf-8");
406
- if (content.includes(`## [${version}]`)) {
407
- return;
408
- }
409
- const block = `## [${version}] - ${date}
410
-
411
- ### Added
412
-
413
- - Describe changes here.
414
-
415
- `;
416
- const unreleasedMatch = content.match(/## \[Unreleased\]\r?\n(?:\r?\n)?/);
417
- if (!unreleasedMatch || unreleasedMatch.index === void 0) {
418
- throw new Error("Changelog must have an [Unreleased] section");
419
- }
420
- const insertIndex = unreleasedMatch.index + unreleasedMatch[0].length;
421
- const updated = `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
422
- fs.writeFileSync(changelogPath, updated, "utf-8");
423
- }
424
- function isChangesetMangled(changelogPath) {
425
- if (!fs.existsSync(changelogPath)) return false;
426
- const content = fs.readFileSync(changelogPath, "utf-8");
427
- return /^## \d+\.\d+/m.test(content) && content.includes("## [Unreleased]");
428
- }
429
- const SECTION_MAP = {
430
- "Major Changes": "Changed",
431
- "Minor Changes": "Added",
432
- "Patch Changes": "Fixed"
433
- };
434
- function fixChangesetMangling(changelogPath, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
435
- if (!fs.existsSync(changelogPath)) return false;
436
- const content = fs.readFileSync(changelogPath, "utf-8");
437
- const versionMatch = content.match(/^## (\d+\.\d+\.\d+[^\n]*)\n/m);
438
- if (!versionMatch || versionMatch.index === void 0) return false;
439
- const fullHeader = versionMatch[0];
440
- if (fullHeader.includes("[")) return false;
441
- const version = versionMatch[1].trim();
442
- if (content.includes(`## [${version}]`)) return false;
443
- const startIndex = versionMatch.index;
444
- const preambleMatch = content.indexOf("All notable changes", startIndex);
445
- const unreleasedMatch = content.indexOf("## [Unreleased]", startIndex);
446
- let endIndex;
447
- if (preambleMatch !== -1 && preambleMatch < unreleasedMatch) {
448
- endIndex = preambleMatch;
449
- } else if (unreleasedMatch !== -1) {
450
- endIndex = unreleasedMatch;
451
- } else {
452
- return false;
453
- }
454
- const changesetsBlock = content.slice(startIndex + fullHeader.length, endIndex).trim();
455
- const transformedSections = transformChangesetsContent(changesetsBlock);
456
- const newEntry = `## [${version}] - ${date}
457
-
458
- ${transformedSections}
459
-
460
- `;
461
- const beforeChangesets = content.slice(0, startIndex);
462
- const afterChangesets = content.slice(endIndex);
463
- const unreleasedInAfter = afterChangesets.indexOf("## [Unreleased]");
464
- if (unreleasedInAfter === -1) {
465
- const rebuilt2 = `${beforeChangesets}${newEntry}${afterChangesets}`;
466
- fs.writeFileSync(changelogPath, rebuilt2, "utf-8");
467
- return true;
468
- }
469
- const unreleasedLineEnd = afterChangesets.indexOf("\n", unreleasedInAfter);
470
- const afterUnreleased = unreleasedLineEnd !== -1 ? afterChangesets.slice(0, unreleasedLineEnd + 1) : afterChangesets;
471
- const rest = unreleasedLineEnd !== -1 ? afterChangesets.slice(unreleasedLineEnd + 1) : "";
472
- const rebuilt = `${beforeChangesets}${afterUnreleased}
473
- ${newEntry}${rest}`;
474
- const withLinks = updateCompareLinks(rebuilt, version);
475
- fs.writeFileSync(changelogPath, withLinks, "utf-8");
476
- return true;
477
- }
478
- function transformChangesetsContent(block) {
479
- const lines = block.split("\n");
480
- const result = [];
481
- for (const line of lines) {
482
- const sectionMatch = line.match(/^### (.+)/);
483
- if (sectionMatch) {
484
- const mapped = SECTION_MAP[sectionMatch[1]] ?? sectionMatch[1];
485
- result.push(`### ${mapped}`);
486
- continue;
487
- }
488
- const entryMatch = line.match(
489
- /^(\s*-\s+)[a-f0-9]{7,}: (?:feat|fix|chore|docs|refactor|perf|test|ci|build|style)(?:\([^)]*\))?: (.+)/
490
- );
491
- if (entryMatch) {
492
- result.push(`${entryMatch[1]}${entryMatch[2]}`);
493
- continue;
494
- }
495
- const simpleHashMatch = line.match(/^(\s*-\s+)[a-f0-9]{7,}: (.+)/);
496
- if (simpleHashMatch) {
497
- result.push(`${simpleHashMatch[1]}${simpleHashMatch[2]}`);
498
- continue;
499
- }
500
- result.push(line);
501
- }
502
- return result.join("\n");
503
- }
504
- function updateCompareLinks(content, version) {
505
- const unreleasedLinkRegex = /\[Unreleased\]: (https:\/\/[^\s]+\/compare\/v)([\d.]+)(\.\.\.HEAD)/;
506
- const match = content.match(unreleasedLinkRegex);
507
- if (match) {
508
- const baseUrl = match[1].replace(/v$/, "");
509
- const previousVersion = match[2];
510
- const newUnreleasedLink = `[Unreleased]: ${baseUrl}v${version}...HEAD`;
511
- const newVersionLink = `[${version}]: ${baseUrl}v${previousVersion}...v${version}`;
512
- let updated = content.replace(unreleasedLinkRegex, newUnreleasedLink);
513
- if (!updated.includes(`[${version}]:`)) {
514
- updated = updated.replace(newUnreleasedLink, `${newUnreleasedLink}
515
- ${newVersionLink}`);
516
- }
517
- return updated;
518
- }
519
- return content;
520
- }
521
- function escapeRegExp$1(value) {
522
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
523
- }
524
- const HOOK_NAMES$1 = ["pre-commit", "pre-push", "post-tag"];
525
- const VG_BLOCK_START = "# >>> versionguard >>>";
526
- const VG_BLOCK_END = "# <<< versionguard <<<";
527
- function installHooks(config, cwd = process.cwd()) {
528
- const gitDir = findGitDir(cwd);
529
- if (!gitDir) {
530
- throw new Error("Not a git repository. Run `git init` first.");
531
- }
532
- const hooksDir = path.join(gitDir, "hooks");
533
- fs.mkdirSync(hooksDir, { recursive: true });
534
- for (const hookName of HOOK_NAMES$1) {
535
- if (config.hooks[hookName]) {
536
- const hookPath = path.join(hooksDir, hookName);
537
- const vgBlock = generateHookBlock(hookName);
538
- if (fs.existsSync(hookPath)) {
539
- const existing = fs.readFileSync(hookPath, "utf-8");
540
- if (existing.includes(VG_BLOCK_START)) {
541
- const updated = replaceVgBlock(existing, vgBlock);
542
- fs.writeFileSync(hookPath, updated, { encoding: "utf-8", mode: 493 });
543
- } else if (isLegacyVgHook(existing)) {
544
- fs.writeFileSync(hookPath, `#!/bin/sh
545
-
546
- ${vgBlock}
547
- `, {
548
- encoding: "utf-8",
549
- mode: 493
550
- });
551
- } else {
552
- const appended = `${existing.trimEnd()}
553
-
554
- ${vgBlock}
555
- `;
556
- fs.writeFileSync(hookPath, appended, { encoding: "utf-8", mode: 493 });
557
- }
558
- } else {
559
- fs.writeFileSync(hookPath, `#!/bin/sh
560
-
561
- ${vgBlock}
562
- `, {
563
- encoding: "utf-8",
564
- mode: 493
565
- });
566
- }
567
- }
568
- }
569
- }
570
- function uninstallHooks(cwd = process.cwd()) {
571
- const gitDir = findGitDir(cwd);
572
- if (!gitDir) {
573
- return;
574
- }
575
- const hooksDir = path.join(gitDir, "hooks");
576
- for (const hookName of HOOK_NAMES$1) {
577
- const hookPath = path.join(hooksDir, hookName);
578
- if (!fs.existsSync(hookPath)) continue;
579
- const content = fs.readFileSync(hookPath, "utf-8");
580
- if (!content.includes("versionguard")) continue;
581
- if (content.includes(VG_BLOCK_START)) {
582
- const cleaned = removeVgBlock(content);
583
- const trimmed = cleaned.trim();
584
- if (!trimmed || trimmed === "#!/bin/sh") {
585
- fs.unlinkSync(hookPath);
586
- } else if (isLegacyVgHook(trimmed)) {
587
- fs.unlinkSync(hookPath);
588
- } else {
589
- fs.writeFileSync(hookPath, `${trimmed}
590
- `, { encoding: "utf-8", mode: 493 });
591
- }
592
- } else if (isLegacyVgHook(content)) {
593
- fs.unlinkSync(hookPath);
594
- }
595
- }
596
- }
597
- function findGitDir(cwd) {
598
- let current = cwd;
599
- while (true) {
600
- const gitPath = path.join(current, ".git");
601
- if (fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory()) {
602
- return gitPath;
603
- }
604
- const parent = path.dirname(current);
605
- if (parent === current) {
606
- return null;
607
- }
608
- current = parent;
609
- }
610
- }
611
- function areHooksInstalled(cwd = process.cwd()) {
612
- const gitDir = findGitDir(cwd);
613
- if (!gitDir) {
614
- return false;
615
- }
616
- return HOOK_NAMES$1.every((hookName) => {
617
- const hookPath = path.join(gitDir, "hooks", hookName);
618
- return fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard");
619
- });
620
- }
621
- function generateHookBlock(hookName) {
622
- return `${VG_BLOCK_START}
623
- # VersionGuard ${hookName} hook
624
- # --no-install prevents accidentally downloading an unscoped package
625
- # if @codluv/versionguard is not installed locally
626
- npx --no-install versionguard validate --hook=${hookName}
627
- status=$?
628
- if [ $status -ne 0 ]; then
629
- echo "VersionGuard validation failed."
630
- exit $status
631
- fi
632
- ${VG_BLOCK_END}`;
633
- }
634
- function generateHookScript(hookName) {
635
- return `#!/bin/sh
636
-
637
- ${generateHookBlock(hookName)}
638
- `;
639
- }
640
- function isLegacyVgHook(content) {
641
- if (!content.includes("versionguard validate")) return false;
642
- if (content.includes(VG_BLOCK_START)) return false;
643
- if (content.includes("husky")) return false;
644
- if (content.includes("lefthook")) return false;
645
- if (content.includes("pre-commit run")) return false;
646
- return true;
647
- }
648
- function replaceVgBlock(content, newBlock) {
649
- const startIdx = content.indexOf(VG_BLOCK_START);
650
- const endIdx = content.indexOf(VG_BLOCK_END);
651
- if (startIdx === -1 || endIdx === -1) return content;
652
- return content.slice(0, startIdx) + newBlock + content.slice(endIdx + VG_BLOCK_END.length);
653
- }
654
- function removeVgBlock(content) {
655
- const startIdx = content.indexOf(VG_BLOCK_START);
656
- const endIdx = content.indexOf(VG_BLOCK_END);
657
- if (startIdx === -1 || endIdx === -1) return content;
658
- const before = content.slice(0, startIdx).replace(/\n\n$/, "\n");
659
- const after = content.slice(endIdx + VG_BLOCK_END.length).replace(/^\n\n/, "\n");
660
- return before + after;
661
- }
662
- class GitTagSource {
663
- /** Human-readable provider name. */
664
- name = "git-tag";
665
- /** Empty string since git-tag has no manifest file. */
666
- manifestFile = "";
667
- /**
668
- * Returns `true` when `cwd` is inside a Git repository.
669
- *
670
- * @param cwd - Project directory to check.
671
- * @returns Whether a Git repository is found.
672
- */
673
- exists(cwd) {
674
- try {
675
- execFileSync("git", ["rev-parse", "--git-dir"], {
676
- cwd,
677
- stdio: ["pipe", "pipe", "ignore"]
678
- });
679
- return true;
680
- } catch {
681
- return false;
682
- }
683
- }
684
- /**
685
- * Reads the version string from the latest Git tag.
686
- *
687
- * @param cwd - Project directory containing the Git repository.
688
- * @returns The version string extracted from the latest version tag.
689
- */
690
- getVersion(cwd) {
691
- try {
692
- const tag = this.describeVersionTag(cwd);
693
- return tag.replace(/^v/, "");
694
- } catch {
695
- throw new Error("No version tags found. Create a tag first (e.g., git tag v0.1.0)");
696
- }
697
- }
698
- /**
699
- * Creates a new annotated Git tag for the given version.
700
- *
701
- * @param version - Version string to tag.
702
- * @param cwd - Project directory containing the Git repository.
703
- */
704
- setVersion(version, cwd) {
705
- const prefix = this.detectPrefix(cwd);
706
- const tagName = `${prefix}${version}`;
707
- execFileSync("git", ["tag", "-a", tagName, "-m", `Release ${version}`], {
708
- cwd,
709
- stdio: ["pipe", "pipe", "ignore"]
710
- });
711
- }
712
- /** Try version-like tag patterns, fall back to any tag. */
713
- describeVersionTag(cwd) {
714
- try {
715
- return execFileSync("git", ["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"], {
716
- cwd,
717
- encoding: "utf-8",
718
- stdio: ["pipe", "pipe", "ignore"]
719
- }).trim();
720
- } catch {
721
- }
722
- try {
723
- return execFileSync("git", ["describe", "--tags", "--abbrev=0", "--match", "[0-9]*"], {
724
- cwd,
725
- encoding: "utf-8",
726
- stdio: ["pipe", "pipe", "ignore"]
727
- }).trim();
728
- } catch {
729
- throw new Error("No version tags found");
730
- }
731
- }
732
- /** Detect whether existing tags use a `v` prefix or not. */
733
- detectPrefix(cwd) {
734
- try {
735
- const tag = this.describeVersionTag(cwd);
736
- return tag.startsWith("v") ? "v" : "";
737
- } catch {
738
- return "v";
739
- }
740
- }
741
- }
742
- function getNestedValue(obj, dotPath) {
743
- let current = obj;
744
- for (const key of dotPath.split(".")) {
745
- if (current === null || typeof current !== "object") {
746
- return void 0;
747
- }
748
- current = current[key];
749
- }
750
- return current;
751
- }
752
- function setNestedValue(obj, dotPath, value) {
753
- const keys = dotPath.split(".");
754
- let current = obj;
755
- for (let i = 0; i < keys.length - 1; i++) {
756
- const next = current[keys[i]];
757
- if (typeof next !== "object" || next === null) {
758
- throw new Error(`Missing intermediate key '${keys.slice(0, i + 1).join(".")}' in manifest`);
759
- }
760
- current = next;
761
- }
762
- current[keys[keys.length - 1]] = value;
763
- }
764
- function escapeRegExp(value) {
765
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
766
- }
767
- class JsonVersionSource {
768
- /** Human-readable provider name. */
769
- name;
770
- /** Filename of the JSON manifest (e.g. `'package.json'`). */
771
- manifestFile;
772
- /** Dotted key path to the version field within the JSON document. */
773
- versionPath;
774
- /**
775
- * Creates a new JSON version source.
776
- *
777
- * @param manifestFile - JSON manifest filename.
778
- * @param versionPath - Dotted key path to the version field.
779
- */
780
- constructor(manifestFile = "package.json", versionPath = "version") {
781
- this.name = manifestFile;
782
- this.manifestFile = manifestFile;
783
- this.versionPath = versionPath;
784
- }
785
- /**
786
- * Returns `true` when the manifest file exists in `cwd`.
787
- *
788
- * @param cwd - Project directory to check.
789
- * @returns Whether the manifest file exists.
790
- */
791
- exists(cwd) {
792
- return fs.existsSync(path.join(cwd, this.manifestFile));
793
- }
794
- /**
795
- * Reads the version string from the JSON manifest.
796
- *
797
- * @param cwd - Project directory containing the manifest.
798
- * @returns The version string extracted from the manifest.
799
- */
800
- getVersion(cwd) {
801
- const filePath = path.join(cwd, this.manifestFile);
802
- if (!fs.existsSync(filePath)) {
803
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
804
- }
805
- const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
806
- const version = getNestedValue(content, this.versionPath);
807
- if (typeof version !== "string" || version.length === 0) {
808
- throw new Error(`No version field in ${this.manifestFile}`);
809
- }
810
- return version;
811
- }
812
- /**
813
- * Writes a version string to the JSON manifest, preserving indentation.
814
- *
815
- * @param version - Version string to write.
816
- * @param cwd - Project directory containing the manifest.
817
- */
818
- setVersion(version, cwd) {
819
- const filePath = path.join(cwd, this.manifestFile);
820
- if (!fs.existsSync(filePath)) {
821
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
822
- }
823
- const raw = fs.readFileSync(filePath, "utf-8");
824
- const indentMatch = raw.match(/^(\s+)"/m);
825
- const indent = indentMatch?.[1]?.length ?? 2;
826
- const content = JSON.parse(raw);
827
- setNestedValue(content, this.versionPath, version);
828
- fs.writeFileSync(filePath, `${JSON.stringify(content, null, indent)}
829
- `, "utf-8");
830
- }
831
- }
832
- class RegexVersionSource {
833
- /** Human-readable provider name. */
834
- name;
835
- /** Filename of the source manifest (e.g. `'setup.py'`). */
836
- manifestFile;
837
- /** Compiled regex used to locate the version string. */
838
- versionRegex;
839
- /**
840
- * Creates a new regex version source.
841
- *
842
- * @param manifestFile - Source manifest filename.
843
- * @param versionRegex - Regex string with at least one capture group for the version.
844
- */
845
- constructor(manifestFile, versionRegex) {
846
- this.name = manifestFile;
847
- this.manifestFile = manifestFile;
848
- try {
849
- this.versionRegex = new RegExp(versionRegex, "m");
850
- } catch (err) {
851
- throw new Error(`Invalid version regex for ${manifestFile}: ${err.message}`);
852
- }
853
- if (!/\((?!\?)/.test(versionRegex)) {
854
- throw new Error(`Version regex for ${manifestFile} must contain at least one capture group`);
855
- }
856
- }
857
- /**
858
- * Returns `true` when the manifest file exists in `cwd`.
859
- *
860
- * @param cwd - Project directory to check.
861
- * @returns Whether the manifest file exists.
862
- */
863
- exists(cwd) {
864
- return fs.existsSync(path.join(cwd, this.manifestFile));
865
- }
866
- /**
867
- * Reads the version string from the source manifest using regex extraction.
868
- *
869
- * @param cwd - Project directory containing the manifest.
870
- * @returns The version string captured by group 1 of the regex.
871
- */
872
- getVersion(cwd) {
873
- const filePath = path.join(cwd, this.manifestFile);
874
- if (!fs.existsSync(filePath)) {
875
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
876
- }
877
- const content = fs.readFileSync(filePath, "utf-8");
878
- const match = content.match(this.versionRegex);
879
- if (!match?.[1]) {
880
- throw new Error(`No version match found in ${this.manifestFile}`);
881
- }
882
- return match[1];
883
- }
884
- /**
885
- * Writes a version string to the source manifest using position-based replacement.
886
- *
887
- * @param version - Version string to write.
888
- * @param cwd - Project directory containing the manifest.
889
- */
890
- setVersion(version, cwd) {
891
- const filePath = path.join(cwd, this.manifestFile);
892
- if (!fs.existsSync(filePath)) {
893
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
894
- }
895
- const content = fs.readFileSync(filePath, "utf-8");
896
- const match = this.versionRegex.exec(content);
897
- if (!match || match.index === void 0) {
898
- throw new Error(`No version match found in ${this.manifestFile}`);
899
- }
900
- const captureStart = match.index + match[0].indexOf(match[1]);
901
- const captureEnd = captureStart + match[1].length;
902
- const updated = content.slice(0, captureStart) + version + content.slice(captureEnd);
903
- fs.writeFileSync(filePath, updated, "utf-8");
904
- }
905
- }
906
- class TomlVersionSource {
907
- /** Human-readable provider name. */
908
- name;
909
- /** Filename of the TOML manifest (e.g. `'Cargo.toml'`). */
910
- manifestFile;
911
- /** Dotted key path to the version field within the TOML document. */
912
- versionPath;
913
- /**
914
- * Creates a new TOML version source.
915
- *
916
- * @param manifestFile - TOML manifest filename.
917
- * @param versionPath - Dotted key path to the version field.
918
- */
919
- constructor(manifestFile = "Cargo.toml", versionPath = "package.version") {
920
- this.name = manifestFile;
921
- this.manifestFile = manifestFile;
922
- this.versionPath = versionPath;
923
- }
924
- /**
925
- * Returns `true` when the manifest file exists in `cwd`.
926
- *
927
- * @param cwd - Project directory to check.
928
- * @returns Whether the manifest file exists.
929
- */
930
- exists(cwd) {
931
- return fs.existsSync(path.join(cwd, this.manifestFile));
932
- }
933
- /**
934
- * Reads the version string from the TOML manifest.
935
- *
936
- * @param cwd - Project directory containing the manifest.
937
- * @returns The version string extracted from the manifest.
938
- */
939
- getVersion(cwd) {
940
- const filePath = path.join(cwd, this.manifestFile);
941
- if (!fs.existsSync(filePath)) {
942
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
943
- }
944
- const content = fs.readFileSync(filePath, "utf-8");
945
- const parsed = parse$2(content);
946
- const version = getNestedValue(parsed, this.versionPath);
947
- if (typeof version !== "string" || version.length === 0) {
948
- throw new Error(`No version field at '${this.versionPath}' in ${this.manifestFile}`);
949
- }
950
- return version;
951
- }
952
- /**
953
- * Writes a version string to the TOML manifest, preserving formatting.
954
- *
955
- * @param version - Version string to write.
956
- * @param cwd - Project directory containing the manifest.
957
- */
958
- setVersion(version, cwd) {
959
- const filePath = path.join(cwd, this.manifestFile);
960
- if (!fs.existsSync(filePath)) {
961
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
962
- }
963
- const content = fs.readFileSync(filePath, "utf-8");
964
- const sectionKey = this.getSectionKey();
965
- const updated = replaceTomlVersion(content, sectionKey, version);
966
- if (updated === content) {
967
- throw new Error(`Could not find version field to update in ${this.manifestFile}`);
968
- }
969
- fs.writeFileSync(filePath, updated, "utf-8");
970
- }
971
- /**
972
- * Splits the dotted version path into a TOML section name and key name.
973
- *
974
- * @returns An object with `section` and `key` components.
975
- */
976
- getSectionKey() {
977
- const parts = this.versionPath.split(".");
978
- if (parts.length === 1) {
979
- return { section: "", key: parts[0] };
980
- }
981
- return {
982
- section: parts.slice(0, -1).join("."),
983
- key: parts[parts.length - 1]
984
- };
985
- }
986
- }
987
- function replaceTomlVersion(content, target, newVersion) {
988
- const result = replaceInSection(content, target, newVersion);
989
- if (result !== content) return result;
990
- if (target.section) {
991
- const dottedRegex = new RegExp(
992
- `^(\\s*${escapeRegExp(target.section)}\\.${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`,
993
- "m"
994
- );
995
- const dottedResult = content.replace(dottedRegex, `$1$2${newVersion}$4`);
996
- if (dottedResult !== content) return dottedResult;
997
- }
998
- if (target.section) {
999
- const inlineRegex = new RegExp(
1000
- `^(\\s*${escapeRegExp(target.section)}\\s*=\\s*\\{[^}]*${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`,
1001
- "m"
1002
- );
1003
- const inlineResult = content.replace(inlineRegex, `$1$2${newVersion}$4`);
1004
- if (inlineResult !== content) return inlineResult;
1005
- }
1006
- return content;
1007
- }
1008
- function replaceInSection(content, target, newVersion) {
1009
- const lines = content.split("\n");
1010
- const sectionHeader = target.section ? `[${target.section}]` : null;
1011
- let inSection = sectionHeader === null;
1012
- const versionRegex = new RegExp(`^(\\s*${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`);
1013
- for (let i = 0; i < lines.length; i++) {
1014
- const trimmed = lines[i].trim();
1015
- if (sectionHeader !== null) {
1016
- if (trimmed === sectionHeader) {
1017
- inSection = true;
1018
- continue;
1019
- }
1020
- if (inSection && trimmed.startsWith("[") && trimmed !== sectionHeader) {
1021
- inSection = false;
1022
- continue;
1023
- }
1024
- }
1025
- if (inSection) {
1026
- const match = lines[i].match(versionRegex);
1027
- if (match) {
1028
- lines[i] = lines[i].replace(versionRegex, `$1$2${newVersion}$4`);
1029
- return lines.join("\n");
1030
- }
1031
- }
1032
- }
1033
- return content;
1034
- }
1035
- class VersionFileSource {
1036
- /** Human-readable provider name. */
1037
- name;
1038
- /** Filename of the version file (e.g. `'VERSION'`). */
1039
- manifestFile;
1040
- /**
1041
- * Creates a new plain text version file source.
1042
- *
1043
- * @param manifestFile - Version filename.
1044
- */
1045
- constructor(manifestFile = "VERSION") {
1046
- this.name = manifestFile;
1047
- this.manifestFile = manifestFile;
1048
- }
1049
- /**
1050
- * Returns `true` when the version file exists in `cwd`.
1051
- *
1052
- * @param cwd - Project directory to check.
1053
- * @returns Whether the version file exists.
1054
- */
1055
- exists(cwd) {
1056
- return fs.existsSync(path.join(cwd, this.manifestFile));
1057
- }
1058
- /**
1059
- * Reads the version string from the plain text version file.
1060
- *
1061
- * @param cwd - Project directory containing the version file.
1062
- * @returns The version string from the first line of the file.
1063
- */
1064
- getVersion(cwd) {
1065
- const filePath = path.join(cwd, this.manifestFile);
1066
- if (!fs.existsSync(filePath)) {
1067
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
1068
- }
1069
- const raw = fs.readFileSync(filePath, "utf-8");
1070
- if (raw.includes("\0")) {
1071
- throw new Error(`${this.manifestFile} appears to be a binary file`);
1072
- }
1073
- const version = raw.split("\n")[0].trim();
1074
- if (version.length === 0) {
1075
- throw new Error(`${this.manifestFile} is empty`);
1076
- }
1077
- return version;
1078
- }
1079
- /**
1080
- * Writes a version string to the plain text version file.
1081
- *
1082
- * @param version - Version string to write.
1083
- * @param cwd - Project directory containing the version file.
1084
- */
1085
- setVersion(version, cwd) {
1086
- const filePath = path.join(cwd, this.manifestFile);
1087
- if (!fs.existsSync(filePath)) {
1088
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
1089
- }
1090
- fs.writeFileSync(filePath, `${version}
1091
- `, "utf-8");
1092
- }
1093
- }
1094
- class YamlVersionSource {
1095
- /** Human-readable provider name. */
1096
- name;
1097
- /** Filename of the YAML manifest (e.g. `'pubspec.yaml'`). */
1098
- manifestFile;
1099
- /** Dotted key path to the version field within the YAML document. */
1100
- versionKey;
1101
- /**
1102
- * Creates a new YAML version source.
1103
- *
1104
- * @param manifestFile - YAML manifest filename.
1105
- * @param versionKey - Dotted key path to the version field.
1106
- */
1107
- constructor(manifestFile = "pubspec.yaml", versionKey = "version") {
1108
- this.name = manifestFile;
1109
- this.manifestFile = manifestFile;
1110
- this.versionKey = versionKey;
1111
- }
1112
- /**
1113
- * Returns `true` when the manifest file exists in `cwd`.
1114
- *
1115
- * @param cwd - Project directory to check.
1116
- * @returns Whether the manifest file exists.
1117
- */
1118
- exists(cwd) {
1119
- return fs.existsSync(path.join(cwd, this.manifestFile));
1120
- }
1121
- /**
1122
- * Reads the version string from the YAML manifest.
1123
- *
1124
- * @param cwd - Project directory containing the manifest.
1125
- * @returns The version string extracted from the manifest.
1126
- */
1127
- getVersion(cwd) {
1128
- const filePath = path.join(cwd, this.manifestFile);
1129
- if (!fs.existsSync(filePath)) {
1130
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
1131
- }
1132
- const content = fs.readFileSync(filePath, "utf-8");
1133
- const parsed = yaml.load(content);
1134
- if (!parsed || typeof parsed !== "object") {
1135
- throw new Error(`Failed to parse ${this.manifestFile}`);
1136
- }
1137
- const version = getNestedValue(parsed, this.versionKey);
1138
- if (typeof version !== "string" || version.length === 0) {
1139
- if (typeof version === "number") {
1140
- return String(version);
1141
- }
1142
- throw new Error(`No version field in ${this.manifestFile}`);
1143
- }
1144
- return version;
1145
- }
1146
- /**
1147
- * Writes a version string to the YAML manifest, preserving formatting.
1148
- *
1149
- * @param version - Version string to write.
1150
- * @param cwd - Project directory containing the manifest.
1151
- */
1152
- setVersion(version, cwd) {
1153
- const filePath = path.join(cwd, this.manifestFile);
1154
- if (!fs.existsSync(filePath)) {
1155
- throw new Error(`${this.manifestFile} not found in ${cwd}`);
1156
- }
1157
- const keyParts = this.versionKey.split(".");
1158
- const leafKey = keyParts[keyParts.length - 1];
1159
- const content = fs.readFileSync(filePath, "utf-8");
1160
- const regex = new RegExp(`^(\\s*${escapeRegExp(leafKey)}:\\s*)(["']?)(.+?)\\2\\s*$`, "m");
1161
- const updated = content.replace(regex, `$1$2${version}$2`);
1162
- if (updated === content) {
1163
- throw new Error(`Could not find version field to update in ${this.manifestFile}`);
1164
- }
1165
- fs.writeFileSync(filePath, updated, "utf-8");
1166
- }
1167
- }
1168
- const VALID_SOURCES = /* @__PURE__ */ new Set([
1169
- "auto",
1170
- "package.json",
1171
- "composer.json",
1172
- "Cargo.toml",
1173
- "pyproject.toml",
1174
- "pubspec.yaml",
1175
- "pom.xml",
1176
- "VERSION",
1177
- "git-tag",
1178
- "custom"
1179
- ]);
1180
- const DETECTION_TABLE = [
1181
- {
1182
- file: "package.json",
1183
- source: "package.json",
1184
- factory: () => new JsonVersionSource("package.json", "version")
1185
- },
1186
- {
1187
- file: "Cargo.toml",
1188
- source: "Cargo.toml",
1189
- factory: () => new TomlVersionSource("Cargo.toml", "package.version")
1190
- },
1191
- {
1192
- file: "pyproject.toml",
1193
- source: "pyproject.toml",
1194
- factory: () => new TomlVersionSource("pyproject.toml", "project.version")
1195
- },
1196
- {
1197
- file: "pubspec.yaml",
1198
- source: "pubspec.yaml",
1199
- factory: () => new YamlVersionSource("pubspec.yaml", "version")
1200
- },
1201
- {
1202
- file: "composer.json",
1203
- source: "composer.json",
1204
- factory: () => new JsonVersionSource("composer.json", "version")
1205
- },
1206
- {
1207
- // H-002: Use regex that skips <parent> blocks for pom.xml
1208
- file: "pom.xml",
1209
- source: "pom.xml",
1210
- factory: () => new RegexVersionSource("pom.xml", "<project[^>]*>[\\s\\S]*?<version>([^<]+)</version>")
1211
- },
1212
- { file: "VERSION", source: "VERSION", factory: () => new VersionFileSource("VERSION") }
1213
- ];
1214
- function assertPathContained(manifestFile, cwd) {
1215
- const resolved = path.resolve(cwd, manifestFile);
1216
- const root = path.resolve(cwd);
1217
- if (!resolved.startsWith(`${root}${path.sep}`) && resolved !== root) {
1218
- throw new Error(`Manifest path "${manifestFile}" resolves outside the project directory`);
1219
- }
1220
- }
1221
- function createProvider(source, config, cwd) {
1222
- if (!VALID_SOURCES.has(source)) {
1223
- throw new Error(
1224
- `Invalid manifest source "${source}". Valid sources: ${[...VALID_SOURCES].join(", ")}`
1225
- );
1226
- }
1227
- switch (source) {
1228
- case "package.json":
1229
- return new JsonVersionSource("package.json", config.path ?? "version");
1230
- case "composer.json":
1231
- return new JsonVersionSource("composer.json", config.path ?? "version");
1232
- case "Cargo.toml":
1233
- return new TomlVersionSource("Cargo.toml", config.path ?? "package.version");
1234
- case "pyproject.toml":
1235
- return new TomlVersionSource("pyproject.toml", config.path ?? "project.version");
1236
- case "pubspec.yaml":
1237
- return new YamlVersionSource("pubspec.yaml", config.path ?? "version");
1238
- case "pom.xml":
1239
- return new RegexVersionSource(
1240
- "pom.xml",
1241
- config.regex ?? "<project[^>]*>[\\s\\S]*?<version>([^<]+)</version>"
1242
- );
1243
- case "VERSION":
1244
- return new VersionFileSource(config.path ?? "VERSION");
1245
- case "git-tag":
1246
- return new GitTagSource();
1247
- case "custom": {
1248
- if (!config.regex) {
1249
- throw new Error("Custom manifest source requires a 'regex' field in manifest config");
1250
- }
1251
- if (!config.path) {
1252
- throw new Error(
1253
- "Custom manifest source requires a 'path' field (manifest filename) in manifest config"
1254
- );
1255
- }
1256
- assertPathContained(config.path, cwd);
1257
- return new RegexVersionSource(config.path, config.regex);
1258
- }
1259
- default:
1260
- throw new Error(`Unknown manifest source: ${source}`);
1261
- }
1262
- }
1263
- function resolveVersionSource(config, cwd = process.cwd()) {
1264
- if (config.source !== "auto") {
1265
- return createProvider(config.source, config, cwd);
1266
- }
1267
- for (const entry of DETECTION_TABLE) {
1268
- const provider = entry.factory();
1269
- if (provider.exists(cwd)) {
1270
- return provider;
1271
- }
1272
- }
1273
- const supported = DETECTION_TABLE.map((e) => e.file).join(", ");
1274
- throw new Error(
1275
- `No supported manifest file found in ${cwd}. Looked for: ${supported}. Set manifest.source explicitly in .versionguard.yml or create a supported manifest file.`
1276
- );
1277
- }
1278
- function detectManifests(cwd = process.cwd()) {
1279
- const detected = [];
1280
- for (const entry of DETECTION_TABLE) {
1281
- const provider = entry.factory();
1282
- if (provider.exists(cwd)) {
1283
- detected.push(entry.source);
1284
- }
1285
- }
1286
- return detected;
1287
- }
1288
- function getPackageJsonPath(cwd = process.cwd()) {
1289
- return path.join(cwd, "package.json");
1290
- }
1291
- function readPackageJson(cwd = process.cwd()) {
1292
- const packagePath = getPackageJsonPath(cwd);
1293
- if (!fs.existsSync(packagePath)) {
1294
- throw new Error(`package.json not found in ${cwd}`);
1295
- }
1296
- return JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1297
- }
1298
- function writePackageJson(pkg, cwd = process.cwd()) {
1299
- fs.writeFileSync(getPackageJsonPath(cwd), `${JSON.stringify(pkg, null, 2)}
1300
- `, "utf-8");
1301
- }
1302
- function getPackageVersion(cwd = process.cwd(), manifest) {
1303
- if (manifest) {
1304
- const provider = resolveVersionSource(manifest, cwd);
1305
- return provider.getVersion(cwd);
1306
- }
1307
- const pkg = readPackageJson(cwd);
1308
- if (typeof pkg.version !== "string" || pkg.version.length === 0) {
1309
- throw new Error("No version field in package.json");
1310
- }
1311
- return pkg.version;
1312
- }
1313
- function setPackageVersion(version, cwd = process.cwd(), manifest) {
1314
- if (manifest) {
1315
- const provider = resolveVersionSource(manifest, cwd);
1316
- provider.setVersion(version, cwd);
1317
- return;
1318
- }
1319
- const pkg = readPackageJson(cwd);
1320
- pkg.version = version;
1321
- writePackageJson(pkg, cwd);
1322
- }
1323
- function getVersionSource(manifest, cwd = process.cwd()) {
1324
- return resolveVersionSource(manifest, cwd);
1325
- }
1326
- 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-]+)*))?$/;
1327
- function parse(version) {
1328
- const match = version.match(SEMVER_REGEX);
1329
- if (!match) {
1330
- return null;
1331
- }
1332
- return {
1333
- major: Number.parseInt(match[1], 10),
1334
- minor: Number.parseInt(match[2], 10),
1335
- patch: Number.parseInt(match[3], 10),
1336
- prerelease: match[4] ? match[4].split(".") : [],
1337
- build: match[5] ? match[5].split(".") : [],
1338
- raw: version
1339
- };
1340
- }
1341
- function getStructuralErrors(version) {
1342
- const errors = [];
1343
- if (version.startsWith("v")) {
1344
- errors.push({
1345
- message: `Version should not start with 'v': ${version}`,
1346
- severity: "error"
1347
- });
1348
- return errors;
1349
- }
1350
- const mainPart = version.split(/[+-]/, 1)[0];
1351
- const segments = mainPart.split(".");
1352
- if (segments.length === 3) {
1353
- const leadingZeroSegment = segments.find((segment) => /^0\d+$/.test(segment));
1354
- if (leadingZeroSegment) {
1355
- errors.push({
1356
- message: `Invalid SemVer: numeric segment "${leadingZeroSegment}" has a leading zero`,
1357
- severity: "error"
1358
- });
1359
- return errors;
1360
- }
1361
- }
1362
- const prerelease = version.match(/-([^+]+)/)?.[1];
1363
- if (prerelease) {
1364
- const invalidPrerelease = prerelease.split(".").find((segment) => /^0\d+$/.test(segment));
1365
- if (invalidPrerelease) {
1366
- errors.push({
1367
- message: `Invalid SemVer: prerelease identifier "${invalidPrerelease}" has a leading zero`,
1368
- severity: "error"
1369
- });
1370
- return errors;
1371
- }
1372
- }
1373
- errors.push({
1374
- message: `Invalid SemVer format: "${version}". Expected MAJOR.MINOR.PATCH[-prerelease][+build].`,
1375
- severity: "error"
1376
- });
1377
- return errors;
1378
- }
1379
- function validate$1(version, semverConfig, schemeRules) {
1380
- let input = version;
1381
- if (input.startsWith("v") || input.startsWith("V")) {
1382
- if (semverConfig?.allowVPrefix) {
1383
- input = input.slice(1);
1384
- }
1385
- }
1386
- const parsed = parse(input);
1387
- if (!parsed) {
1388
- return {
1389
- valid: false,
1390
- errors: getStructuralErrors(version)
1391
- };
1392
- }
1393
- const errors = [];
1394
- if (semverConfig && !semverConfig.allowBuildMetadata && parsed.build.length > 0) {
1395
- errors.push({
1396
- message: `Build metadata is not allowed: "${parsed.build.join(".")}"`,
1397
- severity: "error"
1398
- });
1399
- }
1400
- if (semverConfig?.requirePrerelease && parsed.prerelease.length === 0) {
1401
- errors.push({
1402
- message: "A prerelease label is required (e.g., 1.2.3-alpha.1)",
1403
- severity: "error"
1404
- });
1405
- }
1406
- if (parsed.prerelease.length > 0) {
1407
- const modifierError = validateModifier(parsed.prerelease[0], schemeRules);
1408
- if (modifierError) {
1409
- errors.push(modifierError);
1410
- }
1411
- }
1412
- return {
1413
- valid: errors.filter((e) => e.severity === "error").length === 0,
1414
- errors,
1415
- version: { type: "semver", version: parsed }
1416
- };
1417
- }
1418
- function compare(a, b) {
1419
- const left = parse(a);
1420
- const right = parse(b);
1421
- if (!left || !right) {
1422
- throw new Error(`Invalid SemVer comparison between "${a}" and "${b}"`);
1423
- }
1424
- for (const key of ["major", "minor", "patch"]) {
1425
- if (left[key] !== right[key]) {
1426
- return left[key] > right[key] ? 1 : -1;
1427
- }
1428
- }
1429
- const leftHasPrerelease = left.prerelease.length > 0;
1430
- const rightHasPrerelease = right.prerelease.length > 0;
1431
- if (leftHasPrerelease && !rightHasPrerelease) {
1432
- return -1;
1433
- }
1434
- if (!leftHasPrerelease && rightHasPrerelease) {
1435
- return 1;
1436
- }
1437
- const length = Math.max(left.prerelease.length, right.prerelease.length);
1438
- for (let index = 0; index < length; index += 1) {
1439
- const leftValue = left.prerelease[index];
1440
- const rightValue = right.prerelease[index];
1441
- if (leftValue === void 0) {
1442
- return -1;
1443
- }
1444
- if (rightValue === void 0) {
1445
- return 1;
1446
- }
1447
- const leftNumeric = /^\d+$/.test(leftValue) ? Number.parseInt(leftValue, 10) : null;
1448
- const rightNumeric = /^\d+$/.test(rightValue) ? Number.parseInt(rightValue, 10) : null;
1449
- if (leftNumeric !== null && rightNumeric !== null) {
1450
- if (leftNumeric !== rightNumeric) {
1451
- return leftNumeric > rightNumeric ? 1 : -1;
1452
- }
1453
- continue;
1454
- }
1455
- if (leftNumeric !== null) {
1456
- return -1;
1457
- }
1458
- if (rightNumeric !== null) {
1459
- return 1;
1460
- }
1461
- if (leftValue !== rightValue) {
1462
- return leftValue > rightValue ? 1 : -1;
1463
- }
1464
- }
1465
- return 0;
1466
- }
1467
- function gt(a, b) {
1468
- return compare(a, b) > 0;
1469
- }
1470
- function lt(a, b) {
1471
- return compare(a, b) < 0;
1472
- }
1473
- function eq(a, b) {
1474
- return compare(a, b) === 0;
1475
- }
1476
- function increment(version, release, prerelease) {
1477
- const parsed = parse(version);
1478
- if (!parsed) {
1479
- throw new Error(`Invalid SemVer version: ${version}`);
1480
- }
1481
- if (release === "major") {
1482
- return `${parsed.major + 1}.0.0${prerelease ? `-${prerelease}` : ""}`;
1483
- }
1484
- if (release === "minor") {
1485
- return `${parsed.major}.${parsed.minor + 1}.0${prerelease ? `-${prerelease}` : ""}`;
1486
- }
1487
- return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}${prerelease ? `-${prerelease}` : ""}`;
1488
- }
1489
- function format(version) {
1490
- let output = `${version.major}.${version.minor}.${version.patch}`;
1491
- if (version.prerelease.length > 0) {
1492
- output += `-${version.prerelease.join(".")}`;
1493
- }
1494
- if (version.build.length > 0) {
1495
- output += `+${version.build.join(".")}`;
1496
- }
1497
- return output;
1498
- }
1499
- const semver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1500
- __proto__: null,
1501
- compare,
1502
- eq,
1503
- format,
1504
- gt,
1505
- increment,
1506
- lt,
1507
- parse,
1508
- validate: validate$1
1509
- }, Symbol.toStringTag, { value: "Module" }));
1510
- function resolveFiles(patterns, cwd, ignore = []) {
1511
- return [
1512
- ...new Set(patterns.flatMap((pattern) => globSync(pattern, { cwd, absolute: true, ignore })))
1513
- ].sort();
1514
- }
1515
- function getLineNumber(content, offset) {
1516
- return content.slice(0, offset).split("\n").length;
1517
- }
1518
- function extractVersion(groups) {
1519
- return groups[1] ?? groups[0] ?? "";
1520
- }
1521
- function applyTemplate(template, groups, version) {
1522
- return template.replace(/\$(\d+)|\{\{version\}\}/g, (match, groupIndex) => {
1523
- if (match === "{{version}}") {
1524
- return version;
1525
- }
1526
- return groups[Number.parseInt(groupIndex ?? "0", 10) - 1] ?? "";
1527
- });
1528
- }
1529
- function stringifyCapture(value) {
1530
- return typeof value === "string" ? value : "";
1531
- }
1532
- function syncVersion(version, config, cwd = process.cwd()) {
1533
- return resolveFiles(config.files, cwd).map(
1534
- (filePath) => syncFile(filePath, version, config.patterns)
1535
- );
1536
- }
1537
- function syncFile(filePath, version, patterns) {
1538
- const original = fs.readFileSync(filePath, "utf-8");
1539
- let updatedContent = original;
1540
- const changes = [];
1541
- for (const pattern of patterns) {
1542
- const regex = new RegExp(pattern.regex, "gm");
1543
- updatedContent = updatedContent.replace(regex, (match, ...args) => {
1544
- const hasNamedGroups = typeof args.at(-1) === "object" && args.at(-1) !== null;
1545
- const offsetIndex = hasNamedGroups ? -3 : -2;
1546
- const offset = args.at(offsetIndex);
1547
- const groups = args.slice(0, offsetIndex).map((value) => stringifyCapture(value));
1548
- const found = extractVersion(groups);
1549
- if (found === "Unreleased") {
1550
- return match;
1551
- }
1552
- if (found !== version) {
1553
- changes.push({
1554
- line: getLineNumber(updatedContent, offset),
1555
- oldValue: found,
1556
- newValue: version
1557
- });
1558
- }
1559
- return applyTemplate(pattern.template, groups, version) || match;
1560
- });
1561
- }
1562
- const result = {
1563
- file: filePath,
1564
- updated: updatedContent !== original,
1565
- changes
1566
- };
1567
- if (result.updated) {
1568
- fs.writeFileSync(filePath, updatedContent, "utf-8");
1569
- }
1570
- return result;
1571
- }
1572
- function checkHardcodedVersions(expectedVersion, config, ignorePatterns, cwd = process.cwd()) {
1573
- const mismatches = [];
1574
- const files = resolveFiles(config.files, cwd, ignorePatterns);
1575
- for (const filePath of files) {
1576
- const content = fs.readFileSync(filePath, "utf-8");
1577
- for (const pattern of config.patterns) {
1578
- const regex = new RegExp(pattern.regex, "gm");
1579
- let match = regex.exec(content);
1580
- while (match) {
1581
- const found = extractVersion(match.slice(1));
1582
- if (found !== "Unreleased" && found !== expectedVersion) {
1583
- mismatches.push({
1584
- file: path.relative(cwd, filePath),
1585
- line: getLineNumber(content, match.index),
1586
- found
1587
- });
1588
- }
1589
- match = regex.exec(content);
1590
- }
1591
- }
1592
- }
1593
- return mismatches;
1594
- }
1595
- const DEFAULT_SEMVER_CONFIG = {
1596
- allowVPrefix: false,
1597
- allowBuildMetadata: true,
1598
- requirePrerelease: false
1599
- };
1600
- function getSemVerConfig(config) {
1601
- return { ...DEFAULT_SEMVER_CONFIG, ...config.versioning.semver };
1602
- }
1603
- function getCalVerConfig(config) {
1604
- if (!config.versioning.calver) {
1605
- throw new Error('CalVer configuration is required when versioning.type is "calver"');
1606
- }
1607
- return config.versioning.calver;
1608
- }
1609
- function deriveTopicSlug(conceptName) {
1610
- return conceptName.replace(/Config$/, "").replace(/Result$/, "").replace(/Options$/, "").toLowerCase();
1611
- }
1612
- function isTopicConcept(name) {
1613
- return name.endsWith("Config") && name !== "VersionGuardConfig";
1614
- }
1615
- function operationMatchesTopic(op, topicSlug, conceptNames) {
1616
- const haystack = `${op.name} ${op.what}`.toLowerCase();
1617
- if (haystack.includes(topicSlug)) return true;
1618
- return conceptNames.some((n) => haystack.includes(n.toLowerCase()));
1619
- }
1620
- function createCkmEngine(manifest) {
1621
- const topics = deriveTopics(manifest);
1622
- return {
1623
- topics,
1624
- getTopicIndex: (toolName = "tool") => formatTopicIndex(topics, toolName),
1625
- getTopicContent: (name) => formatTopicContent(topics, name),
1626
- getTopicJson: (name) => buildTopicJson(topics, manifest, name),
1627
- getManifest: () => manifest
1628
- };
1629
- }
1630
- function deriveTopics(manifest) {
1631
- const topics = [];
1632
- for (const concept of manifest.concepts) {
1633
- if (!isTopicConcept(concept.name)) continue;
1634
- const slug = deriveTopicSlug(concept.name);
1635
- const conceptNames = [concept.name];
1636
- const relatedConcepts = manifest.concepts.filter(
1637
- (c) => c.name !== concept.name && (c.name.toLowerCase().includes(slug) || slug.includes(deriveTopicSlug(c.name)))
1638
- );
1639
- conceptNames.push(...relatedConcepts.map((c) => c.name));
1640
- const operations = manifest.operations.filter(
1641
- (op) => operationMatchesTopic(op, slug, conceptNames)
1642
- );
1643
- const configSchema = manifest.configSchema.filter(
1644
- (c) => conceptNames.some((n) => c.key?.startsWith(n))
1645
- );
1646
- const constraints = manifest.constraints.filter(
1647
- (c) => conceptNames.some((n) => c.enforcedBy?.includes(n)) || operations.some((o) => c.enforcedBy?.includes(o.name))
1648
- );
1649
- topics.push({
1650
- name: slug,
1651
- summary: concept.what,
1652
- concepts: [concept, ...relatedConcepts],
1653
- operations,
1654
- configSchema,
1655
- constraints
1656
- });
1657
- }
1658
- return topics;
1659
- }
1660
- function formatTopicIndex(topics, toolName) {
1661
- const lines = [
1662
- `${toolName} CKM — Codebase Knowledge Manifest`,
1663
- "",
1664
- `Usage: ${toolName} ckm [topic] [--json] [--llm]`,
1665
- "",
1666
- "Topics:"
1667
- ];
1668
- const maxName = Math.max(...topics.map((t) => t.name.length));
1669
- for (const topic of topics) {
1670
- lines.push(` ${topic.name.padEnd(maxName + 2)}${topic.summary}`);
1671
- }
1672
- lines.push("");
1673
- lines.push("Flags:");
1674
- lines.push(" --json Machine-readable CKM output (concepts, operations, config schema)");
1675
- lines.push(" --llm Full API context for LLM agents (forge-ts llms.txt)");
1676
- return lines.join("\n");
1677
- }
1678
- function formatTopicContent(topics, topicName) {
1679
- const topic = topics.find((t) => t.name === topicName);
1680
- if (!topic) return null;
1681
- const lines = [`# ${topic.summary}`, ""];
1682
- if (topic.concepts.length > 0) {
1683
- lines.push("## Concepts", "");
1684
- for (const c of topic.concepts) {
1685
- lines.push(` ${c.name} — ${c.what}`);
1686
- if (c.properties) {
1687
- for (const p of c.properties) {
1688
- const def = findDefault(topic.configSchema, c.name, p.name);
1689
- lines.push(` ${p.name}: ${p.type}${def ? ` = ${def}` : ""}`);
1690
- if (p.description) {
1691
- lines.push(` ${p.description}`);
1692
- }
1693
- }
1694
- }
1695
- lines.push("");
1696
- }
1697
- }
1698
- if (topic.operations.length > 0) {
1699
- lines.push("## Operations", "");
1700
- for (const o of topic.operations) {
1701
- lines.push(` ${o.name}() — ${o.what}`);
1702
- if (o.inputs) {
1703
- for (const i of o.inputs) {
1704
- lines.push(` @param ${i.name}: ${i.description}`);
1705
- }
1706
- }
1707
- lines.push("");
1708
- }
1709
- }
1710
- if (topic.configSchema.length > 0) {
1711
- lines.push("## Config Fields", "");
1712
- for (const c of topic.configSchema) {
1713
- lines.push(` ${c.key}: ${c.type}${c.default ? ` = ${c.default}` : ""}`);
1714
- if (c.description) {
1715
- lines.push(` ${c.description}`);
1716
- }
1717
- }
1718
- lines.push("");
1719
- }
1720
- if (topic.constraints.length > 0) {
1721
- lines.push("## Constraints", "");
1722
- for (const c of topic.constraints) {
1723
- lines.push(` [${c.id}] ${c.rule}`);
1724
- lines.push(` Enforced by: ${c.enforcedBy}`);
1725
- }
1726
- lines.push("");
1727
- }
1728
- return lines.join("\n");
1729
- }
1730
- function findDefault(schema, conceptName, propName) {
1731
- return schema.find((c) => c.key === `${conceptName}.${propName}`)?.default;
1732
- }
1733
- function buildTopicJson(topics, manifest, topicName) {
1734
- if (!topicName) {
1735
- return {
1736
- topics: topics.map((t) => ({
1737
- name: t.name,
1738
- summary: t.summary,
1739
- concepts: t.concepts.length,
1740
- operations: t.operations.length,
1741
- configFields: t.configSchema.length,
1742
- constraints: t.constraints.length
1743
- })),
1744
- ckm: {
1745
- concepts: manifest.concepts.length,
1746
- operations: manifest.operations.length,
1747
- constraints: manifest.constraints.length,
1748
- workflows: manifest.workflows.length,
1749
- configSchema: manifest.configSchema.length
1750
- }
1751
- };
1752
- }
1753
- const topic = topics.find((t) => t.name === topicName);
1754
- if (!topic) {
1755
- return { error: `Unknown topic: ${topicName}`, topics: topics.map((t) => t.name) };
1756
- }
1757
- return {
1758
- topic: topic.name,
1759
- summary: topic.summary,
1760
- concepts: topic.concepts,
1761
- operations: topic.operations,
1762
- configSchema: topic.configSchema,
1763
- constraints: topic.constraints
1764
- };
1765
- }
1766
- const CONFIG_FILE_NAMES = [
1767
- ".versionguard.yml",
1768
- ".versionguard.yaml",
1769
- "versionguard.yml",
1770
- "versionguard.yaml"
1771
- ];
1772
- const DEFAULT_CONFIG = {
1773
- versioning: {
1774
- type: "semver",
1775
- schemeRules: {
1776
- maxNumericSegments: 3,
1777
- allowedModifiers: ["dev", "alpha", "beta", "rc"]
1778
- },
1779
- semver: {
1780
- allowVPrefix: false,
1781
- allowBuildMetadata: true,
1782
- requirePrerelease: false
1783
- },
1784
- calver: {
1785
- format: "YYYY.MM.PATCH",
1786
- preventFutureDates: true,
1787
- strictMutualExclusion: true
1788
- }
1789
- },
1790
- manifest: {
1791
- source: "auto"
1792
- },
1793
- sync: {
1794
- files: ["README.md", "CHANGELOG.md"],
1795
- patterns: [
1796
- {
1797
- regex: `(version\\s*[=:]\\s*["'])(.+?)(["'])`,
1798
- template: "$1{{version}}$3"
1799
- },
1800
- {
1801
- regex: "(##\\s*\\[)(.+?)(\\])",
1802
- template: "$1{{version}}$3"
1803
- }
1804
- ]
1805
- },
1806
- changelog: {
1807
- enabled: true,
1808
- file: "CHANGELOG.md",
1809
- strict: true,
1810
- requireEntry: true,
1811
- enforceStructure: false,
1812
- sections: ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
1813
- },
1814
- git: {
1815
- hooks: {
1816
- "pre-commit": true,
1817
- "pre-push": true,
1818
- "post-tag": true
1819
- },
1820
- enforceHooks: true
1821
- },
1822
- ignore: ["node_modules/**", "dist/**", ".git/**", "*.lock", "package-lock.json"]
1823
- };
1824
- const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
1825
- function getDefaultConfig() {
1826
- return structuredClone(DEFAULT_CONFIG);
1827
- }
1828
- function findConfig(cwd = process.cwd()) {
1829
- for (const fileName of CONFIG_FILE_NAMES) {
1830
- const fullPath = path.join(cwd, fileName);
1831
- if (fs.existsSync(fullPath)) {
1832
- return fullPath;
1833
- }
1834
- }
1835
- return null;
1836
- }
1837
- function loadConfig(configPath) {
1838
- const content = fs.readFileSync(configPath, "utf-8");
1839
- const parsed = yaml.load(content);
1840
- if (parsed === void 0) {
1841
- return getDefaultConfig();
1842
- }
1843
- if (!isPlainObject(parsed)) {
1844
- throw new Error(`Config file must contain a YAML object: ${configPath}`);
1845
- }
1846
- return mergeDeep(getDefaultConfig(), parsed);
1847
- }
1848
- function getConfig(cwd = process.cwd()) {
1849
- const configPath = findConfig(cwd);
1850
- return configPath ? loadConfig(configPath) : getDefaultConfig();
1851
- }
1852
- function initConfig(cwd = process.cwd()) {
1853
- const configPath = path.join(cwd, ".versionguard.yml");
1854
- const existingConfigPath = findConfig(cwd);
1855
- if (existingConfigPath) {
1856
- throw new Error(`Config file already exists: ${existingConfigPath}`);
1857
- }
1858
- const examplePath = path.join(MODULE_DIR, "..", ".versionguard.yml.example");
1859
- const content = fs.existsSync(examplePath) ? fs.readFileSync(examplePath, "utf-8") : generateDefaultConfig();
1860
- fs.writeFileSync(configPath, content, "utf-8");
1861
- return configPath;
1862
- }
1863
- function generateDefaultConfig() {
1864
- return `# VersionGuard Configuration
1865
- # Change "type" to switch between semver and calver — both blocks are always present.
1866
- versioning:
1867
- type: semver
1868
- semver:
1869
- allowVPrefix: false
1870
- allowBuildMetadata: true
1871
- requirePrerelease: false
1872
- calver:
1873
- format: "YYYY.MM.PATCH"
1874
- preventFutureDates: true
1875
-
1876
- sync:
1877
- files:
1878
- - "README.md"
1879
- - "CHANGELOG.md"
1880
- patterns:
1881
- - regex: '(version\\s*[=:]\\s*["''])(.+?)(["''])'
1882
- template: '$1{{version}}$3'
1883
- - regex: '(##\\s*\\[)(.+?)(\\])'
1884
- template: '$1{{version}}$3'
1885
-
1886
- changelog:
1887
- enabled: true
1888
- file: "CHANGELOG.md"
1889
- strict: true
1890
- requireEntry: true
1891
-
1892
- git:
1893
- hooks:
1894
- pre-commit: true
1895
- pre-push: true
1896
- post-tag: true
1897
- enforceHooks: true
1898
-
1899
- ignore:
1900
- - "node_modules/**"
1901
- - "dist/**"
1902
- - ".git/**"
1903
- `;
1904
- }
1905
- function isPlainObject(value) {
1906
- return typeof value === "object" && value !== null && !Array.isArray(value);
1907
- }
1908
- function mergeDeep(target, source) {
1909
- if (!isPlainObject(target) || !isPlainObject(source)) {
1910
- return source ?? target;
1911
- }
1912
- const output = { ...target };
1913
- for (const [key, value] of Object.entries(source)) {
1914
- const current = output[key];
1915
- output[key] = isPlainObject(current) && isPlainObject(value) ? mergeDeep(current, value) : value;
1916
- }
1917
- return output;
1918
- }
1919
- function getVersionFeedback(version, config, previousVersion) {
1920
- if (config.versioning.type === "semver") {
1921
- return getSemVerFeedback(version, previousVersion);
1922
- }
1923
- return getCalVerFeedback(version, getCalVerConfig(config), previousVersion);
1924
- }
1925
- function getSemVerFeedback(version, previousVersion) {
1926
- const errors = [];
1927
- const suggestions = [];
1928
- const parsed = parse(version);
1929
- if (!parsed) {
1930
- const validation = validate$1(version);
1931
- if (version.startsWith("v")) {
1932
- const cleanVersion = version.slice(1);
1933
- errors.push({
1934
- message: `Version should not start with 'v': ${version}`,
1935
- severity: "error"
1936
- });
1937
- suggestions.push({
1938
- message: `Remove the 'v' prefix`,
1939
- fix: `npx versionguard fix --version ${cleanVersion}`,
1940
- autoFixable: true
1941
- });
1942
- } else if (version.split(".").length === 2) {
1943
- errors.push({
1944
- message: `Version missing patch number: ${version}`,
1945
- severity: "error"
1946
- });
1947
- suggestions.push({
1948
- message: `Add patch number (e.g., ${version}.0)`,
1949
- fix: `npx versionguard fix --version ${version}.0`,
1950
- autoFixable: true
1951
- });
1952
- } else if (/^\d+\.\d+\.\d+\.\d+$/.test(version)) {
1953
- errors.push({
1954
- message: `Version has too many segments: ${version}`,
1955
- severity: "error"
1956
- });
1957
- suggestions.push({
1958
- message: `Use only 3 segments (MAJOR.MINOR.PATCH)`,
1959
- autoFixable: false
1960
- });
1961
- } else if (validation.errors.some((error) => error.message.includes("leading zero"))) {
1962
- errors.push(...validation.errors);
1963
- suggestions.push({
1964
- message: `Remove leading zeros (e.g., 1.2.3 instead of 01.02.03)`,
1965
- autoFixable: false
1966
- });
1967
- } else {
1968
- errors.push(
1969
- ...validation.errors.length > 0 ? validation.errors : [
1970
- {
1971
- message: `Invalid SemVer format: ${version}`,
1972
- severity: "error"
1973
- }
1974
- ]
1975
- );
1976
- suggestions.push({
1977
- message: `Use format: MAJOR.MINOR.PATCH (e.g., 1.0.0)`,
1978
- autoFixable: false
1979
- });
1980
- }
1981
- return {
1982
- valid: false,
1983
- errors,
1984
- suggestions,
1985
- canAutoFix: suggestions.some((s) => s.autoFixable)
1986
- };
1987
- }
1988
- if (previousVersion) {
1989
- const prevParsed = parse(previousVersion);
1990
- if (prevParsed) {
1991
- const comparison = compare(version, previousVersion);
1992
- if (comparison < 0) {
1993
- errors.push({
1994
- message: `Version ${version} is older than previous ${previousVersion}`,
1995
- severity: "error"
1996
- });
1997
- suggestions.push({
1998
- message: `Version must be greater than ${previousVersion}`,
1999
- fix: `npx versionguard fix --version ${increment(previousVersion, "patch")}`,
2000
- autoFixable: true
2001
- });
2002
- } else if (comparison === 0) {
2003
- errors.push({
2004
- message: `Version ${version} is the same as previous`,
2005
- severity: "error"
2006
- });
2007
- suggestions.push({
2008
- message: `Bump the version`,
2009
- fix: `npx versionguard fix --version ${increment(previousVersion, "patch")}`,
2010
- autoFixable: true
2011
- });
2012
- } else {
2013
- const majorJump = parsed.major - prevParsed.major;
2014
- const minorJump = parsed.minor - prevParsed.minor;
2015
- const patchJump = parsed.patch - prevParsed.patch;
2016
- if (majorJump > 1) {
2017
- suggestions.push({
2018
- message: `⚠️ Major version jumped by ${majorJump} (from ${previousVersion} to ${version})`,
2019
- autoFixable: false
2020
- });
2021
- }
2022
- if (minorJump > 10) {
2023
- suggestions.push({
2024
- message: `⚠️ Minor version jumped by ${minorJump} - did you mean to do a major bump?`,
2025
- autoFixable: false
2026
- });
2027
- }
2028
- if (patchJump > 20) {
2029
- suggestions.push({
2030
- message: `⚠️ Patch version jumped by ${patchJump} - consider a minor bump instead`,
2031
- autoFixable: false
2032
- });
2033
- }
2034
- }
2035
- }
2036
- }
2037
- return {
2038
- valid: errors.length === 0,
2039
- errors,
2040
- suggestions,
2041
- canAutoFix: suggestions.some((s) => s.autoFixable)
2042
- };
2043
- }
2044
- function getCalVerFeedback(version, calverConfig, previousVersion) {
2045
- const errors = [];
2046
- const suggestions = [];
2047
- const { format: format2, preventFutureDates } = calverConfig;
2048
- const parsed = parse$1(version, format2);
2049
- if (!parsed) {
2050
- errors.push({
2051
- message: `Invalid CalVer format: ${version}`,
2052
- severity: "error"
2053
- });
2054
- suggestions.push({
2055
- message: `Expected format: ${format2}`,
2056
- fix: `Update version to current date: "${getCurrentVersion(format2)}"`,
2057
- autoFixable: true
2058
- });
2059
- return { valid: false, errors, suggestions, canAutoFix: true };
2060
- }
2061
- const validation = validate$2(version, format2, preventFutureDates);
2062
- errors.push(...validation.errors);
2063
- const now = /* @__PURE__ */ new Date();
2064
- if (validation.errors.some((error) => error.message.startsWith("Invalid month:"))) {
2065
- suggestions.push({
2066
- message: `Month must be between 1-12`,
2067
- autoFixable: false
2068
- });
2069
- }
2070
- if (validation.errors.some((error) => error.message.startsWith("Invalid day:"))) {
2071
- suggestions.push({
2072
- message: `Day must be valid for the selected month`,
2073
- autoFixable: false
2074
- });
2075
- }
2076
- if (preventFutureDates && parsed.year > now.getFullYear()) {
2077
- suggestions.push({
2078
- message: `Use current year (${now.getFullYear()}) or a past year`,
2079
- fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, year: now.getFullYear() })}`,
2080
- autoFixable: true
2081
- });
2082
- }
2083
- if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month > now.getMonth() + 1) {
2084
- suggestions.push({
2085
- message: `Current month is ${now.getMonth() + 1}`,
2086
- fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, month: now.getMonth() + 1 })}`,
2087
- autoFixable: true
2088
- });
2089
- }
2090
- if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month === now.getMonth() + 1 && parsed.day !== void 0 && parsed.day > now.getDate()) {
2091
- suggestions.push({
2092
- message: `Current day is ${now.getDate()}`,
2093
- fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, day: now.getDate() })}`,
2094
- autoFixable: true
2095
- });
2096
- }
2097
- if (previousVersion) {
2098
- const prevParsed = parse$1(previousVersion, format2);
2099
- if (prevParsed) {
2100
- if (compare$1(version, previousVersion, format2) <= 0) {
2101
- errors.push({
2102
- message: `Version ${version} is not newer than previous ${previousVersion}`,
2103
- severity: "error"
2104
- });
2105
- suggestions.push({
2106
- message: `CalVer must increase over time`,
2107
- fix: `npx versionguard fix --version ${increment$1(previousVersion, format2)}`,
2108
- autoFixable: true
2109
- });
2110
- }
2111
- }
2112
- }
2113
- return {
2114
- valid: errors.length === 0,
2115
- errors,
2116
- suggestions,
2117
- canAutoFix: suggestions.some((s) => s.autoFixable)
2118
- };
2119
- }
2120
- function formatCalVerVersion(version) {
2121
- return format$1({
2122
- ...version,
2123
- raw: version.raw || ""
2124
- });
2125
- }
2126
- function getSyncFeedback(file, foundVersion, expectedVersion) {
2127
- const suggestions = [
2128
- {
2129
- message: `${file} has version "${foundVersion}" but should be "${expectedVersion}"`,
2130
- fix: `npx versionguard sync`,
2131
- autoFixable: true
2132
- }
2133
- ];
2134
- if (file.endsWith(".md")) {
2135
- suggestions.push({
2136
- message: `For markdown files, check headers like "## [${expectedVersion}]"`,
2137
- autoFixable: false
2138
- });
2139
- }
2140
- if (file.endsWith(".ts") || file.endsWith(".js")) {
2141
- suggestions.push({
2142
- message: `For code files, check constants like "export const VERSION = '${expectedVersion}'"`,
2143
- autoFixable: false
2144
- });
2145
- }
2146
- return suggestions;
2147
- }
2148
- function getChangelogFeedback(hasEntry, version, latestChangelogVersion) {
2149
- const suggestions = [];
2150
- if (!hasEntry) {
2151
- suggestions.push({
2152
- message: `CHANGELOG.md is missing entry for version ${version}`,
2153
- fix: `npx versionguard fix`,
2154
- autoFixable: true
2155
- });
2156
- suggestions.push({
2157
- message: `Or manually add: "## [${version}] - YYYY-MM-DD" under [Unreleased]`,
2158
- autoFixable: false
2159
- });
2160
- }
2161
- if (latestChangelogVersion && latestChangelogVersion !== version) {
2162
- suggestions.push({
2163
- message: `CHANGELOG.md latest entry is ${latestChangelogVersion}, but manifest version is ${version}`,
2164
- fix: `Make sure versions are in sync`,
2165
- autoFixable: false
2166
- });
2167
- }
2168
- return suggestions;
2169
- }
2170
- function getTagFeedback(tagVersion, packageVersion, hasUnsyncedFiles) {
2171
- const suggestions = [];
2172
- if (tagVersion !== packageVersion) {
2173
- suggestions.push({
2174
- message: `Git tag "${tagVersion}" doesn't match manifest version "${packageVersion}"`,
2175
- fix: `Delete tag and recreate: git tag -d ${tagVersion} && git tag ${packageVersion}`,
2176
- autoFixable: false
2177
- });
2178
- }
2179
- if (hasUnsyncedFiles) {
2180
- suggestions.push({
2181
- message: `Files are out of sync with version ${packageVersion}`,
2182
- fix: `npx versionguard sync`,
2183
- autoFixable: true
2184
- });
2185
- }
2186
- return suggestions;
2187
- }
2188
- function fixPackageVersion(targetVersion, cwd = process.cwd(), manifest) {
2189
- if (!manifest) {
2190
- const packagePath = path.join(cwd, "package.json");
2191
- if (!fs.existsSync(packagePath)) {
2192
- return { fixed: false, message: "package.json not found" };
2193
- }
2194
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
2195
- const oldVersion2 = typeof pkg.version === "string" ? pkg.version : void 0;
2196
- if (oldVersion2 === targetVersion) {
2197
- return { fixed: false, message: `Already at version ${targetVersion}` };
2198
- }
2199
- setPackageVersion(targetVersion, cwd);
2200
- return {
2201
- fixed: true,
2202
- message: `Updated package.json from ${oldVersion2} to ${targetVersion}`,
2203
- file: packagePath
2204
- };
2205
- }
2206
- const provider = getVersionSource(manifest, cwd);
2207
- let oldVersion;
2208
- try {
2209
- oldVersion = provider.getVersion(cwd);
2210
- } catch {
2211
- return { fixed: false, message: "Version source not found" };
2212
- }
2213
- if (oldVersion === targetVersion) {
2214
- return { fixed: false, message: `Already at version ${targetVersion}` };
2215
- }
2216
- provider.setVersion(targetVersion, cwd);
2217
- return {
2218
- fixed: true,
2219
- message: `Updated version from ${oldVersion} to ${targetVersion}`,
2220
- file: provider.manifestFile ? path.join(cwd, provider.manifestFile) : void 0
2221
- };
2222
- }
2223
- function fixSyncIssues(config, cwd = process.cwd()) {
2224
- const version = getPackageVersion(cwd, config.manifest);
2225
- const results = syncVersion(version, config.sync, cwd).filter((result) => result.updated).map((result) => ({
2226
- fixed: true,
2227
- message: `Updated ${path.relative(cwd, result.file)} (${result.changes.length} changes)`,
2228
- file: result.file
2229
- }));
2230
- if (results.length === 0) {
2231
- results.push({ fixed: false, message: "All files already in sync" });
2232
- }
2233
- return results;
2234
- }
2235
- function fixChangelog(version, config, cwd = process.cwd()) {
2236
- const changelogPath = path.join(cwd, config.changelog.file);
2237
- if (!fs.existsSync(changelogPath)) {
2238
- createInitialChangelog(changelogPath, version);
2239
- return {
2240
- fixed: true,
2241
- message: `Created ${config.changelog.file} with entry for ${version}`,
2242
- file: changelogPath
2243
- };
2244
- }
2245
- const content = fs.readFileSync(changelogPath, "utf-8");
2246
- if (content.includes(`## [${version}]`)) {
2247
- return { fixed: false, message: `Changelog already has entry for ${version}` };
2248
- }
2249
- addVersionEntry(changelogPath, version);
2250
- return {
2251
- fixed: true,
2252
- message: `Added entry for ${version} to ${config.changelog.file}`,
2253
- file: changelogPath
2254
- };
2255
- }
2256
- function createInitialChangelog(changelogPath, version) {
2257
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2258
- const content = `# Changelog
2259
-
2260
- All notable changes to this project will be documented in this file.
2261
-
2262
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2263
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2264
-
2265
- ## [Unreleased]
2266
-
2267
- ## [${version}] - ${today}
2268
-
2269
- ### Added
2270
-
2271
- - Initial release
2272
-
2273
- [Unreleased]: https://github.com/yourorg/project/compare/v${version}...HEAD
2274
- [${version}]: https://github.com/yourorg/project/releases/tag/v${version}
2275
- `;
2276
- fs.writeFileSync(changelogPath, content, "utf-8");
2277
- }
2278
- function fixAll(config, targetVersion, cwd = process.cwd()) {
2279
- const results = [];
2280
- const version = targetVersion || getPackageVersion(cwd, config.manifest);
2281
- if (targetVersion && targetVersion !== getPackageVersion(cwd, config.manifest)) {
2282
- results.push(fixPackageVersion(targetVersion, cwd, config.manifest));
2283
- }
2284
- const syncResults = fixSyncIssues(config, cwd);
2285
- results.push(...syncResults);
2286
- if (config.changelog.enabled) {
2287
- const changelogPath = path.join(cwd, config.changelog.file);
2288
- if (isChangesetMangled(changelogPath)) {
2289
- const fixed = fixChangesetMangling(changelogPath);
2290
- if (fixed) {
2291
- results.push({
2292
- fixed: true,
2293
- message: `Restructured ${config.changelog.file} from Changesets format to Keep a Changelog`,
2294
- file: changelogPath
2295
- });
2296
- }
2297
- }
2298
- const changelogResult = fixChangelog(version, config, cwd);
2299
- if (changelogResult.fixed) {
2300
- results.push(changelogResult);
2301
- }
2302
- }
2303
- return results;
2304
- }
2305
- function suggestNextVersion(currentVersion, config, changeType) {
2306
- const suggestions = [];
2307
- if (config.versioning.type === "semver") {
2308
- if (!changeType || changeType === "auto" || changeType === "patch") {
2309
- suggestions.push({
2310
- version: increment(currentVersion, "patch"),
2311
- reason: "Patch - bug fixes, small changes"
2312
- });
2313
- }
2314
- if (!changeType || changeType === "auto" || changeType === "minor") {
2315
- suggestions.push({
2316
- version: increment(currentVersion, "minor"),
2317
- reason: "Minor - new features, backwards compatible"
2318
- });
2319
- }
2320
- if (!changeType || changeType === "auto" || changeType === "major") {
2321
- suggestions.push({
2322
- version: increment(currentVersion, "major"),
2323
- reason: "Major - breaking changes"
2324
- });
2325
- }
2326
- } else {
2327
- const format2 = getCalVerConfig(config).format;
2328
- const currentCal = getCurrentVersion(format2);
2329
- suggestions.push({
2330
- version: currentCal,
2331
- reason: "Current date - new release today"
2332
- });
2333
- suggestions.push({
2334
- version: increment$1(currentVersion, format2),
2335
- reason: "Increment patch - additional release today"
2336
- });
2337
- }
2338
- return suggestions;
2339
- }
2340
- const HOOK_NAMES = ["pre-commit", "pre-push", "post-tag"];
2341
- function checkHooksPathOverride(cwd) {
2342
- try {
2343
- const hooksPath = execSync("git config core.hooksPath", {
2344
- cwd,
2345
- encoding: "utf-8"
2346
- }).trim();
2347
- if (hooksPath) {
2348
- const resolved = path.resolve(cwd, hooksPath);
2349
- const huskyDir = path.resolve(cwd, ".husky");
2350
- if (resolved === huskyDir || resolved.startsWith(`${huskyDir}${path.sep}`)) {
2351
- return {
2352
- code: "HOOKS_PATH_HUSKY",
2353
- severity: "warning",
2354
- 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.`
2355
- };
2356
- }
2357
- return {
2358
- code: "HOOKS_PATH_OVERRIDE",
2359
- severity: "error",
2360
- message: `git core.hooksPath is set to "${hooksPath}" — hooks in .git/hooks/ are bypassed`,
2361
- fix: "git config --unset core.hooksPath"
2362
- };
2363
- }
2364
- } catch {
2365
- }
2366
- return null;
2367
- }
2368
- function checkHuskyBypass() {
2369
- if (process.env.HUSKY === "0") {
2370
- return {
2371
- code: "HUSKY_BYPASS",
2372
- severity: "error",
2373
- message: "HUSKY=0 is set — git hooks are disabled via environment variable",
2374
- fix: "unset HUSKY"
2375
- };
2376
- }
2377
- return null;
2378
- }
2379
- function checkHookIntegrity(config, cwd) {
2380
- const warnings = [];
2381
- const gitDir = findGitDir(cwd);
2382
- if (!gitDir) {
2383
- return warnings;
2384
- }
2385
- const hooksDir = path.join(gitDir, "hooks");
2386
- for (const hookName of HOOK_NAMES) {
2387
- if (!config.git.hooks[hookName]) {
2388
- continue;
2389
- }
2390
- const hookPath = path.join(hooksDir, hookName);
2391
- if (!fs.existsSync(hookPath)) {
2392
- warnings.push({
2393
- code: "HOOK_MISSING",
2394
- severity: "error",
2395
- message: `Required hook "${hookName}" is not installed`,
2396
- fix: "npx versionguard hooks install"
2397
- });
2398
- continue;
2399
- }
2400
- const actual = fs.readFileSync(hookPath, "utf-8");
2401
- const expected = generateHookScript(hookName);
2402
- if (actual !== expected) {
2403
- if (!actual.includes("versionguard")) {
2404
- warnings.push({
2405
- code: "HOOK_REPLACED",
2406
- severity: "error",
2407
- message: `Hook "${hookName}" has been replaced — versionguard invocation is missing`,
2408
- fix: "npx versionguard hooks install"
2409
- });
2410
- } else {
2411
- warnings.push({
2412
- code: "HOOK_TAMPERED",
2413
- severity: "warning",
2414
- message: `Hook "${hookName}" has been modified from the expected template`,
2415
- fix: "npx versionguard hooks install"
2416
- });
2417
- }
2418
- }
2419
- }
2420
- return warnings;
2421
- }
2422
- function checkEnforceHooksPolicy(config) {
2423
- const anyHookEnabled = HOOK_NAMES.some((name) => config.git.hooks[name]);
2424
- if (anyHookEnabled && !config.git.enforceHooks) {
2425
- return {
2426
- code: "HOOKS_NOT_ENFORCED",
2427
- severity: "warning",
2428
- message: "Hooks are enabled but enforceHooks is false — missing hooks will not fail validation",
2429
- fix: "Set git.enforceHooks: true in .versionguard.yml"
2430
- };
2431
- }
2432
- return null;
2433
- }
2434
- function runGuardChecks(config, cwd) {
2435
- const warnings = [];
2436
- const hooksPathWarning = checkHooksPathOverride(cwd);
2437
- if (hooksPathWarning) {
2438
- warnings.push(hooksPathWarning);
2439
- }
2440
- const huskyWarning = checkHuskyBypass();
2441
- if (huskyWarning) {
2442
- warnings.push(huskyWarning);
2443
- }
2444
- const integrityWarnings = checkHookIntegrity(config, cwd);
2445
- warnings.push(...integrityWarnings);
2446
- const enforceWarning = checkEnforceHooksPolicy(config);
2447
- if (enforceWarning) {
2448
- warnings.push(enforceWarning);
2449
- }
2450
- const hasErrors = warnings.some((w) => w.severity === "error");
2451
- return {
2452
- safe: !hasErrors,
2453
- warnings
2454
- };
2455
- }
2456
- const PROJECT_MARKERS = [
2457
- ".versionguard.yml",
2458
- ".versionguard.yaml",
2459
- "versionguard.yml",
2460
- "versionguard.yaml",
2461
- ".git",
2462
- "package.json",
2463
- "Cargo.toml",
2464
- "pyproject.toml",
2465
- "pubspec.yaml",
2466
- "composer.json",
2467
- "pom.xml",
2468
- "go.mod",
2469
- "mix.exs",
2470
- "Gemfile",
2471
- ".csproj"
2472
- ];
2473
- function findProjectRoot(startDir) {
2474
- let current = path.resolve(startDir);
2475
- while (true) {
2476
- for (const marker of PROJECT_MARKERS) {
2477
- if (marker.startsWith(".") && marker !== ".git" && !marker.startsWith(".version")) {
2478
- try {
2479
- const files = fs.readdirSync(current);
2480
- if (files.some((f) => f.endsWith(marker))) {
2481
- return buildResult(current, marker);
2482
- }
2483
- } catch {
2484
- }
2485
- } else if (fs.existsSync(path.join(current, marker))) {
2486
- return buildResult(current, marker);
2487
- }
2488
- }
2489
- const parent = path.dirname(current);
2490
- if (parent === current) {
2491
- return {
2492
- found: false,
2493
- root: path.resolve(startDir),
2494
- hasConfig: false,
2495
- hasGit: false,
2496
- hasManifest: false
2497
- };
2498
- }
2499
- current = parent;
2500
- }
2501
- }
2502
- function buildResult(root, marker) {
2503
- const configNames = [
2504
- ".versionguard.yml",
2505
- ".versionguard.yaml",
2506
- "versionguard.yml",
2507
- "versionguard.yaml"
2508
- ];
2509
- return {
2510
- found: true,
2511
- root,
2512
- marker,
2513
- hasConfig: configNames.some((c) => fs.existsSync(path.join(root, c))),
2514
- hasGit: fs.existsSync(path.join(root, ".git")),
2515
- hasManifest: [
2516
- "package.json",
2517
- "Cargo.toml",
2518
- "pyproject.toml",
2519
- "pubspec.yaml",
2520
- "composer.json",
2521
- "pom.xml",
2522
- "VERSION"
2523
- ].some((m) => fs.existsSync(path.join(root, m)))
2524
- };
2525
- }
2526
- function formatNotProjectError(cwd, command) {
2527
- const dir = path.basename(cwd) || cwd;
2528
- const lines = [
2529
- `Not a VersionGuard project: ${dir}`,
2530
- "",
2531
- "No .versionguard.yml, .git directory, or manifest file found.",
2532
- "",
2533
- "To get started:",
2534
- " versionguard init Set up a new project interactively",
2535
- " versionguard init --yes Set up with defaults",
2536
- "",
2537
- "Or run from a project root directory:",
2538
- ` cd /path/to/project && versionguard ${command}`
2539
- ];
2540
- return lines.join("\n");
2541
- }
2542
- function runGit(cwd, args, encoding) {
2543
- return childProcess.execFileSync("git", args, {
2544
- cwd,
2545
- encoding,
2546
- stdio: ["pipe", "pipe", "ignore"]
2547
- });
2548
- }
2549
- function runGitText(cwd, args) {
2550
- return runGit(cwd, args, "utf-8");
2551
- }
2552
- function getLatestTag(cwd = process.cwd()) {
2553
- try {
2554
- const result = runGitText(cwd, ["describe", "--tags", "--abbrev=0"]);
2555
- const tagName = result.trim();
2556
- const version = tagName.replace(/^v/, "");
2557
- const dateResult = runGitText(cwd, ["log", "-1", "--format=%ai", tagName]);
2558
- const messageResult = runGitText(cwd, ["tag", "-l", tagName, "--format=%(contents)"]);
2559
- const message = messageResult.trim();
2560
- return {
2561
- name: tagName,
2562
- version,
2563
- message: message.length > 0 ? message : void 0,
2564
- date: new Date(dateResult.trim())
2565
- };
2566
- } catch {
2567
- return null;
2568
- }
2569
- }
2570
- function getAllTags(cwd = process.cwd()) {
2571
- try {
2572
- const result = runGitText(cwd, ["tag", "--list"]);
2573
- return result.trim().split("\n").filter(Boolean).map((name) => ({
2574
- name,
2575
- version: name.replace(/^v/, ""),
2576
- date: /* @__PURE__ */ new Date()
2577
- // Would need individual lookup for accurate dates
2578
- }));
2579
- } catch {
2580
- return [];
2581
- }
2582
- }
2583
- function createTag(version, message, autoFix = true, config, cwd = process.cwd()) {
2584
- const actions = [];
2585
- try {
2586
- if (!config) {
2587
- return {
2588
- success: false,
2589
- message: "VersionGuard config is required to create tags safely",
2590
- actions
2591
- };
2592
- }
2593
- const packageVersion = getPackageVersion(cwd, config.manifest);
2594
- const shouldAutoFix = autoFix;
2595
- const preflightError = getTagPreflightError(config, cwd, version, shouldAutoFix);
2596
- if (preflightError) {
2597
- return {
2598
- success: false,
2599
- message: preflightError,
2600
- actions
2601
- };
2602
- }
2603
- if (version !== packageVersion && !autoFix) {
2604
- return {
2605
- success: false,
2606
- message: `Version mismatch: manifest version is ${packageVersion}, tag is ${version}`,
2607
- actions: []
2608
- };
2609
- }
2610
- if (autoFix) {
2611
- const fixResults = version !== packageVersion ? fixAll(config, version, cwd) : fixAll(config, void 0, cwd);
2612
- for (const result of fixResults) {
2613
- if (result.fixed) {
2614
- actions.push(result.message);
2615
- }
2616
- }
2617
- if (fixResults.some((result) => result.fixed)) {
2618
- runGit(cwd, ["add", "-A"]);
2619
- runGit(cwd, ["commit", "--no-verify", "-m", `chore(release): ${version}`]);
2620
- actions.push("Committed version changes");
2621
- }
2622
- }
2623
- const tagName = `v${version}`;
2624
- const tagMessage = message || `Release ${version}`;
2625
- if (getAllTags(cwd).some((tag) => tag.name === tagName)) {
2626
- return {
2627
- success: false,
2628
- message: `Tag ${tagName} already exists`,
2629
- actions
2630
- };
2631
- }
2632
- runGit(cwd, ["tag", "-a", tagName, "-m", tagMessage]);
2633
- actions.push(`Created tag ${tagName}`);
2634
- return {
2635
- success: true,
2636
- message: `Successfully created tag ${tagName}`,
2637
- actions
2638
- };
2639
- } catch (err) {
2640
- return {
2641
- success: false,
2642
- message: `Failed to create tag: ${err.message}`,
2643
- actions
2644
- };
2645
- }
2646
- }
2647
- function handlePostTag(config, cwd = process.cwd()) {
2648
- const actions = [];
2649
- try {
2650
- const preflightError = getTagPreflightError(config, cwd);
2651
- if (preflightError) {
2652
- return {
2653
- success: false,
2654
- message: preflightError,
2655
- actions
2656
- };
2657
- }
2658
- const tag = getLatestTag(cwd);
2659
- if (!tag) {
2660
- return {
2661
- success: false,
2662
- message: "No tag found",
2663
- actions
2664
- };
2665
- }
2666
- const packageVersion = getPackageVersion(cwd, config.manifest);
2667
- if (tag.version !== packageVersion) {
2668
- return {
2669
- success: false,
2670
- message: `Tag version ${tag.version} doesn't match manifest version ${packageVersion}`,
2671
- actions: [
2672
- "To fix: delete tag and recreate with correct version",
2673
- ` git tag -d ${tag.name}`,
2674
- ` Update manifest to ${tag.version}`,
2675
- ` git tag ${tag.name}`
2676
- ]
2677
- };
2678
- }
2679
- const syncResults = fixAll(config, packageVersion, cwd);
2680
- for (const result of syncResults) {
2681
- if (result.fixed) {
2682
- actions.push(result.message);
2683
- }
2684
- }
2685
- return {
2686
- success: true,
2687
- message: `Post-tag workflow completed for ${tag.name}`,
2688
- actions
2689
- };
2690
- } catch (err) {
2691
- return {
2692
- success: false,
2693
- message: `Post-tag workflow failed: ${err.message}`,
2694
- actions
2695
- };
2696
- }
2697
- }
2698
- function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false) {
2699
- if (config.git.enforceHooks && !areHooksInstalled(cwd)) {
2700
- return "Git hooks must be installed before creating or validating release tags";
2701
- }
2702
- if (hasDirtyWorktree(cwd)) {
2703
- return "Working tree must be clean before creating or validating release tags";
2704
- }
2705
- const version = expectedVersion ?? getPackageVersion(cwd, config.manifest);
2706
- const versionResult = config.versioning.type === "semver" ? validate$1(version, getSemVerConfig(config), config.versioning.schemeRules) : validate$2(
2707
- version,
2708
- config.versioning.calver?.format ?? "YYYY.MM.PATCH",
2709
- config.versioning.calver?.preventFutureDates ?? true,
2710
- config.versioning.schemeRules
2711
- );
2712
- if (!versionResult.valid) {
2713
- return versionResult.errors[0]?.message ?? `Invalid version: ${version}`;
2714
- }
2715
- if (allowAutoFix) {
2716
- return null;
2717
- }
2718
- const mismatches = checkHardcodedVersions(version, config.sync, config.ignore, cwd);
2719
- if (mismatches.length > 0) {
2720
- const mismatch = mismatches[0];
2721
- return `Version mismatch in ${mismatch.file}:${mismatch.line} - found "${mismatch.found}" but expected "${version}"`;
2722
- }
2723
- const changelogResult = validateChangelog(
2724
- path.join(cwd, config.changelog.file),
2725
- version,
2726
- config.changelog.strict,
2727
- config.changelog.requireEntry,
2728
- {
2729
- enforceStructure: config.changelog.enforceStructure,
2730
- sections: config.changelog.sections
2731
- }
2732
- );
2733
- if (!changelogResult.valid) {
2734
- return changelogResult.errors[0] ?? "Changelog validation failed";
2735
- }
2736
- return null;
2737
- }
2738
- function hasDirtyWorktree(cwd) {
2739
- try {
2740
- return runGitText(cwd, ["status", "--porcelain"]).trim().length > 0;
2741
- } catch {
2742
- return true;
2743
- }
2744
- }
2745
- function validateTagForPush(tagName, cwd = process.cwd()) {
2746
- try {
2747
- runGit(cwd, ["rev-parse", tagName]);
2748
- try {
2749
- runGit(cwd, ["ls-remote", "--tags", "origin", tagName]);
2750
- const localHash = runGitText(cwd, ["rev-parse", tagName]).trim();
2751
- const remoteOutput = runGitText(cwd, ["ls-remote", "--tags", "origin", tagName]).trim();
2752
- if (remoteOutput && !remoteOutput.includes(localHash)) {
2753
- return {
2754
- valid: false,
2755
- message: `Tag ${tagName} exists on remote with different commit`,
2756
- fix: `Delete remote tag first: git push origin :refs/tags/${tagName}`
2757
- };
2758
- }
2759
- } catch {
2760
- return { valid: true, message: `Tag ${tagName} is valid for push` };
2761
- }
2762
- return { valid: true, message: `Tag ${tagName} is valid for push` };
2763
- } catch {
2764
- return {
2765
- valid: false,
2766
- message: `Tag ${tagName} not found locally`,
2767
- fix: `Create tag: git tag ${tagName}`
2768
- };
2769
- }
2770
- }
2771
- function suggestTagMessage(version, cwd = process.cwd()) {
2772
- try {
2773
- const changelogPath = path.join(cwd, "CHANGELOG.md");
2774
- if (fs.existsSync(changelogPath)) {
2775
- const content = fs.readFileSync(changelogPath, "utf-8");
2776
- const versionRegex = new RegExp(
2777
- `## \\[${version}\\].*?\\n(.*?)(?=\\n## \\[|\\n\\n## |$)`,
2778
- "s"
2779
- );
2780
- const match = content.match(versionRegex);
2781
- if (match) {
2782
- const bulletMatch = match[1].match(/- (.+)/);
2783
- if (bulletMatch) {
2784
- return `Release ${version}: ${bulletMatch[1].trim()}`;
2785
- }
2786
- }
2787
- }
2788
- } catch {
2789
- return `Release ${version}`;
2790
- }
2791
- return `Release ${version}`;
2792
- }
2793
- function validateVersion(version, config) {
2794
- if (config.versioning.type === "semver") {
2795
- return validate$1(version, getSemVerConfig(config), config.versioning.schemeRules);
2796
- }
2797
- const calverConfig = getCalVerConfig(config);
2798
- return validate$2(
2799
- version,
2800
- calverConfig.format,
2801
- calverConfig.preventFutureDates,
2802
- config.versioning.schemeRules
2803
- );
2804
- }
2805
- function validate(config, cwd = process.cwd()) {
2806
- const errors = [];
2807
- let version;
2808
- try {
2809
- version = getPackageVersion(cwd, config.manifest);
2810
- } catch (err) {
2811
- return {
2812
- valid: false,
2813
- version: "",
2814
- versionValid: false,
2815
- syncValid: false,
2816
- changelogValid: false,
2817
- errors: [err.message]
2818
- };
2819
- }
2820
- const versionResult = validateVersion(version, config);
2821
- if (!versionResult.valid) {
2822
- errors.push(...versionResult.errors.map((error) => error.message));
2823
- }
2824
- const hardcoded = checkHardcodedVersions(version, config.sync, config.ignore, cwd);
2825
- if (hardcoded.length > 0) {
2826
- for (const mismatch of hardcoded) {
2827
- errors.push(
2828
- `Version mismatch in ${mismatch.file}:${mismatch.line} - found "${mismatch.found}" but expected "${version}"`
2829
- );
2830
- }
2831
- }
2832
- let changelogValid = true;
2833
- if (config.changelog.enabled) {
2834
- const changelogPath = path.join(cwd, config.changelog.file);
2835
- const changelogResult = validateChangelog(
2836
- changelogPath,
2837
- version,
2838
- config.changelog.strict,
2839
- config.changelog.requireEntry,
2840
- {
2841
- enforceStructure: config.changelog.enforceStructure,
2842
- sections: config.changelog.sections
2843
- }
2844
- );
2845
- if (!changelogResult.valid) {
2846
- changelogValid = false;
2847
- errors.push(...changelogResult.errors);
2848
- }
2849
- }
2850
- return {
2851
- valid: errors.length === 0,
2852
- version,
2853
- versionValid: versionResult.valid,
2854
- syncValid: hardcoded.length === 0,
2855
- changelogValid,
2856
- errors
2857
- };
2858
- }
2859
- function doctor(config, cwd = process.cwd()) {
2860
- const validation = validate(config, cwd);
2861
- const gitRepository = findGitDir(cwd) !== null;
2862
- const hooksInstalled = gitRepository ? areHooksInstalled(cwd) : false;
2863
- const worktreeClean = gitRepository ? isWorktreeClean(cwd) : true;
2864
- const errors = [...validation.errors];
2865
- if (gitRepository && config.git.enforceHooks && !hooksInstalled) {
2866
- errors.push("Git hooks are not installed");
2867
- }
2868
- if (gitRepository && !worktreeClean) {
2869
- errors.push("Working tree is not clean");
2870
- }
2871
- return {
2872
- ready: errors.length === 0,
2873
- version: validation.version,
2874
- versionValid: validation.versionValid,
2875
- syncValid: validation.syncValid,
2876
- changelogValid: validation.changelogValid,
2877
- gitRepository,
2878
- hooksInstalled,
2879
- worktreeClean,
2880
- errors
2881
- };
2882
- }
2883
- function sync(config, cwd = process.cwd()) {
2884
- const version = getPackageVersion(cwd, config.manifest);
2885
- syncVersion(version, config.sync, cwd);
2886
- }
2887
- function canBump(currentVersion, newVersion, config) {
2888
- const currentValid = validateVersion(currentVersion, config);
2889
- const newValid = validateVersion(newVersion, config);
2890
- if (!currentValid.valid) {
2891
- return { canBump: false, error: `Current version is invalid: ${currentVersion}` };
2892
- }
2893
- if (!newValid.valid) {
2894
- return { canBump: false, error: `New version is invalid: ${newVersion}` };
2895
- }
2896
- if (config.versioning.type === "semver") {
2897
- if (!gt(newVersion, currentVersion)) {
2898
- return {
2899
- canBump: false,
2900
- error: `New version ${newVersion} must be greater than current ${currentVersion}`
2901
- };
2902
- }
2903
- } else {
2904
- const calverConfig = getCalVerConfig(config);
2905
- const currentParsed = parse$1(currentVersion, calverConfig.format);
2906
- const newParsed = parse$1(newVersion, calverConfig.format);
2907
- if (!currentParsed || !newParsed) {
2908
- return { canBump: false, error: "Failed to parse CalVer versions" };
2909
- }
2910
- if (compare$1(newVersion, currentVersion, calverConfig.format) <= 0) {
2911
- return {
2912
- canBump: false,
2913
- error: `New CalVer ${newVersion} must be newer than current ${currentVersion}`
2914
- };
2915
- }
2916
- }
2917
- return { canBump: true };
2918
- }
2919
- function isWorktreeClean(cwd) {
2920
- try {
2921
- return execSync("git status --porcelain", { cwd, encoding: "utf-8" }).trim().length === 0;
2922
- } catch {
2923
- return false;
2924
- }
2925
- }
2926
- export {
2927
- validateVersion as $,
2928
- checkHardcodedVersions as A,
2929
- checkHookIntegrity as B,
2930
- checkHooksPathOverride as C,
2931
- checkHuskyBypass as D,
2932
- detectManifests as E,
2933
- fixChangelog as F,
2934
- GitTagSource as G,
2935
- fixPackageVersion as H,
2936
- getAllTags as I,
2937
- JsonVersionSource as J,
2938
- getCalVerConfig as K,
2939
- getLatestTag as L,
2940
- getSemVerConfig as M,
2941
- getTagFeedback as N,
2942
- getVersionSource as O,
2943
- initConfig as P,
2944
- resolveVersionSource as Q,
2945
- RegexVersionSource as R,
2946
- semver as S,
2947
- TomlVersionSource as T,
2948
- suggestTagMessage as U,
2949
- VersionFileSource as V,
2950
- sync as W,
2951
- syncVersion as X,
2952
- YamlVersionSource as Y,
2953
- validateChangelog as Z,
2954
- validateTagForPush as _,
2955
- installHooks as a,
2956
- getPackageVersion as b,
2957
- createCkmEngine as c,
2958
- getVersionFeedback as d,
2959
- getSyncFeedback as e,
2960
- getChangelogFeedback as f,
2961
- getConfig as g,
2962
- handlePostTag as h,
2963
- isValidCalVerFormat as i,
2964
- doctor as j,
2965
- fixAll as k,
2966
- isChangesetMangled as l,
2967
- fixChangesetMangling as m,
2968
- fixSyncIssues as n,
2969
- setPackageVersion as o,
2970
- createTag as p,
2971
- areHooksInstalled as q,
2972
- runGuardChecks as r,
2973
- suggestNextVersion as s,
2974
- findProjectRoot as t,
2975
- uninstallHooks as u,
2976
- validate as v,
2977
- formatNotProjectError as w,
2978
- calver as x,
2979
- canBump as y,
2980
- checkEnforceHooksPolicy as z
2981
- };
2982
- //# sourceMappingURL=index-DWiw8Nps.js.map