@codluv/versionguard 0.3.0 → 0.5.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.
- package/dist/calver.d.ts +66 -22
- package/dist/calver.d.ts.map +1 -1
- package/dist/changelog.d.ts +52 -0
- package/dist/changelog.d.ts.map +1 -1
- package/dist/chunks/{index-BrZJDWya.js → index-CwOyEn5L.js} +904 -176
- package/dist/chunks/index-CwOyEn5L.js.map +1 -0
- package/dist/ckm/engine.d.ts +92 -0
- package/dist/ckm/engine.d.ts.map +1 -0
- package/dist/ckm/index.d.ts +32 -0
- package/dist/ckm/index.d.ts.map +1 -0
- package/dist/ckm/types.d.ts +168 -0
- package/dist/ckm/types.d.ts.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1333 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/feedback/index.d.ts +1 -1
- package/dist/feedback/index.d.ts.map +1 -1
- package/dist/fix/index.d.ts +6 -1
- package/dist/fix/index.d.ts.map +1 -1
- package/dist/guard.d.ts +45 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/hooks.d.ts +14 -7
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -32
- package/dist/init-wizard.d.ts +68 -0
- package/dist/init-wizard.d.ts.map +1 -0
- package/dist/project-root.d.ts +74 -0
- package/dist/project-root.d.ts.map +1 -0
- package/dist/project.d.ts +23 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/sources/git-tag.d.ts +29 -0
- package/dist/sources/git-tag.d.ts.map +1 -1
- package/dist/sources/json.d.ts +31 -0
- package/dist/sources/json.d.ts.map +1 -1
- package/dist/sources/regex.d.ts +30 -0
- package/dist/sources/regex.d.ts.map +1 -1
- package/dist/sources/resolve.d.ts +27 -8
- package/dist/sources/resolve.d.ts.map +1 -1
- package/dist/sources/toml.d.ts +36 -1
- package/dist/sources/toml.d.ts.map +1 -1
- package/dist/sources/utils.d.ts +73 -0
- package/dist/sources/utils.d.ts.map +1 -0
- package/dist/sources/version-file.d.ts +28 -1
- package/dist/sources/version-file.d.ts.map +1 -1
- package/dist/sources/yaml.d.ts +29 -0
- package/dist/sources/yaml.d.ts.map +1 -1
- package/dist/tag/index.d.ts.map +1 -1
- package/dist/types.d.ts +89 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/chunks/index-BrZJDWya.js.map +0 -1
|
@@ -6,38 +6,83 @@ import { parse as parse$2 } from "smol-toml";
|
|
|
6
6
|
import * as yaml from "js-yaml";
|
|
7
7
|
import { globSync } from "glob";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
const VALID_TOKENS = /* @__PURE__ */ new Set([
|
|
10
|
+
"YYYY",
|
|
11
|
+
"YY",
|
|
12
|
+
"0Y",
|
|
13
|
+
"MM",
|
|
14
|
+
"M",
|
|
15
|
+
"0M",
|
|
16
|
+
"WW",
|
|
17
|
+
"0W",
|
|
18
|
+
"DD",
|
|
19
|
+
"D",
|
|
20
|
+
"0D",
|
|
21
|
+
"MICRO",
|
|
22
|
+
"PATCH"
|
|
23
|
+
]);
|
|
24
|
+
const YEAR_TOKENS = /* @__PURE__ */ new Set(["YYYY", "YY", "0Y"]);
|
|
25
|
+
const MONTH_TOKENS = /* @__PURE__ */ new Set(["MM", "M", "0M"]);
|
|
26
|
+
const WEEK_TOKENS = /* @__PURE__ */ new Set(["WW", "0W"]);
|
|
27
|
+
const DAY_TOKENS = /* @__PURE__ */ new Set(["DD", "D", "0D"]);
|
|
28
|
+
const COUNTER_TOKENS = /* @__PURE__ */ new Set(["MICRO", "PATCH"]);
|
|
29
|
+
function isValidCalVerFormat(formatStr) {
|
|
30
|
+
const tokens = formatStr.split(".");
|
|
31
|
+
if (tokens.length < 2) return false;
|
|
32
|
+
if (!tokens.every((t) => VALID_TOKENS.has(t))) return false;
|
|
33
|
+
if (!YEAR_TOKENS.has(tokens[0])) return false;
|
|
34
|
+
const hasWeek = tokens.some((t) => WEEK_TOKENS.has(t));
|
|
35
|
+
const hasMonthOrDay = tokens.some((t) => MONTH_TOKENS.has(t) || DAY_TOKENS.has(t));
|
|
36
|
+
if (hasWeek && hasMonthOrDay) return false;
|
|
37
|
+
const counterIndex = tokens.findIndex((t) => COUNTER_TOKENS.has(t));
|
|
38
|
+
if (counterIndex !== -1 && counterIndex !== tokens.length - 1) return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
9
41
|
function parseFormat(calverFormat) {
|
|
10
|
-
const
|
|
42
|
+
const tokens = calverFormat.split(".");
|
|
11
43
|
const result = {
|
|
12
|
-
year:
|
|
13
|
-
month: parts[1]
|
|
44
|
+
year: tokens[0]
|
|
14
45
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
47
|
+
const token = tokens[i];
|
|
48
|
+
if (MONTH_TOKENS.has(token)) {
|
|
49
|
+
result.month = token;
|
|
50
|
+
} else if (WEEK_TOKENS.has(token)) {
|
|
51
|
+
result.week = token;
|
|
52
|
+
} else if (DAY_TOKENS.has(token)) {
|
|
53
|
+
result.day = token;
|
|
54
|
+
} else if (COUNTER_TOKENS.has(token)) {
|
|
55
|
+
result.counter = token;
|
|
56
|
+
}
|
|
22
57
|
}
|
|
23
58
|
return result;
|
|
24
59
|
}
|
|
60
|
+
const MODIFIER_PATTERN = "(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?";
|
|
25
61
|
function tokenPattern(token) {
|
|
26
62
|
switch (token) {
|
|
27
63
|
case "YYYY":
|
|
28
|
-
return "(\\d{
|
|
64
|
+
return "([1-9]\\d{3})";
|
|
29
65
|
case "YY":
|
|
30
|
-
return "(\\d{
|
|
31
|
-
case "
|
|
32
|
-
|
|
33
|
-
return "(\\d{2})";
|
|
66
|
+
return "(\\d{1,3})";
|
|
67
|
+
case "0Y":
|
|
68
|
+
return "(\\d{2,3})";
|
|
34
69
|
case "MM":
|
|
35
|
-
case "DD":
|
|
36
70
|
case "M":
|
|
71
|
+
return "([1-9]|1[0-2])";
|
|
72
|
+
case "0M":
|
|
73
|
+
return "(0[1-9]|1[0-2])";
|
|
74
|
+
case "WW":
|
|
75
|
+
return "([1-9]|[1-4]\\d|5[0-3])";
|
|
76
|
+
case "0W":
|
|
77
|
+
return "(0[1-9]|[1-4]\\d|5[0-3])";
|
|
78
|
+
case "DD":
|
|
37
79
|
case "D":
|
|
38
|
-
return "(\\d
|
|
80
|
+
return "([1-9]|[12]\\d|3[01])";
|
|
81
|
+
case "0D":
|
|
82
|
+
return "(0[1-9]|[12]\\d|3[01])";
|
|
83
|
+
case "MICRO":
|
|
39
84
|
case "PATCH":
|
|
40
|
-
return "(\\d
|
|
85
|
+
return "(0|[1-9]\\d*)";
|
|
41
86
|
default:
|
|
42
87
|
throw new Error(`Unsupported CalVer token: ${token}`);
|
|
43
88
|
}
|
|
@@ -45,7 +90,7 @@ function tokenPattern(token) {
|
|
|
45
90
|
function getRegexForFormat(calverFormat) {
|
|
46
91
|
const tokens = calverFormat.split(".");
|
|
47
92
|
const pattern = tokens.map(tokenPattern).join("\\.");
|
|
48
|
-
return new RegExp(`^${pattern}$`);
|
|
93
|
+
return new RegExp(`^${pattern}${MODIFIER_PATTERN}$`);
|
|
49
94
|
}
|
|
50
95
|
function parse$1(version, calverFormat) {
|
|
51
96
|
const match = version.match(getRegexForFormat(calverFormat));
|
|
@@ -53,28 +98,44 @@ function parse$1(version, calverFormat) {
|
|
|
53
98
|
return null;
|
|
54
99
|
}
|
|
55
100
|
const definition = parseFormat(calverFormat);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
const yearToken = definition.year;
|
|
102
|
+
let year = Number.parseInt(match[1], 10);
|
|
103
|
+
if (yearToken === "YY" || yearToken === "0Y") {
|
|
104
|
+
year = 2e3 + year;
|
|
105
|
+
}
|
|
106
|
+
let cursor = 2;
|
|
107
|
+
let month;
|
|
59
108
|
let day;
|
|
60
109
|
let patch;
|
|
110
|
+
if (definition.month) {
|
|
111
|
+
month = Number.parseInt(match[cursor], 10);
|
|
112
|
+
cursor += 1;
|
|
113
|
+
}
|
|
114
|
+
if (definition.week) {
|
|
115
|
+
month = Number.parseInt(match[cursor], 10);
|
|
116
|
+
cursor += 1;
|
|
117
|
+
}
|
|
61
118
|
if (definition.day) {
|
|
62
119
|
day = Number.parseInt(match[cursor], 10);
|
|
63
120
|
cursor += 1;
|
|
64
121
|
}
|
|
65
|
-
if (definition.
|
|
122
|
+
if (definition.counter) {
|
|
66
123
|
patch = Number.parseInt(match[cursor], 10);
|
|
124
|
+
cursor += 1;
|
|
67
125
|
}
|
|
126
|
+
const modifierGroup = match[cursor];
|
|
127
|
+
const modifier = modifierGroup || void 0;
|
|
68
128
|
return {
|
|
69
129
|
year,
|
|
70
|
-
month,
|
|
130
|
+
month: month ?? 1,
|
|
71
131
|
day,
|
|
72
132
|
patch,
|
|
133
|
+
modifier,
|
|
73
134
|
format: calverFormat,
|
|
74
135
|
raw: version
|
|
75
136
|
};
|
|
76
137
|
}
|
|
77
|
-
function validate$2(version, calverFormat, preventFutureDates = true) {
|
|
138
|
+
function validate$2(version, calverFormat, preventFutureDates = true, schemeRules) {
|
|
78
139
|
const errors = [];
|
|
79
140
|
const parsed = parse$1(version, calverFormat);
|
|
80
141
|
if (!parsed) {
|
|
@@ -88,19 +149,26 @@ function validate$2(version, calverFormat, preventFutureDates = true) {
|
|
|
88
149
|
]
|
|
89
150
|
};
|
|
90
151
|
}
|
|
91
|
-
|
|
152
|
+
const definition = parseFormat(calverFormat);
|
|
153
|
+
if (definition.month && (parsed.month < 1 || parsed.month > 12)) {
|
|
92
154
|
errors.push({
|
|
93
155
|
message: `Invalid month: ${parsed.month}. Must be between 1 and 12.`,
|
|
94
156
|
severity: "error"
|
|
95
157
|
});
|
|
96
158
|
}
|
|
159
|
+
if (definition.week && (parsed.month < 1 || parsed.month > 53)) {
|
|
160
|
+
errors.push({
|
|
161
|
+
message: `Invalid week: ${parsed.month}. Must be between 1 and 53.`,
|
|
162
|
+
severity: "error"
|
|
163
|
+
});
|
|
164
|
+
}
|
|
97
165
|
if (parsed.day !== void 0) {
|
|
98
166
|
if (parsed.day < 1 || parsed.day > 31) {
|
|
99
167
|
errors.push({
|
|
100
168
|
message: `Invalid day: ${parsed.day}. Must be between 1 and 31.`,
|
|
101
169
|
severity: "error"
|
|
102
170
|
});
|
|
103
|
-
} else {
|
|
171
|
+
} else if (definition.month) {
|
|
104
172
|
const daysInMonth = new Date(parsed.year, parsed.month, 0).getDate();
|
|
105
173
|
if (parsed.day > daysInMonth) {
|
|
106
174
|
errors.push({
|
|
@@ -120,56 +188,83 @@ function validate$2(version, calverFormat, preventFutureDates = true) {
|
|
|
120
188
|
message: `Future year not allowed: ${parsed.year}. Current year is ${currentYear}.`,
|
|
121
189
|
severity: "error"
|
|
122
190
|
});
|
|
123
|
-
} else if (parsed.year === currentYear && parsed.month > currentMonth) {
|
|
191
|
+
} else if (definition.month && parsed.year === currentYear && parsed.month > currentMonth) {
|
|
124
192
|
errors.push({
|
|
125
193
|
message: `Future month not allowed: ${parsed.year}.${parsed.month}. Current month is ${currentMonth}.`,
|
|
126
194
|
severity: "error"
|
|
127
195
|
});
|
|
128
|
-
} else if (parsed.year === currentYear && parsed.month === currentMonth && parsed.day !== void 0 && parsed.day > currentDay) {
|
|
196
|
+
} else if (definition.month && parsed.year === currentYear && parsed.month === currentMonth && parsed.day !== void 0 && parsed.day > currentDay) {
|
|
129
197
|
errors.push({
|
|
130
198
|
message: `Future day not allowed: ${parsed.year}.${parsed.month}.${parsed.day}. Current day is ${currentDay}.`,
|
|
131
199
|
severity: "error"
|
|
132
200
|
});
|
|
133
201
|
}
|
|
134
202
|
}
|
|
203
|
+
if (parsed.modifier && schemeRules?.allowedModifiers) {
|
|
204
|
+
const baseModifier = parsed.modifier.replace(/[\d.]+$/, "") || parsed.modifier;
|
|
205
|
+
if (!schemeRules.allowedModifiers.includes(baseModifier)) {
|
|
206
|
+
errors.push({
|
|
207
|
+
message: `Modifier "${parsed.modifier}" is not allowed. Allowed: ${schemeRules.allowedModifiers.join(", ")}`,
|
|
208
|
+
severity: "error"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (schemeRules?.maxNumericSegments) {
|
|
213
|
+
const segmentCount = calverFormat.split(".").length;
|
|
214
|
+
if (segmentCount > schemeRules.maxNumericSegments) {
|
|
215
|
+
errors.push({
|
|
216
|
+
message: `Format has ${segmentCount} segments, convention recommends ${schemeRules.maxNumericSegments} or fewer`,
|
|
217
|
+
severity: "warning"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
135
221
|
return {
|
|
136
|
-
valid: errors.length === 0,
|
|
222
|
+
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
137
223
|
errors,
|
|
138
224
|
version: { type: "calver", version: parsed }
|
|
139
225
|
};
|
|
140
226
|
}
|
|
141
227
|
function formatToken(token, value) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
228
|
+
switch (token) {
|
|
229
|
+
case "0M":
|
|
230
|
+
case "0D":
|
|
231
|
+
case "0W":
|
|
232
|
+
case "0Y":
|
|
233
|
+
return String(token === "0Y" ? value % 100 : value).padStart(2, "0");
|
|
234
|
+
case "YY":
|
|
235
|
+
return String(value % 100).padStart(2, "0");
|
|
236
|
+
default:
|
|
237
|
+
return String(value);
|
|
147
238
|
}
|
|
148
|
-
return String(value);
|
|
149
239
|
}
|
|
150
240
|
function format$1(version) {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
241
|
+
const definition = parseFormat(version.format);
|
|
242
|
+
const parts = [formatToken(definition.year, version.year)];
|
|
243
|
+
if (definition.month) {
|
|
244
|
+
parts.push(formatToken(definition.month, version.month));
|
|
155
245
|
}
|
|
156
|
-
if (
|
|
157
|
-
|
|
246
|
+
if (definition.week) {
|
|
247
|
+
parts.push(formatToken(definition.week, version.month));
|
|
158
248
|
}
|
|
159
|
-
|
|
249
|
+
if (definition.day) {
|
|
250
|
+
parts.push(formatToken(definition.day, version.day ?? 1));
|
|
251
|
+
}
|
|
252
|
+
if (definition.counter) {
|
|
253
|
+
parts.push(formatToken(definition.counter, version.patch ?? 0));
|
|
254
|
+
}
|
|
255
|
+
const base = parts.join(".");
|
|
256
|
+
return version.modifier ? `${base}-${version.modifier}` : base;
|
|
160
257
|
}
|
|
161
258
|
function getCurrentVersion(calverFormat, now = /* @__PURE__ */ new Date()) {
|
|
162
259
|
const definition = parseFormat(calverFormat);
|
|
163
|
-
const currentDay = now.getDate();
|
|
164
260
|
const base = {
|
|
165
261
|
year: now.getFullYear(),
|
|
166
262
|
month: now.getMonth() + 1,
|
|
167
|
-
day: definition.day ?
|
|
168
|
-
patch: definition.
|
|
263
|
+
day: definition.day ? now.getDate() : void 0,
|
|
264
|
+
patch: definition.counter ? 0 : void 0,
|
|
265
|
+
format: calverFormat
|
|
169
266
|
};
|
|
170
|
-
|
|
171
|
-
const patch = base.patch ?? 0;
|
|
172
|
-
return formatToken(definition.year, base.year).concat(`.${formatToken(definition.month, base.month)}`).concat(definition.day ? `.${formatToken(definition.day, day)}` : "").concat(definition.patch ? `.${patch}` : "");
|
|
267
|
+
return format$1(base);
|
|
173
268
|
}
|
|
174
269
|
function compare$1(a, b, calverFormat) {
|
|
175
270
|
const left = parse$1(a, calverFormat);
|
|
@@ -195,12 +290,11 @@ function increment$1(version, calverFormat) {
|
|
|
195
290
|
const next = {
|
|
196
291
|
...parsed
|
|
197
292
|
};
|
|
198
|
-
if (definition.
|
|
199
|
-
|
|
200
|
-
next.patch = patch + 1;
|
|
293
|
+
if (definition.counter) {
|
|
294
|
+
next.patch = (parsed.patch ?? 0) + 1;
|
|
201
295
|
} else {
|
|
202
296
|
next.patch = 0;
|
|
203
|
-
next.format = `${calverFormat}.
|
|
297
|
+
next.format = `${calverFormat}.MICRO`;
|
|
204
298
|
}
|
|
205
299
|
return format$1(next);
|
|
206
300
|
}
|
|
@@ -215,6 +309,7 @@ const calver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
|
|
|
215
309
|
getNextVersions,
|
|
216
310
|
getRegexForFormat,
|
|
217
311
|
increment: increment$1,
|
|
312
|
+
isValidCalVerFormat,
|
|
218
313
|
parse: parse$1,
|
|
219
314
|
parseFormat,
|
|
220
315
|
validate: validate$2
|
|
@@ -246,7 +341,7 @@ function validateChangelog(changelogPath, version, strict = true, requireEntry =
|
|
|
246
341
|
errors.push("Changelog should include compare links at the bottom");
|
|
247
342
|
}
|
|
248
343
|
const versionHeaderMatch = content.match(
|
|
249
|
-
new RegExp(`## \\[${escapeRegExp$
|
|
344
|
+
new RegExp(`## \\[${escapeRegExp$1(version)}\\] - ([^\r
|
|
250
345
|
]+)`)
|
|
251
346
|
);
|
|
252
347
|
if (requireEntry && hasEntryForVersion) {
|
|
@@ -286,10 +381,109 @@ function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Dat
|
|
|
286
381
|
const updated = `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
|
|
287
382
|
fs.writeFileSync(changelogPath, updated, "utf-8");
|
|
288
383
|
}
|
|
289
|
-
function
|
|
384
|
+
function isChangesetMangled(changelogPath) {
|
|
385
|
+
if (!fs.existsSync(changelogPath)) return false;
|
|
386
|
+
const content = fs.readFileSync(changelogPath, "utf-8");
|
|
387
|
+
return /^## \d+\.\d+/m.test(content) && content.includes("## [Unreleased]");
|
|
388
|
+
}
|
|
389
|
+
const SECTION_MAP = {
|
|
390
|
+
"Major Changes": "Changed",
|
|
391
|
+
"Minor Changes": "Added",
|
|
392
|
+
"Patch Changes": "Fixed"
|
|
393
|
+
};
|
|
394
|
+
function fixChangesetMangling(changelogPath, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
395
|
+
if (!fs.existsSync(changelogPath)) return false;
|
|
396
|
+
const content = fs.readFileSync(changelogPath, "utf-8");
|
|
397
|
+
const versionMatch = content.match(/^## (\d+\.\d+\.\d+[^\n]*)\n/m);
|
|
398
|
+
if (!versionMatch || versionMatch.index === void 0) return false;
|
|
399
|
+
const fullHeader = versionMatch[0];
|
|
400
|
+
if (fullHeader.includes("[")) return false;
|
|
401
|
+
const version = versionMatch[1].trim();
|
|
402
|
+
if (content.includes(`## [${version}]`)) return false;
|
|
403
|
+
const startIndex = versionMatch.index;
|
|
404
|
+
const preambleMatch = content.indexOf("All notable changes", startIndex);
|
|
405
|
+
const unreleasedMatch = content.indexOf("## [Unreleased]", startIndex);
|
|
406
|
+
let endIndex;
|
|
407
|
+
if (preambleMatch !== -1 && preambleMatch < unreleasedMatch) {
|
|
408
|
+
endIndex = preambleMatch;
|
|
409
|
+
} else if (unreleasedMatch !== -1) {
|
|
410
|
+
endIndex = unreleasedMatch;
|
|
411
|
+
} else {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const changesetsBlock = content.slice(startIndex + fullHeader.length, endIndex).trim();
|
|
415
|
+
const transformedSections = transformChangesetsContent(changesetsBlock);
|
|
416
|
+
const newEntry = `## [${version}] - ${date}
|
|
417
|
+
|
|
418
|
+
${transformedSections}
|
|
419
|
+
|
|
420
|
+
`;
|
|
421
|
+
const beforeChangesets = content.slice(0, startIndex);
|
|
422
|
+
const afterChangesets = content.slice(endIndex);
|
|
423
|
+
const unreleasedInAfter = afterChangesets.indexOf("## [Unreleased]");
|
|
424
|
+
if (unreleasedInAfter === -1) {
|
|
425
|
+
const rebuilt2 = `${beforeChangesets}${newEntry}${afterChangesets}`;
|
|
426
|
+
fs.writeFileSync(changelogPath, rebuilt2, "utf-8");
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const unreleasedLineEnd = afterChangesets.indexOf("\n", unreleasedInAfter);
|
|
430
|
+
const afterUnreleased = unreleasedLineEnd !== -1 ? afterChangesets.slice(0, unreleasedLineEnd + 1) : afterChangesets;
|
|
431
|
+
const rest = unreleasedLineEnd !== -1 ? afterChangesets.slice(unreleasedLineEnd + 1) : "";
|
|
432
|
+
const rebuilt = `${beforeChangesets}${afterUnreleased}
|
|
433
|
+
${newEntry}${rest}`;
|
|
434
|
+
const withLinks = updateCompareLinks(rebuilt, version);
|
|
435
|
+
fs.writeFileSync(changelogPath, withLinks, "utf-8");
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
function transformChangesetsContent(block) {
|
|
439
|
+
const lines = block.split("\n");
|
|
440
|
+
const result = [];
|
|
441
|
+
for (const line of lines) {
|
|
442
|
+
const sectionMatch = line.match(/^### (.+)/);
|
|
443
|
+
if (sectionMatch) {
|
|
444
|
+
const mapped = SECTION_MAP[sectionMatch[1]] ?? sectionMatch[1];
|
|
445
|
+
result.push(`### ${mapped}`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const entryMatch = line.match(
|
|
449
|
+
/^(\s*-\s+)[a-f0-9]{7,}: (?:feat|fix|chore|docs|refactor|perf|test|ci|build|style)(?:\([^)]*\))?: (.+)/
|
|
450
|
+
);
|
|
451
|
+
if (entryMatch) {
|
|
452
|
+
result.push(`${entryMatch[1]}${entryMatch[2]}`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const simpleHashMatch = line.match(/^(\s*-\s+)[a-f0-9]{7,}: (.+)/);
|
|
456
|
+
if (simpleHashMatch) {
|
|
457
|
+
result.push(`${simpleHashMatch[1]}${simpleHashMatch[2]}`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
result.push(line);
|
|
461
|
+
}
|
|
462
|
+
return result.join("\n");
|
|
463
|
+
}
|
|
464
|
+
function updateCompareLinks(content, version) {
|
|
465
|
+
const unreleasedLinkRegex = /\[Unreleased\]: (https:\/\/[^\s]+\/compare\/v)([\d.]+)(\.\.\.HEAD)/;
|
|
466
|
+
const match = content.match(unreleasedLinkRegex);
|
|
467
|
+
if (match) {
|
|
468
|
+
const baseUrl = match[1].replace(/v$/, "");
|
|
469
|
+
const previousVersion = match[2];
|
|
470
|
+
const newUnreleasedLink = `[Unreleased]: ${baseUrl}v${version}...HEAD`;
|
|
471
|
+
const newVersionLink = `[${version}]: ${baseUrl}v${previousVersion}...v${version}`;
|
|
472
|
+
let updated = content.replace(unreleasedLinkRegex, newUnreleasedLink);
|
|
473
|
+
if (!updated.includes(`[${version}]:`)) {
|
|
474
|
+
updated = updated.replace(newUnreleasedLink, `${newUnreleasedLink}
|
|
475
|
+
${newVersionLink}`);
|
|
476
|
+
}
|
|
477
|
+
return updated;
|
|
478
|
+
}
|
|
479
|
+
return content;
|
|
480
|
+
}
|
|
481
|
+
function escapeRegExp$1(value) {
|
|
290
482
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
291
483
|
}
|
|
292
484
|
const HOOK_NAMES$1 = ["pre-commit", "pre-push", "post-tag"];
|
|
485
|
+
const VG_BLOCK_START = "# >>> versionguard >>>";
|
|
486
|
+
const VG_BLOCK_END = "# <<< versionguard <<<";
|
|
293
487
|
function installHooks(config, cwd = process.cwd()) {
|
|
294
488
|
const gitDir = findGitDir(cwd);
|
|
295
489
|
if (!gitDir) {
|
|
@@ -300,7 +494,36 @@ function installHooks(config, cwd = process.cwd()) {
|
|
|
300
494
|
for (const hookName of HOOK_NAMES$1) {
|
|
301
495
|
if (config.hooks[hookName]) {
|
|
302
496
|
const hookPath = path.join(hooksDir, hookName);
|
|
303
|
-
|
|
497
|
+
const vgBlock = generateHookBlock(hookName);
|
|
498
|
+
if (fs.existsSync(hookPath)) {
|
|
499
|
+
const existing = fs.readFileSync(hookPath, "utf-8");
|
|
500
|
+
if (existing.includes(VG_BLOCK_START)) {
|
|
501
|
+
const updated = replaceVgBlock(existing, vgBlock);
|
|
502
|
+
fs.writeFileSync(hookPath, updated, { encoding: "utf-8", mode: 493 });
|
|
503
|
+
} else if (isLegacyVgHook(existing)) {
|
|
504
|
+
fs.writeFileSync(hookPath, `#!/bin/sh
|
|
505
|
+
|
|
506
|
+
${vgBlock}
|
|
507
|
+
`, {
|
|
508
|
+
encoding: "utf-8",
|
|
509
|
+
mode: 493
|
|
510
|
+
});
|
|
511
|
+
} else {
|
|
512
|
+
const appended = `${existing.trimEnd()}
|
|
513
|
+
|
|
514
|
+
${vgBlock}
|
|
515
|
+
`;
|
|
516
|
+
fs.writeFileSync(hookPath, appended, { encoding: "utf-8", mode: 493 });
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
fs.writeFileSync(hookPath, `#!/bin/sh
|
|
520
|
+
|
|
521
|
+
${vgBlock}
|
|
522
|
+
`, {
|
|
523
|
+
encoding: "utf-8",
|
|
524
|
+
mode: 493
|
|
525
|
+
});
|
|
526
|
+
}
|
|
304
527
|
}
|
|
305
528
|
}
|
|
306
529
|
}
|
|
@@ -312,7 +535,21 @@ function uninstallHooks(cwd = process.cwd()) {
|
|
|
312
535
|
const hooksDir = path.join(gitDir, "hooks");
|
|
313
536
|
for (const hookName of HOOK_NAMES$1) {
|
|
314
537
|
const hookPath = path.join(hooksDir, hookName);
|
|
315
|
-
if (fs.existsSync(hookPath)
|
|
538
|
+
if (!fs.existsSync(hookPath)) continue;
|
|
539
|
+
const content = fs.readFileSync(hookPath, "utf-8");
|
|
540
|
+
if (!content.includes("versionguard")) continue;
|
|
541
|
+
if (content.includes(VG_BLOCK_START)) {
|
|
542
|
+
const cleaned = removeVgBlock(content);
|
|
543
|
+
const trimmed = cleaned.trim();
|
|
544
|
+
if (!trimmed || trimmed === "#!/bin/sh") {
|
|
545
|
+
fs.unlinkSync(hookPath);
|
|
546
|
+
} else if (isLegacyVgHook(trimmed)) {
|
|
547
|
+
fs.unlinkSync(hookPath);
|
|
548
|
+
} else {
|
|
549
|
+
fs.writeFileSync(hookPath, `${trimmed}
|
|
550
|
+
`, { encoding: "utf-8", mode: 493 });
|
|
551
|
+
}
|
|
552
|
+
} else if (isLegacyVgHook(content)) {
|
|
316
553
|
fs.unlinkSync(hookPath);
|
|
317
554
|
}
|
|
318
555
|
}
|
|
@@ -341,9 +578,8 @@ function areHooksInstalled(cwd = process.cwd()) {
|
|
|
341
578
|
return fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard");
|
|
342
579
|
});
|
|
343
580
|
}
|
|
344
|
-
function
|
|
345
|
-
return
|
|
346
|
-
# versionguard
|
|
581
|
+
function generateHookBlock(hookName) {
|
|
582
|
+
return `${VG_BLOCK_START}
|
|
347
583
|
# VersionGuard ${hookName} hook
|
|
348
584
|
# --no-install prevents accidentally downloading an unscoped package
|
|
349
585
|
# if @codluv/versionguard is not installed locally
|
|
@@ -353,11 +589,47 @@ if [ $status -ne 0 ]; then
|
|
|
353
589
|
echo "VersionGuard validation failed."
|
|
354
590
|
exit $status
|
|
355
591
|
fi
|
|
592
|
+
${VG_BLOCK_END}`;
|
|
593
|
+
}
|
|
594
|
+
function generateHookScript(hookName) {
|
|
595
|
+
return `#!/bin/sh
|
|
596
|
+
|
|
597
|
+
${generateHookBlock(hookName)}
|
|
356
598
|
`;
|
|
357
599
|
}
|
|
600
|
+
function isLegacyVgHook(content) {
|
|
601
|
+
if (!content.includes("versionguard validate")) return false;
|
|
602
|
+
if (content.includes(VG_BLOCK_START)) return false;
|
|
603
|
+
if (content.includes("husky")) return false;
|
|
604
|
+
if (content.includes("lefthook")) return false;
|
|
605
|
+
if (content.includes("pre-commit run")) return false;
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
function replaceVgBlock(content, newBlock) {
|
|
609
|
+
const startIdx = content.indexOf(VG_BLOCK_START);
|
|
610
|
+
const endIdx = content.indexOf(VG_BLOCK_END);
|
|
611
|
+
if (startIdx === -1 || endIdx === -1) return content;
|
|
612
|
+
return content.slice(0, startIdx) + newBlock + content.slice(endIdx + VG_BLOCK_END.length);
|
|
613
|
+
}
|
|
614
|
+
function removeVgBlock(content) {
|
|
615
|
+
const startIdx = content.indexOf(VG_BLOCK_START);
|
|
616
|
+
const endIdx = content.indexOf(VG_BLOCK_END);
|
|
617
|
+
if (startIdx === -1 || endIdx === -1) return content;
|
|
618
|
+
const before = content.slice(0, startIdx).replace(/\n\n$/, "\n");
|
|
619
|
+
const after = content.slice(endIdx + VG_BLOCK_END.length).replace(/^\n\n/, "\n");
|
|
620
|
+
return before + after;
|
|
621
|
+
}
|
|
358
622
|
class GitTagSource {
|
|
623
|
+
/** Human-readable provider name. */
|
|
359
624
|
name = "git-tag";
|
|
625
|
+
/** Empty string since git-tag has no manifest file. */
|
|
360
626
|
manifestFile = "";
|
|
627
|
+
/**
|
|
628
|
+
* Returns `true` when `cwd` is inside a Git repository.
|
|
629
|
+
*
|
|
630
|
+
* @param cwd - Project directory to check.
|
|
631
|
+
* @returns Whether a Git repository is found.
|
|
632
|
+
*/
|
|
361
633
|
exists(cwd) {
|
|
362
634
|
try {
|
|
363
635
|
execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
@@ -369,87 +641,167 @@ class GitTagSource {
|
|
|
369
641
|
return false;
|
|
370
642
|
}
|
|
371
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Reads the version string from the latest Git tag.
|
|
646
|
+
*
|
|
647
|
+
* @param cwd - Project directory containing the Git repository.
|
|
648
|
+
* @returns The version string extracted from the latest version tag.
|
|
649
|
+
*/
|
|
372
650
|
getVersion(cwd) {
|
|
373
651
|
try {
|
|
374
|
-
const tag =
|
|
375
|
-
cwd,
|
|
376
|
-
encoding: "utf-8",
|
|
377
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
378
|
-
}).trim();
|
|
652
|
+
const tag = this.describeVersionTag(cwd);
|
|
379
653
|
return tag.replace(/^v/, "");
|
|
380
654
|
} catch {
|
|
381
|
-
throw new Error("No
|
|
655
|
+
throw new Error("No version tags found. Create a tag first (e.g., git tag v0.1.0)");
|
|
382
656
|
}
|
|
383
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Creates a new annotated Git tag for the given version.
|
|
660
|
+
*
|
|
661
|
+
* @param version - Version string to tag.
|
|
662
|
+
* @param cwd - Project directory containing the Git repository.
|
|
663
|
+
*/
|
|
384
664
|
setVersion(version, cwd) {
|
|
385
|
-
const
|
|
665
|
+
const prefix = this.detectPrefix(cwd);
|
|
666
|
+
const tagName = `${prefix}${version}`;
|
|
386
667
|
execFileSync("git", ["tag", "-a", tagName, "-m", `Release ${version}`], {
|
|
387
668
|
cwd,
|
|
388
669
|
stdio: ["pipe", "pipe", "ignore"]
|
|
389
670
|
});
|
|
390
671
|
}
|
|
672
|
+
/** Try version-like tag patterns, fall back to any tag. */
|
|
673
|
+
describeVersionTag(cwd) {
|
|
674
|
+
try {
|
|
675
|
+
return execFileSync("git", ["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"], {
|
|
676
|
+
cwd,
|
|
677
|
+
encoding: "utf-8",
|
|
678
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
679
|
+
}).trim();
|
|
680
|
+
} catch {
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
return execFileSync("git", ["describe", "--tags", "--abbrev=0", "--match", "[0-9]*"], {
|
|
684
|
+
cwd,
|
|
685
|
+
encoding: "utf-8",
|
|
686
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
687
|
+
}).trim();
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error("No version tags found");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/** Detect whether existing tags use a `v` prefix or not. */
|
|
693
|
+
detectPrefix(cwd) {
|
|
694
|
+
try {
|
|
695
|
+
const tag = this.describeVersionTag(cwd);
|
|
696
|
+
return tag.startsWith("v") ? "v" : "";
|
|
697
|
+
} catch {
|
|
698
|
+
return "v";
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function getNestedValue(obj, dotPath) {
|
|
703
|
+
let current = obj;
|
|
704
|
+
for (const key of dotPath.split(".")) {
|
|
705
|
+
if (current === null || typeof current !== "object") {
|
|
706
|
+
return void 0;
|
|
707
|
+
}
|
|
708
|
+
current = current[key];
|
|
709
|
+
}
|
|
710
|
+
return current;
|
|
711
|
+
}
|
|
712
|
+
function setNestedValue(obj, dotPath, value) {
|
|
713
|
+
const keys = dotPath.split(".");
|
|
714
|
+
let current = obj;
|
|
715
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
716
|
+
const next = current[keys[i]];
|
|
717
|
+
if (typeof next !== "object" || next === null) {
|
|
718
|
+
throw new Error(`Missing intermediate key '${keys.slice(0, i + 1).join(".")}' in manifest`);
|
|
719
|
+
}
|
|
720
|
+
current = next;
|
|
721
|
+
}
|
|
722
|
+
current[keys[keys.length - 1]] = value;
|
|
723
|
+
}
|
|
724
|
+
function escapeRegExp(value) {
|
|
725
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
391
726
|
}
|
|
392
727
|
class JsonVersionSource {
|
|
728
|
+
/** Human-readable provider name. */
|
|
393
729
|
name;
|
|
730
|
+
/** Filename of the JSON manifest (e.g. `'package.json'`). */
|
|
394
731
|
manifestFile;
|
|
732
|
+
/** Dotted key path to the version field within the JSON document. */
|
|
395
733
|
versionPath;
|
|
734
|
+
/**
|
|
735
|
+
* Creates a new JSON version source.
|
|
736
|
+
*
|
|
737
|
+
* @param manifestFile - JSON manifest filename.
|
|
738
|
+
* @param versionPath - Dotted key path to the version field.
|
|
739
|
+
*/
|
|
396
740
|
constructor(manifestFile = "package.json", versionPath = "version") {
|
|
397
741
|
this.name = manifestFile;
|
|
398
742
|
this.manifestFile = manifestFile;
|
|
399
743
|
this.versionPath = versionPath;
|
|
400
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Returns `true` when the manifest file exists in `cwd`.
|
|
747
|
+
*
|
|
748
|
+
* @param cwd - Project directory to check.
|
|
749
|
+
* @returns Whether the manifest file exists.
|
|
750
|
+
*/
|
|
401
751
|
exists(cwd) {
|
|
402
752
|
return fs.existsSync(path.join(cwd, this.manifestFile));
|
|
403
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* Reads the version string from the JSON manifest.
|
|
756
|
+
*
|
|
757
|
+
* @param cwd - Project directory containing the manifest.
|
|
758
|
+
* @returns The version string extracted from the manifest.
|
|
759
|
+
*/
|
|
404
760
|
getVersion(cwd) {
|
|
405
761
|
const filePath = path.join(cwd, this.manifestFile);
|
|
406
762
|
if (!fs.existsSync(filePath)) {
|
|
407
763
|
throw new Error(`${this.manifestFile} not found in ${cwd}`);
|
|
408
764
|
}
|
|
409
765
|
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
410
|
-
const version = getNestedValue
|
|
766
|
+
const version = getNestedValue(content, this.versionPath);
|
|
411
767
|
if (typeof version !== "string" || version.length === 0) {
|
|
412
768
|
throw new Error(`No version field in ${this.manifestFile}`);
|
|
413
769
|
}
|
|
414
770
|
return version;
|
|
415
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Writes a version string to the JSON manifest, preserving indentation.
|
|
774
|
+
*
|
|
775
|
+
* @param version - Version string to write.
|
|
776
|
+
* @param cwd - Project directory containing the manifest.
|
|
777
|
+
*/
|
|
416
778
|
setVersion(version, cwd) {
|
|
417
779
|
const filePath = path.join(cwd, this.manifestFile);
|
|
418
780
|
if (!fs.existsSync(filePath)) {
|
|
419
781
|
throw new Error(`${this.manifestFile} not found in ${cwd}`);
|
|
420
782
|
}
|
|
421
|
-
const
|
|
783
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
784
|
+
const indentMatch = raw.match(/^(\s+)"/m);
|
|
785
|
+
const indent = indentMatch?.[1]?.length ?? 2;
|
|
786
|
+
const content = JSON.parse(raw);
|
|
422
787
|
setNestedValue(content, this.versionPath, version);
|
|
423
|
-
fs.writeFileSync(filePath, `${JSON.stringify(content, null,
|
|
788
|
+
fs.writeFileSync(filePath, `${JSON.stringify(content, null, indent)}
|
|
424
789
|
`, "utf-8");
|
|
425
790
|
}
|
|
426
791
|
}
|
|
427
|
-
function getNestedValue$1(obj, dotPath) {
|
|
428
|
-
let current = obj;
|
|
429
|
-
for (const key of dotPath.split(".")) {
|
|
430
|
-
if (current === null || typeof current !== "object") {
|
|
431
|
-
return void 0;
|
|
432
|
-
}
|
|
433
|
-
current = current[key];
|
|
434
|
-
}
|
|
435
|
-
return current;
|
|
436
|
-
}
|
|
437
|
-
function setNestedValue(obj, dotPath, value) {
|
|
438
|
-
const keys = dotPath.split(".");
|
|
439
|
-
let current = obj;
|
|
440
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
441
|
-
const key = keys[i];
|
|
442
|
-
if (typeof current[key] !== "object" || current[key] === null) {
|
|
443
|
-
current[key] = {};
|
|
444
|
-
}
|
|
445
|
-
current = current[key];
|
|
446
|
-
}
|
|
447
|
-
current[keys[keys.length - 1]] = value;
|
|
448
|
-
}
|
|
449
792
|
class RegexVersionSource {
|
|
793
|
+
/** Human-readable provider name. */
|
|
450
794
|
name;
|
|
795
|
+
/** Filename of the source manifest (e.g. `'setup.py'`). */
|
|
451
796
|
manifestFile;
|
|
797
|
+
/** Compiled regex used to locate the version string. */
|
|
452
798
|
versionRegex;
|
|
799
|
+
/**
|
|
800
|
+
* Creates a new regex version source.
|
|
801
|
+
*
|
|
802
|
+
* @param manifestFile - Source manifest filename.
|
|
803
|
+
* @param versionRegex - Regex string with at least one capture group for the version.
|
|
804
|
+
*/
|
|
453
805
|
constructor(manifestFile, versionRegex) {
|
|
454
806
|
this.name = manifestFile;
|
|
455
807
|
this.manifestFile = manifestFile;
|
|
@@ -462,9 +814,21 @@ class RegexVersionSource {
|
|
|
462
814
|
throw new Error(`Version regex for ${manifestFile} must contain at least one capture group`);
|
|
463
815
|
}
|
|
464
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Returns `true` when the manifest file exists in `cwd`.
|
|
819
|
+
*
|
|
820
|
+
* @param cwd - Project directory to check.
|
|
821
|
+
* @returns Whether the manifest file exists.
|
|
822
|
+
*/
|
|
465
823
|
exists(cwd) {
|
|
466
824
|
return fs.existsSync(path.join(cwd, this.manifestFile));
|
|
467
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Reads the version string from the source manifest using regex extraction.
|
|
828
|
+
*
|
|
829
|
+
* @param cwd - Project directory containing the manifest.
|
|
830
|
+
* @returns The version string captured by group 1 of the regex.
|
|
831
|
+
*/
|
|
468
832
|
getVersion(cwd) {
|
|
469
833
|
const filePath = path.join(cwd, this.manifestFile);
|
|
470
834
|
if (!fs.existsSync(filePath)) {
|
|
@@ -477,6 +841,12 @@ class RegexVersionSource {
|
|
|
477
841
|
}
|
|
478
842
|
return match[1];
|
|
479
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Writes a version string to the source manifest using position-based replacement.
|
|
846
|
+
*
|
|
847
|
+
* @param version - Version string to write.
|
|
848
|
+
* @param cwd - Project directory containing the manifest.
|
|
849
|
+
*/
|
|
480
850
|
setVersion(version, cwd) {
|
|
481
851
|
const filePath = path.join(cwd, this.manifestFile);
|
|
482
852
|
if (!fs.existsSync(filePath)) {
|
|
@@ -494,17 +864,38 @@ class RegexVersionSource {
|
|
|
494
864
|
}
|
|
495
865
|
}
|
|
496
866
|
class TomlVersionSource {
|
|
867
|
+
/** Human-readable provider name. */
|
|
497
868
|
name;
|
|
869
|
+
/** Filename of the TOML manifest (e.g. `'Cargo.toml'`). */
|
|
498
870
|
manifestFile;
|
|
871
|
+
/** Dotted key path to the version field within the TOML document. */
|
|
499
872
|
versionPath;
|
|
873
|
+
/**
|
|
874
|
+
* Creates a new TOML version source.
|
|
875
|
+
*
|
|
876
|
+
* @param manifestFile - TOML manifest filename.
|
|
877
|
+
* @param versionPath - Dotted key path to the version field.
|
|
878
|
+
*/
|
|
500
879
|
constructor(manifestFile = "Cargo.toml", versionPath = "package.version") {
|
|
501
880
|
this.name = manifestFile;
|
|
502
881
|
this.manifestFile = manifestFile;
|
|
503
882
|
this.versionPath = versionPath;
|
|
504
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* Returns `true` when the manifest file exists in `cwd`.
|
|
886
|
+
*
|
|
887
|
+
* @param cwd - Project directory to check.
|
|
888
|
+
* @returns Whether the manifest file exists.
|
|
889
|
+
*/
|
|
505
890
|
exists(cwd) {
|
|
506
891
|
return fs.existsSync(path.join(cwd, this.manifestFile));
|
|
507
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Reads the version string from the TOML manifest.
|
|
895
|
+
*
|
|
896
|
+
* @param cwd - Project directory containing the manifest.
|
|
897
|
+
* @returns The version string extracted from the manifest.
|
|
898
|
+
*/
|
|
508
899
|
getVersion(cwd) {
|
|
509
900
|
const filePath = path.join(cwd, this.manifestFile);
|
|
510
901
|
if (!fs.existsSync(filePath)) {
|
|
@@ -518,6 +909,12 @@ class TomlVersionSource {
|
|
|
518
909
|
}
|
|
519
910
|
return version;
|
|
520
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Writes a version string to the TOML manifest, preserving formatting.
|
|
914
|
+
*
|
|
915
|
+
* @param version - Version string to write.
|
|
916
|
+
* @param cwd - Project directory containing the manifest.
|
|
917
|
+
*/
|
|
521
918
|
setVersion(version, cwd) {
|
|
522
919
|
const filePath = path.join(cwd, this.manifestFile);
|
|
523
920
|
if (!fs.existsSync(filePath)) {
|
|
@@ -531,6 +928,11 @@ class TomlVersionSource {
|
|
|
531
928
|
}
|
|
532
929
|
fs.writeFileSync(filePath, updated, "utf-8");
|
|
533
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* Splits the dotted version path into a TOML section name and key name.
|
|
933
|
+
*
|
|
934
|
+
* @returns An object with `section` and `key` components.
|
|
935
|
+
*/
|
|
534
936
|
getSectionKey() {
|
|
535
937
|
const parts = this.versionPath.split(".");
|
|
536
938
|
if (parts.length === 1) {
|
|
@@ -542,21 +944,32 @@ class TomlVersionSource {
|
|
|
542
944
|
};
|
|
543
945
|
}
|
|
544
946
|
}
|
|
545
|
-
function
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
947
|
+
function replaceTomlVersion(content, target, newVersion) {
|
|
948
|
+
const result = replaceInSection(content, target, newVersion);
|
|
949
|
+
if (result !== content) return result;
|
|
950
|
+
if (target.section) {
|
|
951
|
+
const dottedRegex = new RegExp(
|
|
952
|
+
`^(\\s*${escapeRegExp(target.section)}\\.${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`,
|
|
953
|
+
"m"
|
|
954
|
+
);
|
|
955
|
+
const dottedResult = content.replace(dottedRegex, `$1$2${newVersion}$4`);
|
|
956
|
+
if (dottedResult !== content) return dottedResult;
|
|
552
957
|
}
|
|
553
|
-
|
|
958
|
+
if (target.section) {
|
|
959
|
+
const inlineRegex = new RegExp(
|
|
960
|
+
`^(\\s*${escapeRegExp(target.section)}\\s*=\\s*\\{[^}]*${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`,
|
|
961
|
+
"m"
|
|
962
|
+
);
|
|
963
|
+
const inlineResult = content.replace(inlineRegex, `$1$2${newVersion}$4`);
|
|
964
|
+
if (inlineResult !== content) return inlineResult;
|
|
965
|
+
}
|
|
966
|
+
return content;
|
|
554
967
|
}
|
|
555
|
-
function
|
|
968
|
+
function replaceInSection(content, target, newVersion) {
|
|
556
969
|
const lines = content.split("\n");
|
|
557
970
|
const sectionHeader = target.section ? `[${target.section}]` : null;
|
|
558
971
|
let inSection = sectionHeader === null;
|
|
559
|
-
const versionRegex = new RegExp(`^(\\s*${escapeRegExp
|
|
972
|
+
const versionRegex = new RegExp(`^(\\s*${escapeRegExp(target.key)}\\s*=\\s*)(["'])([^"']*)(\\2)`);
|
|
560
973
|
for (let i = 0; i < lines.length; i++) {
|
|
561
974
|
const trimmed = lines[i].trim();
|
|
562
975
|
if (sectionHeader !== null) {
|
|
@@ -579,30 +992,56 @@ function replaceTomlVersion(content, target, newVersion) {
|
|
|
579
992
|
}
|
|
580
993
|
return content;
|
|
581
994
|
}
|
|
582
|
-
function escapeRegExp$1(value) {
|
|
583
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
584
|
-
}
|
|
585
995
|
class VersionFileSource {
|
|
996
|
+
/** Human-readable provider name. */
|
|
586
997
|
name;
|
|
998
|
+
/** Filename of the version file (e.g. `'VERSION'`). */
|
|
587
999
|
manifestFile;
|
|
1000
|
+
/**
|
|
1001
|
+
* Creates a new plain text version file source.
|
|
1002
|
+
*
|
|
1003
|
+
* @param manifestFile - Version filename.
|
|
1004
|
+
*/
|
|
588
1005
|
constructor(manifestFile = "VERSION") {
|
|
589
1006
|
this.name = manifestFile;
|
|
590
1007
|
this.manifestFile = manifestFile;
|
|
591
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Returns `true` when the version file exists in `cwd`.
|
|
1011
|
+
*
|
|
1012
|
+
* @param cwd - Project directory to check.
|
|
1013
|
+
* @returns Whether the version file exists.
|
|
1014
|
+
*/
|
|
592
1015
|
exists(cwd) {
|
|
593
1016
|
return fs.existsSync(path.join(cwd, this.manifestFile));
|
|
594
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Reads the version string from the plain text version file.
|
|
1020
|
+
*
|
|
1021
|
+
* @param cwd - Project directory containing the version file.
|
|
1022
|
+
* @returns The version string from the first line of the file.
|
|
1023
|
+
*/
|
|
595
1024
|
getVersion(cwd) {
|
|
596
1025
|
const filePath = path.join(cwd, this.manifestFile);
|
|
597
1026
|
if (!fs.existsSync(filePath)) {
|
|
598
1027
|
throw new Error(`${this.manifestFile} not found in ${cwd}`);
|
|
599
1028
|
}
|
|
600
|
-
const
|
|
1029
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1030
|
+
if (raw.includes("\0")) {
|
|
1031
|
+
throw new Error(`${this.manifestFile} appears to be a binary file`);
|
|
1032
|
+
}
|
|
1033
|
+
const version = raw.split("\n")[0].trim();
|
|
601
1034
|
if (version.length === 0) {
|
|
602
1035
|
throw new Error(`${this.manifestFile} is empty`);
|
|
603
1036
|
}
|
|
604
1037
|
return version;
|
|
605
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Writes a version string to the plain text version file.
|
|
1041
|
+
*
|
|
1042
|
+
* @param version - Version string to write.
|
|
1043
|
+
* @param cwd - Project directory containing the version file.
|
|
1044
|
+
*/
|
|
606
1045
|
setVersion(version, cwd) {
|
|
607
1046
|
const filePath = path.join(cwd, this.manifestFile);
|
|
608
1047
|
if (!fs.existsSync(filePath)) {
|
|
@@ -613,17 +1052,38 @@ class VersionFileSource {
|
|
|
613
1052
|
}
|
|
614
1053
|
}
|
|
615
1054
|
class YamlVersionSource {
|
|
1055
|
+
/** Human-readable provider name. */
|
|
616
1056
|
name;
|
|
1057
|
+
/** Filename of the YAML manifest (e.g. `'pubspec.yaml'`). */
|
|
617
1058
|
manifestFile;
|
|
1059
|
+
/** Dotted key path to the version field within the YAML document. */
|
|
618
1060
|
versionKey;
|
|
1061
|
+
/**
|
|
1062
|
+
* Creates a new YAML version source.
|
|
1063
|
+
*
|
|
1064
|
+
* @param manifestFile - YAML manifest filename.
|
|
1065
|
+
* @param versionKey - Dotted key path to the version field.
|
|
1066
|
+
*/
|
|
619
1067
|
constructor(manifestFile = "pubspec.yaml", versionKey = "version") {
|
|
620
1068
|
this.name = manifestFile;
|
|
621
1069
|
this.manifestFile = manifestFile;
|
|
622
1070
|
this.versionKey = versionKey;
|
|
623
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Returns `true` when the manifest file exists in `cwd`.
|
|
1074
|
+
*
|
|
1075
|
+
* @param cwd - Project directory to check.
|
|
1076
|
+
* @returns Whether the manifest file exists.
|
|
1077
|
+
*/
|
|
624
1078
|
exists(cwd) {
|
|
625
1079
|
return fs.existsSync(path.join(cwd, this.manifestFile));
|
|
626
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Reads the version string from the YAML manifest.
|
|
1083
|
+
*
|
|
1084
|
+
* @param cwd - Project directory containing the manifest.
|
|
1085
|
+
* @returns The version string extracted from the manifest.
|
|
1086
|
+
*/
|
|
627
1087
|
getVersion(cwd) {
|
|
628
1088
|
const filePath = path.join(cwd, this.manifestFile);
|
|
629
1089
|
if (!fs.existsSync(filePath)) {
|
|
@@ -634,7 +1094,7 @@ class YamlVersionSource {
|
|
|
634
1094
|
if (!parsed || typeof parsed !== "object") {
|
|
635
1095
|
throw new Error(`Failed to parse ${this.manifestFile}`);
|
|
636
1096
|
}
|
|
637
|
-
const version = parsed
|
|
1097
|
+
const version = getNestedValue(parsed, this.versionKey);
|
|
638
1098
|
if (typeof version !== "string" || version.length === 0) {
|
|
639
1099
|
if (typeof version === "number") {
|
|
640
1100
|
return String(version);
|
|
@@ -643,13 +1103,21 @@ class YamlVersionSource {
|
|
|
643
1103
|
}
|
|
644
1104
|
return version;
|
|
645
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Writes a version string to the YAML manifest, preserving formatting.
|
|
1108
|
+
*
|
|
1109
|
+
* @param version - Version string to write.
|
|
1110
|
+
* @param cwd - Project directory containing the manifest.
|
|
1111
|
+
*/
|
|
646
1112
|
setVersion(version, cwd) {
|
|
647
1113
|
const filePath = path.join(cwd, this.manifestFile);
|
|
648
1114
|
if (!fs.existsSync(filePath)) {
|
|
649
1115
|
throw new Error(`${this.manifestFile} not found in ${cwd}`);
|
|
650
1116
|
}
|
|
1117
|
+
const keyParts = this.versionKey.split(".");
|
|
1118
|
+
const leafKey = keyParts[keyParts.length - 1];
|
|
651
1119
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
652
|
-
const regex = new RegExp(`^(
|
|
1120
|
+
const regex = new RegExp(`^(\\s*${escapeRegExp(leafKey)}:\\s*)(["']?)(.+?)\\2\\s*$`, "m");
|
|
653
1121
|
const updated = content.replace(regex, `$1$2${version}$2`);
|
|
654
1122
|
if (updated === content) {
|
|
655
1123
|
throw new Error(`Could not find version field to update in ${this.manifestFile}`);
|
|
@@ -657,9 +1125,6 @@ class YamlVersionSource {
|
|
|
657
1125
|
fs.writeFileSync(filePath, updated, "utf-8");
|
|
658
1126
|
}
|
|
659
1127
|
}
|
|
660
|
-
function escapeRegExp(value) {
|
|
661
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
662
|
-
}
|
|
663
1128
|
const VALID_SOURCES = /* @__PURE__ */ new Set([
|
|
664
1129
|
"auto",
|
|
665
1130
|
"package.json",
|
|
@@ -765,7 +1230,10 @@ function resolveVersionSource(config, cwd = process.cwd()) {
|
|
|
765
1230
|
return provider;
|
|
766
1231
|
}
|
|
767
1232
|
}
|
|
768
|
-
|
|
1233
|
+
const supported = DETECTION_TABLE.map((e) => e.file).join(", ");
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`No supported manifest file found in ${cwd}. Looked for: ${supported}. Set manifest.source explicitly in .versionguard.yml or create a supported manifest file.`
|
|
1236
|
+
);
|
|
769
1237
|
}
|
|
770
1238
|
function detectManifests(cwd = process.cwd()) {
|
|
771
1239
|
const detected = [];
|
|
@@ -1059,6 +1527,169 @@ function checkHardcodedVersions(expectedVersion, config, ignorePatterns, cwd = p
|
|
|
1059
1527
|
}
|
|
1060
1528
|
return mismatches;
|
|
1061
1529
|
}
|
|
1530
|
+
function getCalVerConfig(config) {
|
|
1531
|
+
if (!config.versioning.calver) {
|
|
1532
|
+
throw new Error('CalVer configuration is required when versioning.type is "calver"');
|
|
1533
|
+
}
|
|
1534
|
+
return config.versioning.calver;
|
|
1535
|
+
}
|
|
1536
|
+
function deriveTopicSlug(conceptName) {
|
|
1537
|
+
return conceptName.replace(/Config$/, "").replace(/Result$/, "").replace(/Options$/, "").toLowerCase();
|
|
1538
|
+
}
|
|
1539
|
+
function isTopicConcept(name) {
|
|
1540
|
+
return name.endsWith("Config") && name !== "VersionGuardConfig";
|
|
1541
|
+
}
|
|
1542
|
+
function operationMatchesTopic(op, topicSlug, conceptNames) {
|
|
1543
|
+
const haystack = `${op.name} ${op.what}`.toLowerCase();
|
|
1544
|
+
if (haystack.includes(topicSlug)) return true;
|
|
1545
|
+
return conceptNames.some((n) => haystack.includes(n.toLowerCase()));
|
|
1546
|
+
}
|
|
1547
|
+
function createCkmEngine(manifest) {
|
|
1548
|
+
const topics = deriveTopics(manifest);
|
|
1549
|
+
return {
|
|
1550
|
+
topics,
|
|
1551
|
+
getTopicIndex: (toolName = "tool") => formatTopicIndex(topics, toolName),
|
|
1552
|
+
getTopicContent: (name) => formatTopicContent(topics, name),
|
|
1553
|
+
getTopicJson: (name) => buildTopicJson(topics, manifest, name),
|
|
1554
|
+
getManifest: () => manifest
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function deriveTopics(manifest) {
|
|
1558
|
+
const topics = [];
|
|
1559
|
+
for (const concept of manifest.concepts) {
|
|
1560
|
+
if (!isTopicConcept(concept.name)) continue;
|
|
1561
|
+
const slug = deriveTopicSlug(concept.name);
|
|
1562
|
+
const conceptNames = [concept.name];
|
|
1563
|
+
const relatedConcepts = manifest.concepts.filter(
|
|
1564
|
+
(c) => c.name !== concept.name && (c.name.toLowerCase().includes(slug) || slug.includes(deriveTopicSlug(c.name)))
|
|
1565
|
+
);
|
|
1566
|
+
conceptNames.push(...relatedConcepts.map((c) => c.name));
|
|
1567
|
+
const operations = manifest.operations.filter(
|
|
1568
|
+
(op) => operationMatchesTopic(op, slug, conceptNames)
|
|
1569
|
+
);
|
|
1570
|
+
const configSchema = manifest.configSchema.filter(
|
|
1571
|
+
(c) => conceptNames.some((n) => c.key?.startsWith(n))
|
|
1572
|
+
);
|
|
1573
|
+
const constraints = manifest.constraints.filter(
|
|
1574
|
+
(c) => conceptNames.some((n) => c.enforcedBy?.includes(n)) || operations.some((o) => c.enforcedBy?.includes(o.name))
|
|
1575
|
+
);
|
|
1576
|
+
topics.push({
|
|
1577
|
+
name: slug,
|
|
1578
|
+
summary: concept.what,
|
|
1579
|
+
concepts: [concept, ...relatedConcepts],
|
|
1580
|
+
operations,
|
|
1581
|
+
configSchema,
|
|
1582
|
+
constraints
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
return topics;
|
|
1586
|
+
}
|
|
1587
|
+
function formatTopicIndex(topics, toolName) {
|
|
1588
|
+
const lines = [
|
|
1589
|
+
`${toolName} CKM — Codebase Knowledge Manifest`,
|
|
1590
|
+
"",
|
|
1591
|
+
`Usage: ${toolName} ckm [topic] [--json] [--llm]`,
|
|
1592
|
+
"",
|
|
1593
|
+
"Topics:"
|
|
1594
|
+
];
|
|
1595
|
+
const maxName = Math.max(...topics.map((t) => t.name.length));
|
|
1596
|
+
for (const topic of topics) {
|
|
1597
|
+
lines.push(` ${topic.name.padEnd(maxName + 2)}${topic.summary}`);
|
|
1598
|
+
}
|
|
1599
|
+
lines.push("");
|
|
1600
|
+
lines.push("Flags:");
|
|
1601
|
+
lines.push(" --json Machine-readable CKM output (concepts, operations, config schema)");
|
|
1602
|
+
lines.push(" --llm Full API context for LLM agents (forge-ts llms.txt)");
|
|
1603
|
+
return lines.join("\n");
|
|
1604
|
+
}
|
|
1605
|
+
function formatTopicContent(topics, topicName) {
|
|
1606
|
+
const topic = topics.find((t) => t.name === topicName);
|
|
1607
|
+
if (!topic) return null;
|
|
1608
|
+
const lines = [`# ${topic.summary}`, ""];
|
|
1609
|
+
if (topic.concepts.length > 0) {
|
|
1610
|
+
lines.push("## Concepts", "");
|
|
1611
|
+
for (const c of topic.concepts) {
|
|
1612
|
+
lines.push(` ${c.name} — ${c.what}`);
|
|
1613
|
+
if (c.properties) {
|
|
1614
|
+
for (const p of c.properties) {
|
|
1615
|
+
const def = findDefault(topic.configSchema, c.name, p.name);
|
|
1616
|
+
lines.push(` ${p.name}: ${p.type}${def ? ` = ${def}` : ""}`);
|
|
1617
|
+
if (p.description) {
|
|
1618
|
+
lines.push(` ${p.description}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
lines.push("");
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (topic.operations.length > 0) {
|
|
1626
|
+
lines.push("## Operations", "");
|
|
1627
|
+
for (const o of topic.operations) {
|
|
1628
|
+
lines.push(` ${o.name}() — ${o.what}`);
|
|
1629
|
+
if (o.inputs) {
|
|
1630
|
+
for (const i of o.inputs) {
|
|
1631
|
+
lines.push(` @param ${i.name}: ${i.description}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
lines.push("");
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
if (topic.configSchema.length > 0) {
|
|
1638
|
+
lines.push("## Config Fields", "");
|
|
1639
|
+
for (const c of topic.configSchema) {
|
|
1640
|
+
lines.push(` ${c.key}: ${c.type}${c.default ? ` = ${c.default}` : ""}`);
|
|
1641
|
+
if (c.description) {
|
|
1642
|
+
lines.push(` ${c.description}`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
lines.push("");
|
|
1646
|
+
}
|
|
1647
|
+
if (topic.constraints.length > 0) {
|
|
1648
|
+
lines.push("## Constraints", "");
|
|
1649
|
+
for (const c of topic.constraints) {
|
|
1650
|
+
lines.push(` [${c.id}] ${c.rule}`);
|
|
1651
|
+
lines.push(` Enforced by: ${c.enforcedBy}`);
|
|
1652
|
+
}
|
|
1653
|
+
lines.push("");
|
|
1654
|
+
}
|
|
1655
|
+
return lines.join("\n");
|
|
1656
|
+
}
|
|
1657
|
+
function findDefault(schema, conceptName, propName) {
|
|
1658
|
+
return schema.find((c) => c.key === `${conceptName}.${propName}`)?.default;
|
|
1659
|
+
}
|
|
1660
|
+
function buildTopicJson(topics, manifest, topicName) {
|
|
1661
|
+
if (!topicName) {
|
|
1662
|
+
return {
|
|
1663
|
+
topics: topics.map((t) => ({
|
|
1664
|
+
name: t.name,
|
|
1665
|
+
summary: t.summary,
|
|
1666
|
+
concepts: t.concepts.length,
|
|
1667
|
+
operations: t.operations.length,
|
|
1668
|
+
configFields: t.configSchema.length,
|
|
1669
|
+
constraints: t.constraints.length
|
|
1670
|
+
})),
|
|
1671
|
+
ckm: {
|
|
1672
|
+
concepts: manifest.concepts.length,
|
|
1673
|
+
operations: manifest.operations.length,
|
|
1674
|
+
constraints: manifest.constraints.length,
|
|
1675
|
+
workflows: manifest.workflows.length,
|
|
1676
|
+
configSchema: manifest.configSchema.length
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const topic = topics.find((t) => t.name === topicName);
|
|
1681
|
+
if (!topic) {
|
|
1682
|
+
return { error: `Unknown topic: ${topicName}`, topics: topics.map((t) => t.name) };
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
topic: topic.name,
|
|
1686
|
+
summary: topic.summary,
|
|
1687
|
+
concepts: topic.concepts,
|
|
1688
|
+
operations: topic.operations,
|
|
1689
|
+
configSchema: topic.configSchema,
|
|
1690
|
+
constraints: topic.constraints
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1062
1693
|
const CONFIG_FILE_NAMES = [
|
|
1063
1694
|
".versionguard.yml",
|
|
1064
1695
|
".versionguard.yaml",
|
|
@@ -1068,9 +1699,14 @@ const CONFIG_FILE_NAMES = [
|
|
|
1068
1699
|
const DEFAULT_CONFIG = {
|
|
1069
1700
|
versioning: {
|
|
1070
1701
|
type: "semver",
|
|
1702
|
+
schemeRules: {
|
|
1703
|
+
maxNumericSegments: 3,
|
|
1704
|
+
allowedModifiers: ["dev", "alpha", "beta", "rc"]
|
|
1705
|
+
},
|
|
1071
1706
|
calver: {
|
|
1072
1707
|
format: "YYYY.MM.PATCH",
|
|
1073
|
-
preventFutureDates: true
|
|
1708
|
+
preventFutureDates: true,
|
|
1709
|
+
strictMutualExclusion: true
|
|
1074
1710
|
}
|
|
1075
1711
|
},
|
|
1076
1712
|
manifest: {
|
|
@@ -1196,13 +1832,7 @@ function getVersionFeedback(version, config, previousVersion) {
|
|
|
1196
1832
|
if (config.versioning.type === "semver") {
|
|
1197
1833
|
return getSemVerFeedback(version, previousVersion);
|
|
1198
1834
|
}
|
|
1199
|
-
return getCalVerFeedback(version, getCalVerConfig
|
|
1200
|
-
}
|
|
1201
|
-
function getCalVerConfig$2(config) {
|
|
1202
|
-
if (!config.versioning.calver) {
|
|
1203
|
-
throw new Error('CalVer configuration is required when versioning.type is "calver"');
|
|
1204
|
-
}
|
|
1205
|
-
return config.versioning.calver;
|
|
1835
|
+
return getCalVerFeedback(version, getCalVerConfig(config), previousVersion);
|
|
1206
1836
|
}
|
|
1207
1837
|
function getSemVerFeedback(version, previousVersion) {
|
|
1208
1838
|
const errors = [];
|
|
@@ -1218,7 +1848,7 @@ function getSemVerFeedback(version, previousVersion) {
|
|
|
1218
1848
|
});
|
|
1219
1849
|
suggestions.push({
|
|
1220
1850
|
message: `Remove the 'v' prefix`,
|
|
1221
|
-
fix: `
|
|
1851
|
+
fix: `npx versionguard fix --version ${cleanVersion}`,
|
|
1222
1852
|
autoFixable: true
|
|
1223
1853
|
});
|
|
1224
1854
|
} else if (version.split(".").length === 2) {
|
|
@@ -1228,7 +1858,7 @@ function getSemVerFeedback(version, previousVersion) {
|
|
|
1228
1858
|
});
|
|
1229
1859
|
suggestions.push({
|
|
1230
1860
|
message: `Add patch number (e.g., ${version}.0)`,
|
|
1231
|
-
fix: `
|
|
1861
|
+
fix: `npx versionguard fix --version ${version}.0`,
|
|
1232
1862
|
autoFixable: true
|
|
1233
1863
|
});
|
|
1234
1864
|
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(version)) {
|
|
@@ -1278,7 +1908,7 @@ function getSemVerFeedback(version, previousVersion) {
|
|
|
1278
1908
|
});
|
|
1279
1909
|
suggestions.push({
|
|
1280
1910
|
message: `Version must be greater than ${previousVersion}`,
|
|
1281
|
-
fix: `
|
|
1911
|
+
fix: `npx versionguard fix --version ${increment(previousVersion, "patch")}`,
|
|
1282
1912
|
autoFixable: true
|
|
1283
1913
|
});
|
|
1284
1914
|
} else if (comparison === 0) {
|
|
@@ -1288,7 +1918,7 @@ function getSemVerFeedback(version, previousVersion) {
|
|
|
1288
1918
|
});
|
|
1289
1919
|
suggestions.push({
|
|
1290
1920
|
message: `Bump the version`,
|
|
1291
|
-
fix: `
|
|
1921
|
+
fix: `npx versionguard fix --version ${increment(previousVersion, "patch")}`,
|
|
1292
1922
|
autoFixable: true
|
|
1293
1923
|
});
|
|
1294
1924
|
} else {
|
|
@@ -1358,21 +1988,21 @@ function getCalVerFeedback(version, calverConfig, previousVersion) {
|
|
|
1358
1988
|
if (preventFutureDates && parsed.year > now.getFullYear()) {
|
|
1359
1989
|
suggestions.push({
|
|
1360
1990
|
message: `Use current year (${now.getFullYear()}) or a past year`,
|
|
1361
|
-
fix: `
|
|
1991
|
+
fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, year: now.getFullYear() })}`,
|
|
1362
1992
|
autoFixable: true
|
|
1363
1993
|
});
|
|
1364
1994
|
}
|
|
1365
1995
|
if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month > now.getMonth() + 1) {
|
|
1366
1996
|
suggestions.push({
|
|
1367
1997
|
message: `Current month is ${now.getMonth() + 1}`,
|
|
1368
|
-
fix: `
|
|
1998
|
+
fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, month: now.getMonth() + 1 })}`,
|
|
1369
1999
|
autoFixable: true
|
|
1370
2000
|
});
|
|
1371
2001
|
}
|
|
1372
2002
|
if (preventFutureDates && parsed.year === now.getFullYear() && parsed.month === now.getMonth() + 1 && parsed.day !== void 0 && parsed.day > now.getDate()) {
|
|
1373
2003
|
suggestions.push({
|
|
1374
2004
|
message: `Current day is ${now.getDate()}`,
|
|
1375
|
-
fix: `
|
|
2005
|
+
fix: `npx versionguard fix --version ${formatCalVerVersion({ ...parsed, day: now.getDate() })}`,
|
|
1376
2006
|
autoFixable: true
|
|
1377
2007
|
});
|
|
1378
2008
|
}
|
|
@@ -1386,7 +2016,7 @@ function getCalVerFeedback(version, calverConfig, previousVersion) {
|
|
|
1386
2016
|
});
|
|
1387
2017
|
suggestions.push({
|
|
1388
2018
|
message: `CalVer must increase over time`,
|
|
1389
|
-
fix: `
|
|
2019
|
+
fix: `npx versionguard fix --version ${increment$1(previousVersion, format2)}`,
|
|
1390
2020
|
autoFixable: true
|
|
1391
2021
|
});
|
|
1392
2022
|
}
|
|
@@ -1566,6 +2196,17 @@ function fixAll(config, targetVersion, cwd = process.cwd()) {
|
|
|
1566
2196
|
const syncResults = fixSyncIssues(config, cwd);
|
|
1567
2197
|
results.push(...syncResults);
|
|
1568
2198
|
if (config.changelog.enabled) {
|
|
2199
|
+
const changelogPath = path.join(cwd, config.changelog.file);
|
|
2200
|
+
if (isChangesetMangled(changelogPath)) {
|
|
2201
|
+
const fixed = fixChangesetMangling(changelogPath);
|
|
2202
|
+
if (fixed) {
|
|
2203
|
+
results.push({
|
|
2204
|
+
fixed: true,
|
|
2205
|
+
message: `Restructured ${config.changelog.file} from Changesets format to Keep a Changelog`,
|
|
2206
|
+
file: changelogPath
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
1569
2210
|
const changelogResult = fixChangelog(version, config, cwd);
|
|
1570
2211
|
if (changelogResult.fixed) {
|
|
1571
2212
|
results.push(changelogResult);
|
|
@@ -1595,7 +2236,7 @@ function suggestNextVersion(currentVersion, config, changeType) {
|
|
|
1595
2236
|
});
|
|
1596
2237
|
}
|
|
1597
2238
|
} else {
|
|
1598
|
-
const format2 = getCalVerConfig
|
|
2239
|
+
const format2 = getCalVerConfig(config).format;
|
|
1599
2240
|
const currentCal = getCurrentVersion(format2);
|
|
1600
2241
|
suggestions.push({
|
|
1601
2242
|
version: currentCal,
|
|
@@ -1608,12 +2249,6 @@ function suggestNextVersion(currentVersion, config, changeType) {
|
|
|
1608
2249
|
}
|
|
1609
2250
|
return suggestions;
|
|
1610
2251
|
}
|
|
1611
|
-
function getCalVerConfig$1(config) {
|
|
1612
|
-
if (!config.versioning.calver) {
|
|
1613
|
-
throw new Error('CalVer configuration is required when versioning.type is "calver"');
|
|
1614
|
-
}
|
|
1615
|
-
return config.versioning.calver;
|
|
1616
|
-
}
|
|
1617
2252
|
const HOOK_NAMES = ["pre-commit", "pre-push", "post-tag"];
|
|
1618
2253
|
function checkHooksPathOverride(cwd) {
|
|
1619
2254
|
try {
|
|
@@ -1730,6 +2365,92 @@ function runGuardChecks(config, cwd) {
|
|
|
1730
2365
|
warnings
|
|
1731
2366
|
};
|
|
1732
2367
|
}
|
|
2368
|
+
const PROJECT_MARKERS = [
|
|
2369
|
+
".versionguard.yml",
|
|
2370
|
+
".versionguard.yaml",
|
|
2371
|
+
"versionguard.yml",
|
|
2372
|
+
"versionguard.yaml",
|
|
2373
|
+
".git",
|
|
2374
|
+
"package.json",
|
|
2375
|
+
"Cargo.toml",
|
|
2376
|
+
"pyproject.toml",
|
|
2377
|
+
"pubspec.yaml",
|
|
2378
|
+
"composer.json",
|
|
2379
|
+
"pom.xml",
|
|
2380
|
+
"go.mod",
|
|
2381
|
+
"mix.exs",
|
|
2382
|
+
"Gemfile",
|
|
2383
|
+
".csproj"
|
|
2384
|
+
];
|
|
2385
|
+
function findProjectRoot(startDir) {
|
|
2386
|
+
let current = path.resolve(startDir);
|
|
2387
|
+
while (true) {
|
|
2388
|
+
for (const marker of PROJECT_MARKERS) {
|
|
2389
|
+
if (marker.startsWith(".") && marker !== ".git" && !marker.startsWith(".version")) {
|
|
2390
|
+
try {
|
|
2391
|
+
const files = fs.readdirSync(current);
|
|
2392
|
+
if (files.some((f) => f.endsWith(marker))) {
|
|
2393
|
+
return buildResult(current, marker);
|
|
2394
|
+
}
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
} else if (fs.existsSync(path.join(current, marker))) {
|
|
2398
|
+
return buildResult(current, marker);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
const parent = path.dirname(current);
|
|
2402
|
+
if (parent === current) {
|
|
2403
|
+
return {
|
|
2404
|
+
found: false,
|
|
2405
|
+
root: path.resolve(startDir),
|
|
2406
|
+
hasConfig: false,
|
|
2407
|
+
hasGit: false,
|
|
2408
|
+
hasManifest: false
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
current = parent;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function buildResult(root, marker) {
|
|
2415
|
+
const configNames = [
|
|
2416
|
+
".versionguard.yml",
|
|
2417
|
+
".versionguard.yaml",
|
|
2418
|
+
"versionguard.yml",
|
|
2419
|
+
"versionguard.yaml"
|
|
2420
|
+
];
|
|
2421
|
+
return {
|
|
2422
|
+
found: true,
|
|
2423
|
+
root,
|
|
2424
|
+
marker,
|
|
2425
|
+
hasConfig: configNames.some((c) => fs.existsSync(path.join(root, c))),
|
|
2426
|
+
hasGit: fs.existsSync(path.join(root, ".git")),
|
|
2427
|
+
hasManifest: [
|
|
2428
|
+
"package.json",
|
|
2429
|
+
"Cargo.toml",
|
|
2430
|
+
"pyproject.toml",
|
|
2431
|
+
"pubspec.yaml",
|
|
2432
|
+
"composer.json",
|
|
2433
|
+
"pom.xml",
|
|
2434
|
+
"VERSION"
|
|
2435
|
+
].some((m) => fs.existsSync(path.join(root, m)))
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
function formatNotProjectError(cwd, command) {
|
|
2439
|
+
const dir = path.basename(cwd) || cwd;
|
|
2440
|
+
const lines = [
|
|
2441
|
+
`Not a VersionGuard project: ${dir}`,
|
|
2442
|
+
"",
|
|
2443
|
+
"No .versionguard.yml, .git directory, or manifest file found.",
|
|
2444
|
+
"",
|
|
2445
|
+
"To get started:",
|
|
2446
|
+
" versionguard init Set up a new project interactively",
|
|
2447
|
+
" versionguard init --yes Set up with defaults",
|
|
2448
|
+
"",
|
|
2449
|
+
"Or run from a project root directory:",
|
|
2450
|
+
` cd /path/to/project && versionguard ${command}`
|
|
2451
|
+
];
|
|
2452
|
+
return lines.join("\n");
|
|
2453
|
+
}
|
|
1733
2454
|
function runGit(cwd, args, encoding) {
|
|
1734
2455
|
return childProcess.execFileSync("git", args, {
|
|
1735
2456
|
cwd,
|
|
@@ -1897,7 +2618,8 @@ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false
|
|
|
1897
2618
|
const versionResult = config.versioning.type === "semver" ? validate$1(version) : validate$2(
|
|
1898
2619
|
version,
|
|
1899
2620
|
config.versioning.calver?.format ?? "YYYY.MM.PATCH",
|
|
1900
|
-
config.versioning.calver?.preventFutureDates ?? true
|
|
2621
|
+
config.versioning.calver?.preventFutureDates ?? true,
|
|
2622
|
+
config.versioning.schemeRules
|
|
1901
2623
|
);
|
|
1902
2624
|
if (!versionResult.valid) {
|
|
1903
2625
|
return versionResult.errors[0]?.message ?? `Invalid version: ${version}`;
|
|
@@ -1981,7 +2703,12 @@ function validateVersion(version, config) {
|
|
|
1981
2703
|
return validate$1(version);
|
|
1982
2704
|
}
|
|
1983
2705
|
const calverConfig = getCalVerConfig(config);
|
|
1984
|
-
return validate$2(
|
|
2706
|
+
return validate$2(
|
|
2707
|
+
version,
|
|
2708
|
+
calverConfig.format,
|
|
2709
|
+
calverConfig.preventFutureDates,
|
|
2710
|
+
config.versioning.schemeRules
|
|
2711
|
+
);
|
|
1985
2712
|
}
|
|
1986
2713
|
function validate(config, cwd = process.cwd()) {
|
|
1987
2714
|
const errors = [];
|
|
@@ -2093,12 +2820,6 @@ function canBump(currentVersion, newVersion, config) {
|
|
|
2093
2820
|
}
|
|
2094
2821
|
return { canBump: true };
|
|
2095
2822
|
}
|
|
2096
|
-
function getCalVerConfig(config) {
|
|
2097
|
-
if (!config.versioning.calver) {
|
|
2098
|
-
throw new Error('CalVer configuration is required when versioning.type is "calver"');
|
|
2099
|
-
}
|
|
2100
|
-
return config.versioning.calver;
|
|
2101
|
-
}
|
|
2102
2823
|
function isWorktreeClean(cwd) {
|
|
2103
2824
|
try {
|
|
2104
2825
|
return execSync("git status --porcelain", { cwd, encoding: "utf-8" }).trim().length === 0;
|
|
@@ -2107,51 +2828,58 @@ function isWorktreeClean(cwd) {
|
|
|
2107
2828
|
}
|
|
2108
2829
|
}
|
|
2109
2830
|
export {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2831
|
+
checkHardcodedVersions as A,
|
|
2832
|
+
checkHookIntegrity as B,
|
|
2833
|
+
checkHooksPathOverride as C,
|
|
2834
|
+
checkHuskyBypass as D,
|
|
2835
|
+
detectManifests as E,
|
|
2836
|
+
fixChangelog as F,
|
|
2116
2837
|
GitTagSource as G,
|
|
2117
|
-
|
|
2118
|
-
|
|
2838
|
+
fixPackageVersion as H,
|
|
2839
|
+
getAllTags as I,
|
|
2119
2840
|
JsonVersionSource as J,
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2841
|
+
getCalVerConfig as K,
|
|
2842
|
+
getLatestTag as L,
|
|
2843
|
+
getTagFeedback as M,
|
|
2844
|
+
getVersionSource as N,
|
|
2845
|
+
initConfig as O,
|
|
2846
|
+
resolveVersionSource as P,
|
|
2847
|
+
semver as Q,
|
|
2126
2848
|
RegexVersionSource as R,
|
|
2849
|
+
suggestTagMessage as S,
|
|
2127
2850
|
TomlVersionSource as T,
|
|
2851
|
+
sync as U,
|
|
2128
2852
|
VersionFileSource as V,
|
|
2853
|
+
syncVersion as W,
|
|
2854
|
+
validateChangelog as X,
|
|
2129
2855
|
YamlVersionSource as Y,
|
|
2856
|
+
validateTagForPush as Z,
|
|
2857
|
+
validateVersion as _,
|
|
2130
2858
|
installHooks as a,
|
|
2131
2859
|
getPackageVersion as b,
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2860
|
+
createCkmEngine as c,
|
|
2861
|
+
getVersionFeedback as d,
|
|
2862
|
+
getSyncFeedback as e,
|
|
2863
|
+
getChangelogFeedback as f,
|
|
2136
2864
|
getConfig as g,
|
|
2137
2865
|
handlePostTag as h,
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2866
|
+
isValidCalVerFormat as i,
|
|
2867
|
+
doctor as j,
|
|
2868
|
+
fixAll as k,
|
|
2869
|
+
isChangesetMangled as l,
|
|
2870
|
+
fixChangesetMangling as m,
|
|
2871
|
+
fixSyncIssues as n,
|
|
2872
|
+
setPackageVersion as o,
|
|
2873
|
+
createTag as p,
|
|
2874
|
+
areHooksInstalled as q,
|
|
2147
2875
|
runGuardChecks as r,
|
|
2148
2876
|
suggestNextVersion as s,
|
|
2149
|
-
|
|
2877
|
+
findProjectRoot as t,
|
|
2150
2878
|
uninstallHooks as u,
|
|
2151
2879
|
validate as v,
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2880
|
+
formatNotProjectError as w,
|
|
2881
|
+
calver as x,
|
|
2882
|
+
canBump as y,
|
|
2883
|
+
checkEnforceHooksPolicy as z
|
|
2156
2884
|
};
|
|
2157
|
-
//# sourceMappingURL=index-
|
|
2885
|
+
//# sourceMappingURL=index-CwOyEn5L.js.map
|