@codluv/versionguard 0.4.0 → 0.6.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.
@@ -6,6 +6,17 @@ 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
+ 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
+ }
9
20
  const VALID_TOKENS = /* @__PURE__ */ new Set([
10
21
  "YYYY",
11
22
  "YY",
@@ -200,13 +211,10 @@ function validate$2(version, calverFormat, preventFutureDates = true, schemeRule
200
211
  });
201
212
  }
202
213
  }
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
- });
214
+ if (parsed.modifier) {
215
+ const modifierError = validateModifier(parsed.modifier, schemeRules);
216
+ if (modifierError) {
217
+ errors.push(modifierError);
210
218
  }
211
219
  }
212
220
  if (schemeRules?.maxNumericSegments) {
@@ -381,10 +389,109 @@ function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Dat
381
389
  const updated = `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
382
390
  fs.writeFileSync(changelogPath, updated, "utf-8");
383
391
  }
392
+ function isChangesetMangled(changelogPath) {
393
+ if (!fs.existsSync(changelogPath)) return false;
394
+ const content = fs.readFileSync(changelogPath, "utf-8");
395
+ return /^## \d+\.\d+/m.test(content) && content.includes("## [Unreleased]");
396
+ }
397
+ const SECTION_MAP = {
398
+ "Major Changes": "Changed",
399
+ "Minor Changes": "Added",
400
+ "Patch Changes": "Fixed"
401
+ };
402
+ function fixChangesetMangling(changelogPath, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
403
+ if (!fs.existsSync(changelogPath)) return false;
404
+ const content = fs.readFileSync(changelogPath, "utf-8");
405
+ const versionMatch = content.match(/^## (\d+\.\d+\.\d+[^\n]*)\n/m);
406
+ if (!versionMatch || versionMatch.index === void 0) return false;
407
+ const fullHeader = versionMatch[0];
408
+ if (fullHeader.includes("[")) return false;
409
+ const version = versionMatch[1].trim();
410
+ if (content.includes(`## [${version}]`)) return false;
411
+ const startIndex = versionMatch.index;
412
+ const preambleMatch = content.indexOf("All notable changes", startIndex);
413
+ const unreleasedMatch = content.indexOf("## [Unreleased]", startIndex);
414
+ let endIndex;
415
+ if (preambleMatch !== -1 && preambleMatch < unreleasedMatch) {
416
+ endIndex = preambleMatch;
417
+ } else if (unreleasedMatch !== -1) {
418
+ endIndex = unreleasedMatch;
419
+ } else {
420
+ return false;
421
+ }
422
+ const changesetsBlock = content.slice(startIndex + fullHeader.length, endIndex).trim();
423
+ const transformedSections = transformChangesetsContent(changesetsBlock);
424
+ const newEntry = `## [${version}] - ${date}
425
+
426
+ ${transformedSections}
427
+
428
+ `;
429
+ const beforeChangesets = content.slice(0, startIndex);
430
+ const afterChangesets = content.slice(endIndex);
431
+ const unreleasedInAfter = afterChangesets.indexOf("## [Unreleased]");
432
+ if (unreleasedInAfter === -1) {
433
+ const rebuilt2 = `${beforeChangesets}${newEntry}${afterChangesets}`;
434
+ fs.writeFileSync(changelogPath, rebuilt2, "utf-8");
435
+ return true;
436
+ }
437
+ const unreleasedLineEnd = afterChangesets.indexOf("\n", unreleasedInAfter);
438
+ const afterUnreleased = unreleasedLineEnd !== -1 ? afterChangesets.slice(0, unreleasedLineEnd + 1) : afterChangesets;
439
+ const rest = unreleasedLineEnd !== -1 ? afterChangesets.slice(unreleasedLineEnd + 1) : "";
440
+ const rebuilt = `${beforeChangesets}${afterUnreleased}
441
+ ${newEntry}${rest}`;
442
+ const withLinks = updateCompareLinks(rebuilt, version);
443
+ fs.writeFileSync(changelogPath, withLinks, "utf-8");
444
+ return true;
445
+ }
446
+ function transformChangesetsContent(block) {
447
+ const lines = block.split("\n");
448
+ const result = [];
449
+ for (const line of lines) {
450
+ const sectionMatch = line.match(/^### (.+)/);
451
+ if (sectionMatch) {
452
+ const mapped = SECTION_MAP[sectionMatch[1]] ?? sectionMatch[1];
453
+ result.push(`### ${mapped}`);
454
+ continue;
455
+ }
456
+ const entryMatch = line.match(
457
+ /^(\s*-\s+)[a-f0-9]{7,}: (?:feat|fix|chore|docs|refactor|perf|test|ci|build|style)(?:\([^)]*\))?: (.+)/
458
+ );
459
+ if (entryMatch) {
460
+ result.push(`${entryMatch[1]}${entryMatch[2]}`);
461
+ continue;
462
+ }
463
+ const simpleHashMatch = line.match(/^(\s*-\s+)[a-f0-9]{7,}: (.+)/);
464
+ if (simpleHashMatch) {
465
+ result.push(`${simpleHashMatch[1]}${simpleHashMatch[2]}`);
466
+ continue;
467
+ }
468
+ result.push(line);
469
+ }
470
+ return result.join("\n");
471
+ }
472
+ function updateCompareLinks(content, version) {
473
+ const unreleasedLinkRegex = /\[Unreleased\]: (https:\/\/[^\s]+\/compare\/v)([\d.]+)(\.\.\.HEAD)/;
474
+ const match = content.match(unreleasedLinkRegex);
475
+ if (match) {
476
+ const baseUrl = match[1].replace(/v$/, "");
477
+ const previousVersion = match[2];
478
+ const newUnreleasedLink = `[Unreleased]: ${baseUrl}v${version}...HEAD`;
479
+ const newVersionLink = `[${version}]: ${baseUrl}v${previousVersion}...v${version}`;
480
+ let updated = content.replace(unreleasedLinkRegex, newUnreleasedLink);
481
+ if (!updated.includes(`[${version}]:`)) {
482
+ updated = updated.replace(newUnreleasedLink, `${newUnreleasedLink}
483
+ ${newVersionLink}`);
484
+ }
485
+ return updated;
486
+ }
487
+ return content;
488
+ }
384
489
  function escapeRegExp$1(value) {
385
490
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
386
491
  }
387
492
  const HOOK_NAMES$1 = ["pre-commit", "pre-push", "post-tag"];
493
+ const VG_BLOCK_START = "# >>> versionguard >>>";
494
+ const VG_BLOCK_END = "# <<< versionguard <<<";
388
495
  function installHooks(config, cwd = process.cwd()) {
389
496
  const gitDir = findGitDir(cwd);
390
497
  if (!gitDir) {
@@ -395,7 +502,36 @@ function installHooks(config, cwd = process.cwd()) {
395
502
  for (const hookName of HOOK_NAMES$1) {
396
503
  if (config.hooks[hookName]) {
397
504
  const hookPath = path.join(hooksDir, hookName);
398
- fs.writeFileSync(hookPath, generateHookScript(hookName), { encoding: "utf-8", mode: 493 });
505
+ const vgBlock = generateHookBlock(hookName);
506
+ if (fs.existsSync(hookPath)) {
507
+ const existing = fs.readFileSync(hookPath, "utf-8");
508
+ if (existing.includes(VG_BLOCK_START)) {
509
+ const updated = replaceVgBlock(existing, vgBlock);
510
+ fs.writeFileSync(hookPath, updated, { encoding: "utf-8", mode: 493 });
511
+ } else if (isLegacyVgHook(existing)) {
512
+ fs.writeFileSync(hookPath, `#!/bin/sh
513
+
514
+ ${vgBlock}
515
+ `, {
516
+ encoding: "utf-8",
517
+ mode: 493
518
+ });
519
+ } else {
520
+ const appended = `${existing.trimEnd()}
521
+
522
+ ${vgBlock}
523
+ `;
524
+ fs.writeFileSync(hookPath, appended, { encoding: "utf-8", mode: 493 });
525
+ }
526
+ } else {
527
+ fs.writeFileSync(hookPath, `#!/bin/sh
528
+
529
+ ${vgBlock}
530
+ `, {
531
+ encoding: "utf-8",
532
+ mode: 493
533
+ });
534
+ }
399
535
  }
400
536
  }
401
537
  }
@@ -407,7 +543,21 @@ function uninstallHooks(cwd = process.cwd()) {
407
543
  const hooksDir = path.join(gitDir, "hooks");
408
544
  for (const hookName of HOOK_NAMES$1) {
409
545
  const hookPath = path.join(hooksDir, hookName);
410
- if (fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard")) {
546
+ if (!fs.existsSync(hookPath)) continue;
547
+ const content = fs.readFileSync(hookPath, "utf-8");
548
+ if (!content.includes("versionguard")) continue;
549
+ if (content.includes(VG_BLOCK_START)) {
550
+ const cleaned = removeVgBlock(content);
551
+ const trimmed = cleaned.trim();
552
+ if (!trimmed || trimmed === "#!/bin/sh") {
553
+ fs.unlinkSync(hookPath);
554
+ } else if (isLegacyVgHook(trimmed)) {
555
+ fs.unlinkSync(hookPath);
556
+ } else {
557
+ fs.writeFileSync(hookPath, `${trimmed}
558
+ `, { encoding: "utf-8", mode: 493 });
559
+ }
560
+ } else if (isLegacyVgHook(content)) {
411
561
  fs.unlinkSync(hookPath);
412
562
  }
413
563
  }
@@ -436,9 +586,8 @@ function areHooksInstalled(cwd = process.cwd()) {
436
586
  return fs.existsSync(hookPath) && fs.readFileSync(hookPath, "utf-8").includes("versionguard");
437
587
  });
438
588
  }
439
- function generateHookScript(hookName) {
440
- return `#!/bin/sh
441
- # versionguard
589
+ function generateHookBlock(hookName) {
590
+ return `${VG_BLOCK_START}
442
591
  # VersionGuard ${hookName} hook
443
592
  # --no-install prevents accidentally downloading an unscoped package
444
593
  # if @codluv/versionguard is not installed locally
@@ -448,8 +597,36 @@ if [ $status -ne 0 ]; then
448
597
  echo "VersionGuard validation failed."
449
598
  exit $status
450
599
  fi
600
+ ${VG_BLOCK_END}`;
601
+ }
602
+ function generateHookScript(hookName) {
603
+ return `#!/bin/sh
604
+
605
+ ${generateHookBlock(hookName)}
451
606
  `;
452
607
  }
608
+ function isLegacyVgHook(content) {
609
+ if (!content.includes("versionguard validate")) return false;
610
+ if (content.includes(VG_BLOCK_START)) return false;
611
+ if (content.includes("husky")) return false;
612
+ if (content.includes("lefthook")) return false;
613
+ if (content.includes("pre-commit run")) return false;
614
+ return true;
615
+ }
616
+ function replaceVgBlock(content, newBlock) {
617
+ const startIdx = content.indexOf(VG_BLOCK_START);
618
+ const endIdx = content.indexOf(VG_BLOCK_END);
619
+ if (startIdx === -1 || endIdx === -1) return content;
620
+ return content.slice(0, startIdx) + newBlock + content.slice(endIdx + VG_BLOCK_END.length);
621
+ }
622
+ function removeVgBlock(content) {
623
+ const startIdx = content.indexOf(VG_BLOCK_START);
624
+ const endIdx = content.indexOf(VG_BLOCK_END);
625
+ if (startIdx === -1 || endIdx === -1) return content;
626
+ const before = content.slice(0, startIdx).replace(/\n\n$/, "\n");
627
+ const after = content.slice(endIdx + VG_BLOCK_END.length).replace(/^\n\n/, "\n");
628
+ return before + after;
629
+ }
453
630
  class GitTagSource {
454
631
  /** Human-readable provider name. */
455
632
  name = "git-tag";
@@ -1167,17 +1344,42 @@ function getStructuralErrors(version) {
1167
1344
  });
1168
1345
  return errors;
1169
1346
  }
1170
- function validate$1(version) {
1171
- const parsed = parse(version);
1347
+ function validate$1(version, semverConfig, schemeRules) {
1348
+ let input = version;
1349
+ if (input.startsWith("v") || input.startsWith("V")) {
1350
+ if (semverConfig?.allowVPrefix) {
1351
+ input = input.slice(1);
1352
+ }
1353
+ }
1354
+ const parsed = parse(input);
1172
1355
  if (!parsed) {
1173
1356
  return {
1174
1357
  valid: false,
1175
1358
  errors: getStructuralErrors(version)
1176
1359
  };
1177
1360
  }
1361
+ const errors = [];
1362
+ if (semverConfig && !semverConfig.allowBuildMetadata && parsed.build.length > 0) {
1363
+ errors.push({
1364
+ message: `Build metadata is not allowed: "${parsed.build.join(".")}"`,
1365
+ severity: "error"
1366
+ });
1367
+ }
1368
+ if (semverConfig?.requirePrerelease && parsed.prerelease.length === 0) {
1369
+ errors.push({
1370
+ message: "A prerelease label is required (e.g., 1.2.3-alpha.1)",
1371
+ severity: "error"
1372
+ });
1373
+ }
1374
+ if (parsed.prerelease.length > 0) {
1375
+ const modifierError = validateModifier(parsed.prerelease[0], schemeRules);
1376
+ if (modifierError) {
1377
+ errors.push(modifierError);
1378
+ }
1379
+ }
1178
1380
  return {
1179
- valid: true,
1180
- errors: [],
1381
+ valid: errors.filter((e) => e.severity === "error").length === 0,
1382
+ errors,
1181
1383
  version: { type: "semver", version: parsed }
1182
1384
  };
1183
1385
  }
@@ -1358,12 +1560,177 @@ function checkHardcodedVersions(expectedVersion, config, ignorePatterns, cwd = p
1358
1560
  }
1359
1561
  return mismatches;
1360
1562
  }
1563
+ const DEFAULT_SEMVER_CONFIG = {
1564
+ allowVPrefix: false,
1565
+ allowBuildMetadata: true,
1566
+ requirePrerelease: false
1567
+ };
1568
+ function getSemVerConfig(config) {
1569
+ return { ...DEFAULT_SEMVER_CONFIG, ...config.versioning.semver };
1570
+ }
1361
1571
  function getCalVerConfig(config) {
1362
1572
  if (!config.versioning.calver) {
1363
1573
  throw new Error('CalVer configuration is required when versioning.type is "calver"');
1364
1574
  }
1365
1575
  return config.versioning.calver;
1366
1576
  }
1577
+ function deriveTopicSlug(conceptName) {
1578
+ return conceptName.replace(/Config$/, "").replace(/Result$/, "").replace(/Options$/, "").toLowerCase();
1579
+ }
1580
+ function isTopicConcept(name) {
1581
+ return name.endsWith("Config") && name !== "VersionGuardConfig";
1582
+ }
1583
+ function operationMatchesTopic(op, topicSlug, conceptNames) {
1584
+ const haystack = `${op.name} ${op.what}`.toLowerCase();
1585
+ if (haystack.includes(topicSlug)) return true;
1586
+ return conceptNames.some((n) => haystack.includes(n.toLowerCase()));
1587
+ }
1588
+ function createCkmEngine(manifest) {
1589
+ const topics = deriveTopics(manifest);
1590
+ return {
1591
+ topics,
1592
+ getTopicIndex: (toolName = "tool") => formatTopicIndex(topics, toolName),
1593
+ getTopicContent: (name) => formatTopicContent(topics, name),
1594
+ getTopicJson: (name) => buildTopicJson(topics, manifest, name),
1595
+ getManifest: () => manifest
1596
+ };
1597
+ }
1598
+ function deriveTopics(manifest) {
1599
+ const topics = [];
1600
+ for (const concept of manifest.concepts) {
1601
+ if (!isTopicConcept(concept.name)) continue;
1602
+ const slug = deriveTopicSlug(concept.name);
1603
+ const conceptNames = [concept.name];
1604
+ const relatedConcepts = manifest.concepts.filter(
1605
+ (c) => c.name !== concept.name && (c.name.toLowerCase().includes(slug) || slug.includes(deriveTopicSlug(c.name)))
1606
+ );
1607
+ conceptNames.push(...relatedConcepts.map((c) => c.name));
1608
+ const operations = manifest.operations.filter(
1609
+ (op) => operationMatchesTopic(op, slug, conceptNames)
1610
+ );
1611
+ const configSchema = manifest.configSchema.filter(
1612
+ (c) => conceptNames.some((n) => c.key?.startsWith(n))
1613
+ );
1614
+ const constraints = manifest.constraints.filter(
1615
+ (c) => conceptNames.some((n) => c.enforcedBy?.includes(n)) || operations.some((o) => c.enforcedBy?.includes(o.name))
1616
+ );
1617
+ topics.push({
1618
+ name: slug,
1619
+ summary: concept.what,
1620
+ concepts: [concept, ...relatedConcepts],
1621
+ operations,
1622
+ configSchema,
1623
+ constraints
1624
+ });
1625
+ }
1626
+ return topics;
1627
+ }
1628
+ function formatTopicIndex(topics, toolName) {
1629
+ const lines = [
1630
+ `${toolName} CKM — Codebase Knowledge Manifest`,
1631
+ "",
1632
+ `Usage: ${toolName} ckm [topic] [--json] [--llm]`,
1633
+ "",
1634
+ "Topics:"
1635
+ ];
1636
+ const maxName = Math.max(...topics.map((t) => t.name.length));
1637
+ for (const topic of topics) {
1638
+ lines.push(` ${topic.name.padEnd(maxName + 2)}${topic.summary}`);
1639
+ }
1640
+ lines.push("");
1641
+ lines.push("Flags:");
1642
+ lines.push(" --json Machine-readable CKM output (concepts, operations, config schema)");
1643
+ lines.push(" --llm Full API context for LLM agents (forge-ts llms.txt)");
1644
+ return lines.join("\n");
1645
+ }
1646
+ function formatTopicContent(topics, topicName) {
1647
+ const topic = topics.find((t) => t.name === topicName);
1648
+ if (!topic) return null;
1649
+ const lines = [`# ${topic.summary}`, ""];
1650
+ if (topic.concepts.length > 0) {
1651
+ lines.push("## Concepts", "");
1652
+ for (const c of topic.concepts) {
1653
+ lines.push(` ${c.name} — ${c.what}`);
1654
+ if (c.properties) {
1655
+ for (const p of c.properties) {
1656
+ const def = findDefault(topic.configSchema, c.name, p.name);
1657
+ lines.push(` ${p.name}: ${p.type}${def ? ` = ${def}` : ""}`);
1658
+ if (p.description) {
1659
+ lines.push(` ${p.description}`);
1660
+ }
1661
+ }
1662
+ }
1663
+ lines.push("");
1664
+ }
1665
+ }
1666
+ if (topic.operations.length > 0) {
1667
+ lines.push("## Operations", "");
1668
+ for (const o of topic.operations) {
1669
+ lines.push(` ${o.name}() — ${o.what}`);
1670
+ if (o.inputs) {
1671
+ for (const i of o.inputs) {
1672
+ lines.push(` @param ${i.name}: ${i.description}`);
1673
+ }
1674
+ }
1675
+ lines.push("");
1676
+ }
1677
+ }
1678
+ if (topic.configSchema.length > 0) {
1679
+ lines.push("## Config Fields", "");
1680
+ for (const c of topic.configSchema) {
1681
+ lines.push(` ${c.key}: ${c.type}${c.default ? ` = ${c.default}` : ""}`);
1682
+ if (c.description) {
1683
+ lines.push(` ${c.description}`);
1684
+ }
1685
+ }
1686
+ lines.push("");
1687
+ }
1688
+ if (topic.constraints.length > 0) {
1689
+ lines.push("## Constraints", "");
1690
+ for (const c of topic.constraints) {
1691
+ lines.push(` [${c.id}] ${c.rule}`);
1692
+ lines.push(` Enforced by: ${c.enforcedBy}`);
1693
+ }
1694
+ lines.push("");
1695
+ }
1696
+ return lines.join("\n");
1697
+ }
1698
+ function findDefault(schema, conceptName, propName) {
1699
+ return schema.find((c) => c.key === `${conceptName}.${propName}`)?.default;
1700
+ }
1701
+ function buildTopicJson(topics, manifest, topicName) {
1702
+ if (!topicName) {
1703
+ return {
1704
+ topics: topics.map((t) => ({
1705
+ name: t.name,
1706
+ summary: t.summary,
1707
+ concepts: t.concepts.length,
1708
+ operations: t.operations.length,
1709
+ configFields: t.configSchema.length,
1710
+ constraints: t.constraints.length
1711
+ })),
1712
+ ckm: {
1713
+ concepts: manifest.concepts.length,
1714
+ operations: manifest.operations.length,
1715
+ constraints: manifest.constraints.length,
1716
+ workflows: manifest.workflows.length,
1717
+ configSchema: manifest.configSchema.length
1718
+ }
1719
+ };
1720
+ }
1721
+ const topic = topics.find((t) => t.name === topicName);
1722
+ if (!topic) {
1723
+ return { error: `Unknown topic: ${topicName}`, topics: topics.map((t) => t.name) };
1724
+ }
1725
+ return {
1726
+ topic: topic.name,
1727
+ summary: topic.summary,
1728
+ concepts: topic.concepts,
1729
+ operations: topic.operations,
1730
+ configSchema: topic.configSchema,
1731
+ constraints: topic.constraints
1732
+ };
1733
+ }
1367
1734
  const CONFIG_FILE_NAMES = [
1368
1735
  ".versionguard.yml",
1369
1736
  ".versionguard.yaml",
@@ -1377,6 +1744,11 @@ const DEFAULT_CONFIG = {
1377
1744
  maxNumericSegments: 3,
1378
1745
  allowedModifiers: ["dev", "alpha", "beta", "rc"]
1379
1746
  },
1747
+ semver: {
1748
+ allowVPrefix: false,
1749
+ allowBuildMetadata: true,
1750
+ requirePrerelease: false
1751
+ },
1380
1752
  calver: {
1381
1753
  format: "YYYY.MM.PATCH",
1382
1754
  preventFutureDates: true,
@@ -1456,8 +1828,16 @@ function initConfig(cwd = process.cwd()) {
1456
1828
  }
1457
1829
  function generateDefaultConfig() {
1458
1830
  return `# VersionGuard Configuration
1831
+ # Change "type" to switch between semver and calver — both blocks are always present.
1459
1832
  versioning:
1460
1833
  type: semver
1834
+ semver:
1835
+ allowVPrefix: false
1836
+ allowBuildMetadata: true
1837
+ requirePrerelease: false
1838
+ calver:
1839
+ format: "YYYY.MM.PATCH"
1840
+ preventFutureDates: true
1461
1841
 
1462
1842
  sync:
1463
1843
  files:
@@ -1870,6 +2250,17 @@ function fixAll(config, targetVersion, cwd = process.cwd()) {
1870
2250
  const syncResults = fixSyncIssues(config, cwd);
1871
2251
  results.push(...syncResults);
1872
2252
  if (config.changelog.enabled) {
2253
+ const changelogPath = path.join(cwd, config.changelog.file);
2254
+ if (isChangesetMangled(changelogPath)) {
2255
+ const fixed = fixChangesetMangling(changelogPath);
2256
+ if (fixed) {
2257
+ results.push({
2258
+ fixed: true,
2259
+ message: `Restructured ${config.changelog.file} from Changesets format to Keep a Changelog`,
2260
+ file: changelogPath
2261
+ });
2262
+ }
2263
+ }
1873
2264
  const changelogResult = fixChangelog(version, config, cwd);
1874
2265
  if (changelogResult.fixed) {
1875
2266
  results.push(changelogResult);
@@ -2028,6 +2419,92 @@ function runGuardChecks(config, cwd) {
2028
2419
  warnings
2029
2420
  };
2030
2421
  }
2422
+ const PROJECT_MARKERS = [
2423
+ ".versionguard.yml",
2424
+ ".versionguard.yaml",
2425
+ "versionguard.yml",
2426
+ "versionguard.yaml",
2427
+ ".git",
2428
+ "package.json",
2429
+ "Cargo.toml",
2430
+ "pyproject.toml",
2431
+ "pubspec.yaml",
2432
+ "composer.json",
2433
+ "pom.xml",
2434
+ "go.mod",
2435
+ "mix.exs",
2436
+ "Gemfile",
2437
+ ".csproj"
2438
+ ];
2439
+ function findProjectRoot(startDir) {
2440
+ let current = path.resolve(startDir);
2441
+ while (true) {
2442
+ for (const marker of PROJECT_MARKERS) {
2443
+ if (marker.startsWith(".") && marker !== ".git" && !marker.startsWith(".version")) {
2444
+ try {
2445
+ const files = fs.readdirSync(current);
2446
+ if (files.some((f) => f.endsWith(marker))) {
2447
+ return buildResult(current, marker);
2448
+ }
2449
+ } catch {
2450
+ }
2451
+ } else if (fs.existsSync(path.join(current, marker))) {
2452
+ return buildResult(current, marker);
2453
+ }
2454
+ }
2455
+ const parent = path.dirname(current);
2456
+ if (parent === current) {
2457
+ return {
2458
+ found: false,
2459
+ root: path.resolve(startDir),
2460
+ hasConfig: false,
2461
+ hasGit: false,
2462
+ hasManifest: false
2463
+ };
2464
+ }
2465
+ current = parent;
2466
+ }
2467
+ }
2468
+ function buildResult(root, marker) {
2469
+ const configNames = [
2470
+ ".versionguard.yml",
2471
+ ".versionguard.yaml",
2472
+ "versionguard.yml",
2473
+ "versionguard.yaml"
2474
+ ];
2475
+ return {
2476
+ found: true,
2477
+ root,
2478
+ marker,
2479
+ hasConfig: configNames.some((c) => fs.existsSync(path.join(root, c))),
2480
+ hasGit: fs.existsSync(path.join(root, ".git")),
2481
+ hasManifest: [
2482
+ "package.json",
2483
+ "Cargo.toml",
2484
+ "pyproject.toml",
2485
+ "pubspec.yaml",
2486
+ "composer.json",
2487
+ "pom.xml",
2488
+ "VERSION"
2489
+ ].some((m) => fs.existsSync(path.join(root, m)))
2490
+ };
2491
+ }
2492
+ function formatNotProjectError(cwd, command) {
2493
+ const dir = path.basename(cwd) || cwd;
2494
+ const lines = [
2495
+ `Not a VersionGuard project: ${dir}`,
2496
+ "",
2497
+ "No .versionguard.yml, .git directory, or manifest file found.",
2498
+ "",
2499
+ "To get started:",
2500
+ " versionguard init Set up a new project interactively",
2501
+ " versionguard init --yes Set up with defaults",
2502
+ "",
2503
+ "Or run from a project root directory:",
2504
+ ` cd /path/to/project && versionguard ${command}`
2505
+ ];
2506
+ return lines.join("\n");
2507
+ }
2031
2508
  function runGit(cwd, args, encoding) {
2032
2509
  return childProcess.execFileSync("git", args, {
2033
2510
  cwd,
@@ -2192,7 +2669,7 @@ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false
2192
2669
  return "Working tree must be clean before creating or validating release tags";
2193
2670
  }
2194
2671
  const version = expectedVersion ?? getPackageVersion(cwd, config.manifest);
2195
- const versionResult = config.versioning.type === "semver" ? validate$1(version) : validate$2(
2672
+ const versionResult = config.versioning.type === "semver" ? validate$1(version, getSemVerConfig(config), config.versioning.schemeRules) : validate$2(
2196
2673
  version,
2197
2674
  config.versioning.calver?.format ?? "YYYY.MM.PATCH",
2198
2675
  config.versioning.calver?.preventFutureDates ?? true,
@@ -2277,7 +2754,7 @@ function suggestTagMessage(version, cwd = process.cwd()) {
2277
2754
  }
2278
2755
  function validateVersion(version, config) {
2279
2756
  if (config.versioning.type === "semver") {
2280
- return validate$1(version);
2757
+ return validate$1(version, getSemVerConfig(config), config.versioning.schemeRules);
2281
2758
  }
2282
2759
  const calverConfig = getCalVerConfig(config);
2283
2760
  return validate$2(
@@ -2405,53 +2882,59 @@ function isWorktreeClean(cwd) {
2405
2882
  }
2406
2883
  }
2407
2884
  export {
2408
- fixChangelog as A,
2409
- fixPackageVersion as B,
2410
- getAllTags as C,
2411
- getCalVerConfig as D,
2412
- getLatestTag as E,
2413
- getTagFeedback as F,
2885
+ validateVersion as $,
2886
+ checkHardcodedVersions as A,
2887
+ checkHookIntegrity as B,
2888
+ checkHooksPathOverride as C,
2889
+ checkHuskyBypass as D,
2890
+ detectManifests as E,
2891
+ fixChangelog as F,
2414
2892
  GitTagSource as G,
2415
- getVersionSource as H,
2416
- initConfig as I,
2893
+ fixPackageVersion as H,
2894
+ getAllTags as I,
2417
2895
  JsonVersionSource as J,
2418
- resolveVersionSource as K,
2419
- semver as L,
2420
- suggestTagMessage as M,
2421
- sync as N,
2422
- syncVersion as O,
2423
- validateChangelog as P,
2424
- validateTagForPush as Q,
2896
+ getCalVerConfig as K,
2897
+ getLatestTag as L,
2898
+ getSemVerConfig as M,
2899
+ getTagFeedback as N,
2900
+ getVersionSource as O,
2901
+ initConfig as P,
2902
+ resolveVersionSource as Q,
2425
2903
  RegexVersionSource as R,
2426
- validateVersion as S,
2904
+ semver as S,
2427
2905
  TomlVersionSource as T,
2906
+ suggestTagMessage as U,
2428
2907
  VersionFileSource as V,
2908
+ sync as W,
2909
+ syncVersion as X,
2429
2910
  YamlVersionSource as Y,
2911
+ validateChangelog as Z,
2912
+ validateTagForPush as _,
2430
2913
  installHooks as a,
2431
2914
  getPackageVersion as b,
2432
- getVersionFeedback as c,
2433
- getSyncFeedback as d,
2434
- getChangelogFeedback as e,
2435
- doctor as f,
2915
+ createCkmEngine as c,
2916
+ getVersionFeedback as d,
2917
+ getSyncFeedback as e,
2918
+ getChangelogFeedback as f,
2436
2919
  getConfig as g,
2437
2920
  handlePostTag as h,
2438
2921
  isValidCalVerFormat as i,
2439
- fixAll as j,
2440
- fixSyncIssues as k,
2441
- setPackageVersion as l,
2442
- createTag as m,
2443
- areHooksInstalled as n,
2444
- calver as o,
2445
- canBump as p,
2446
- checkEnforceHooksPolicy as q,
2922
+ doctor as j,
2923
+ fixAll as k,
2924
+ isChangesetMangled as l,
2925
+ fixChangesetMangling as m,
2926
+ fixSyncIssues as n,
2927
+ setPackageVersion as o,
2928
+ createTag as p,
2929
+ areHooksInstalled as q,
2447
2930
  runGuardChecks as r,
2448
2931
  suggestNextVersion as s,
2449
- checkHardcodedVersions as t,
2932
+ findProjectRoot as t,
2450
2933
  uninstallHooks as u,
2451
2934
  validate as v,
2452
- checkHookIntegrity as w,
2453
- checkHooksPathOverride as x,
2454
- checkHuskyBypass as y,
2455
- detectManifests as z
2935
+ formatNotProjectError as w,
2936
+ calver as x,
2937
+ canBump as y,
2938
+ checkEnforceHooksPolicy as z
2456
2939
  };
2457
- //# sourceMappingURL=index-B3R60bYJ.js.map
2940
+ //# sourceMappingURL=index-DeZAx4Le.js.map