@releasekit/notes 0.2.0-next.9 → 0.3.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,9 @@
1
+ import {
2
+ formatVersion,
3
+ renderMarkdown,
4
+ writeMarkdown
5
+ } from "./chunk-H7G2HRHI.js";
6
+
1
7
  // src/core/config.ts
2
8
  import {
3
9
  loadAuth,
@@ -145,137 +151,10 @@ async function parsePackageVersionerStdin() {
145
151
  return parsePackageVersioner(content);
146
152
  }
147
153
 
148
- // src/output/markdown.ts
154
+ // src/output/json.ts
149
155
  import * as fs2 from "fs";
150
156
  import * as path from "path";
151
- import { info, success } from "@releasekit/core";
152
- var TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
153
- var TYPE_LABELS = {
154
- added: "Added",
155
- changed: "Changed",
156
- deprecated: "Deprecated",
157
- removed: "Removed",
158
- fixed: "Fixed",
159
- security: "Security"
160
- };
161
- function groupEntriesByType(entries) {
162
- const grouped = /* @__PURE__ */ new Map();
163
- for (const type of TYPE_ORDER) {
164
- grouped.set(type, []);
165
- }
166
- for (const entry of entries) {
167
- const existing = grouped.get(entry.type) ?? [];
168
- existing.push(entry);
169
- grouped.set(entry.type, existing);
170
- }
171
- return grouped;
172
- }
173
- function formatEntry(entry) {
174
- let line;
175
- if (entry.breaking && entry.scope) {
176
- line = `- **BREAKING** **${entry.scope}**: ${entry.description}`;
177
- } else if (entry.breaking) {
178
- line = `- **BREAKING** ${entry.description}`;
179
- } else if (entry.scope) {
180
- line = `- **${entry.scope}**: ${entry.description}`;
181
- } else {
182
- line = `- ${entry.description}`;
183
- }
184
- if (entry.issueIds && entry.issueIds.length > 0) {
185
- line += ` (${entry.issueIds.join(", ")})`;
186
- }
187
- return line;
188
- }
189
- function formatVersion(context) {
190
- const lines = [];
191
- const versionHeader = context.previousVersion ? `## [${context.version}]` : `## ${context.version}`;
192
- lines.push(`${versionHeader} - ${context.date}`);
193
- lines.push("");
194
- if (context.compareUrl) {
195
- lines.push(`[Full Changelog](${context.compareUrl})`);
196
- lines.push("");
197
- }
198
- if (context.enhanced?.summary) {
199
- lines.push(context.enhanced.summary);
200
- lines.push("");
201
- }
202
- const grouped = groupEntriesByType(context.entries);
203
- for (const [type, entries] of grouped) {
204
- if (entries.length === 0) continue;
205
- lines.push(`### ${TYPE_LABELS[type]}`);
206
- for (const entry of entries) {
207
- lines.push(formatEntry(entry));
208
- }
209
- lines.push("");
210
- }
211
- return lines.join("\n");
212
- }
213
- function formatHeader() {
214
- return `# Changelog
215
-
216
- All notable changes to this project will be documented in this file.
217
-
218
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
219
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
220
-
221
- `;
222
- }
223
- function renderMarkdown(contexts) {
224
- const sections = [formatHeader()];
225
- for (const context of contexts) {
226
- sections.push(formatVersion(context));
227
- }
228
- return sections.join("\n");
229
- }
230
- function prependVersion(existingPath, context) {
231
- let existing = "";
232
- if (fs2.existsSync(existingPath)) {
233
- existing = fs2.readFileSync(existingPath, "utf-8");
234
- const headerEnd = existing.indexOf("\n## ");
235
- if (headerEnd >= 0) {
236
- const header = existing.slice(0, headerEnd);
237
- const body = existing.slice(headerEnd + 1);
238
- const newVersion = formatVersion(context);
239
- return `${header}
240
-
241
- ${newVersion}
242
- ${body}`;
243
- }
244
- }
245
- return renderMarkdown([context]);
246
- }
247
- function writeMarkdown(outputPath, contexts, config, dryRun) {
248
- const content = renderMarkdown(contexts);
249
- if (dryRun) {
250
- info("--- Changelog Preview ---");
251
- console.log(content);
252
- info("--- End Preview ---");
253
- return;
254
- }
255
- const dir = path.dirname(outputPath);
256
- if (!fs2.existsSync(dir)) {
257
- fs2.mkdirSync(dir, { recursive: true });
258
- }
259
- if (outputPath === "-") {
260
- process.stdout.write(content);
261
- return;
262
- }
263
- if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
264
- const firstContext = contexts[0];
265
- if (firstContext) {
266
- const updated = prependVersion(outputPath, firstContext);
267
- fs2.writeFileSync(outputPath, updated, "utf-8");
268
- }
269
- } else {
270
- fs2.writeFileSync(outputPath, content, "utf-8");
271
- }
272
- success(`Changelog written to ${outputPath}`);
273
- }
274
-
275
- // src/output/json.ts
276
- import * as fs3 from "fs";
277
- import * as path2 from "path";
278
- import { info as info2, success as success2 } from "@releasekit/core";
157
+ import { debug, info, success } from "@releasekit/core";
279
158
  function renderJson(contexts) {
280
159
  return JSON.stringify(
281
160
  {
@@ -295,23 +174,24 @@ function renderJson(contexts) {
295
174
  function writeJson(outputPath, contexts, dryRun) {
296
175
  const content = renderJson(contexts);
297
176
  if (dryRun) {
298
- info2("--- JSON Output Preview ---");
299
- console.log(content);
300
- info2("--- End Preview ---");
177
+ info(`Would write JSON output to ${outputPath}`);
178
+ debug("--- JSON Output Preview ---");
179
+ debug(content);
180
+ debug("--- End Preview ---");
301
181
  return;
302
182
  }
303
- const dir = path2.dirname(outputPath);
304
- if (!fs3.existsSync(dir)) {
305
- fs3.mkdirSync(dir, { recursive: true });
183
+ const dir = path.dirname(outputPath);
184
+ if (!fs2.existsSync(dir)) {
185
+ fs2.mkdirSync(dir, { recursive: true });
306
186
  }
307
- fs3.writeFileSync(outputPath, content, "utf-8");
308
- success2(`JSON output written to ${outputPath}`);
187
+ fs2.writeFileSync(outputPath, content, "utf-8");
188
+ success(`JSON output written to ${outputPath}`);
309
189
  }
310
190
 
311
191
  // src/core/pipeline.ts
312
- import * as fs8 from "fs";
313
- import * as path6 from "path";
314
- import { debug, info as info4, success as success4, warn as warn3 } from "@releasekit/core";
192
+ import * as fs7 from "fs";
193
+ import * as path5 from "path";
194
+ import { debug as debug2, info as info3, success as success3, warn as warn3 } from "@releasekit/core";
315
195
 
316
196
  // src/llm/defaults.ts
317
197
  var LLM_DEFAULTS = {
@@ -508,6 +388,83 @@ var OpenAICompatibleProvider = class extends BaseLLMProvider {
508
388
 
509
389
  // src/llm/tasks/categorize.ts
510
390
  import { warn } from "@releasekit/core";
391
+
392
+ // src/llm/prompts.ts
393
+ function resolvePrompt(taskName, defaultPrompt, promptsConfig) {
394
+ if (!promptsConfig) return defaultPrompt;
395
+ const fullTemplate = promptsConfig.templates?.[taskName];
396
+ if (fullTemplate) return fullTemplate;
397
+ const additionalInstructions = promptsConfig.instructions?.[taskName];
398
+ if (additionalInstructions) {
399
+ const insertionPoint = defaultPrompt.lastIndexOf("Output only valid JSON");
400
+ if (insertionPoint !== -1) {
401
+ return `${defaultPrompt.slice(0, insertionPoint)}Additional instructions:
402
+ ${additionalInstructions}
403
+
404
+ ${defaultPrompt.slice(insertionPoint)}`;
405
+ }
406
+ return `${defaultPrompt}
407
+
408
+ Additional instructions:
409
+ ${additionalInstructions}`;
410
+ }
411
+ return defaultPrompt;
412
+ }
413
+
414
+ // src/llm/scopes.ts
415
+ function getAllowedScopesFromCategories(categories) {
416
+ const scopeMap = /* @__PURE__ */ new Map();
417
+ for (const cat of categories) {
418
+ if (cat.scopes && cat.scopes.length > 0) {
419
+ scopeMap.set(cat.name, cat.scopes);
420
+ }
421
+ }
422
+ return scopeMap;
423
+ }
424
+ function resolveAllowedScopes(scopeConfig, categories, packageNames) {
425
+ if (!scopeConfig || scopeConfig.mode === "unrestricted") return null;
426
+ if (scopeConfig.mode === "none") return [];
427
+ if (scopeConfig.mode === "packages") return packageNames ?? [];
428
+ if (scopeConfig.mode === "restricted") {
429
+ const explicit = scopeConfig.rules?.allowed ?? [];
430
+ const all = new Set(explicit);
431
+ if (categories) {
432
+ const fromCategories = getAllowedScopesFromCategories(categories);
433
+ for (const scopes of fromCategories.values()) {
434
+ for (const s of scopes) all.add(s);
435
+ }
436
+ }
437
+ return [...all];
438
+ }
439
+ return null;
440
+ }
441
+ function validateScope(scope, allowedScopes, rules) {
442
+ if (!scope || allowedScopes === null) return scope;
443
+ if (allowedScopes.length === 0) return void 0;
444
+ const caseSensitive = rules?.caseSensitive ?? false;
445
+ const normalise = (s) => caseSensitive ? s : s.toLowerCase();
446
+ const isAllowed = allowedScopes.some((a) => normalise(a) === normalise(scope));
447
+ if (isAllowed) return scope;
448
+ switch (rules?.invalidScopeAction ?? "remove") {
449
+ case "keep":
450
+ return scope;
451
+ case "fallback":
452
+ return rules?.fallbackScope;
453
+ case "remove":
454
+ default:
455
+ return void 0;
456
+ }
457
+ }
458
+ function validateEntryScopes(entries, scopeConfig, categories) {
459
+ const allowedScopes = resolveAllowedScopes(scopeConfig, categories);
460
+ if (allowedScopes === null) return entries;
461
+ return entries.map((entry) => ({
462
+ ...entry,
463
+ scope: validateScope(entry.scope, allowedScopes, scopeConfig?.rules)
464
+ }));
465
+ }
466
+
467
+ // src/llm/tasks/categorize.ts
511
468
  var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
512
469
 
513
470
  Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
@@ -519,20 +476,21 @@ Entries:
519
476
 
520
477
  Output only valid JSON, nothing else:`;
521
478
  function buildCustomCategorizePrompt(categories) {
522
- const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
523
- const developerCategory = categories.find((c) => c.name === "Developer");
479
+ const categoryList = categories.map((c) => {
480
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
481
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
482
+ }).join("\n");
483
+ const scopeMap = getAllowedScopesFromCategories(categories);
524
484
  let scopeInstructions = "";
525
- if (developerCategory) {
526
- const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
527
- if (scopeMatch?.[1]) {
528
- const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
529
- if (scopes.length > 0) {
530
- scopeInstructions = `
531
-
532
- For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
533
- `;
534
- }
485
+ if (scopeMap.size > 0) {
486
+ const entries = [];
487
+ for (const [catName, scopes] of scopeMap) {
488
+ entries.push(`For "${catName}", assign a scope from: ${scopes.join(", ")}.`);
535
489
  }
490
+ scopeInstructions = `
491
+
492
+ ${entries.join("\n")}
493
+ Only use scopes from these predefined lists. If an entry does not fit any scope, set scope to null.`;
536
494
  }
537
495
  return `You are categorizing changelog entries for a software release.
538
496
 
@@ -540,9 +498,10 @@ Given the following entries, group them into the specified categories. Only use
540
498
 
541
499
  Categories:
542
500
  ${categoryList}${scopeInstructions}
501
+
543
502
  Output a JSON object with two fields:
544
503
  - "categories": an object where keys are category names and values are arrays of entry indices (0-based)
545
- - "scopes": an object where keys are entry indices (as strings) and values are scope labels
504
+ - "scopes": an object where keys are entry indices (as strings) and values are scope labels. Only include entries that have a valid scope from the predefined list.
546
505
 
547
506
  Entries:
548
507
  {{entries}}
@@ -553,9 +512,11 @@ async function categorizeEntries(provider, entries, context) {
553
512
  if (entries.length === 0) {
554
513
  return [];
555
514
  }
556
- const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
515
+ const entriesCopy = entries.map((e) => ({ ...e, scope: void 0 }));
516
+ const entriesText = entriesCopy.map((e, i) => `${i}. [${e.type}]: ${e.description}`).join("\n");
557
517
  const hasCustomCategories = context.categories && context.categories.length > 0;
558
- const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
518
+ const defaultPrompt = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
519
+ const promptTemplate = resolvePrompt("categorize", defaultPrompt, context.prompts);
559
520
  const prompt = promptTemplate.replace("{{entries}}", entriesText);
560
521
  try {
561
522
  const response = await provider.complete(prompt);
@@ -567,13 +528,14 @@ async function categorizeEntries(provider, entries, context) {
567
528
  const scopeMap = parsed.scopes || {};
568
529
  for (const [indexStr, scope] of Object.entries(scopeMap)) {
569
530
  const idx = Number.parseInt(indexStr, 10);
570
- if (entries[idx] && scope) {
571
- entries[idx] = { ...entries[idx], scope };
531
+ if (entriesCopy[idx] && scope && scope.trim()) {
532
+ entriesCopy[idx] = { ...entriesCopy[idx], scope: scope.trim() };
572
533
  }
573
534
  }
535
+ const validatedEntries = validateEntryScopes(entriesCopy, context.scopes, context.categories);
574
536
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
575
537
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
576
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
538
+ const categoryEntries = indices.map((i) => validatedEntries[i]).filter((e) => e !== void 0);
577
539
  if (categoryEntries.length > 0) {
578
540
  result.push({ category, entries: categoryEntries });
579
541
  }
@@ -582,7 +544,7 @@ async function categorizeEntries(provider, entries, context) {
582
544
  const categoryMap = parsed;
583
545
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
584
546
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
585
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
547
+ const categoryEntries = indices.map((i) => entriesCopy[i]).filter((e) => e !== void 0);
586
548
  if (categoryEntries.length > 0) {
587
549
  result.push({ category, entries: categoryEntries });
588
550
  }
@@ -593,12 +555,12 @@ async function categorizeEntries(provider, entries, context) {
593
555
  warn(
594
556
  `LLM categorization failed, falling back to General: ${error instanceof Error ? error.message : String(error)}`
595
557
  );
596
- return [{ category: "General", entries }];
558
+ return [{ category: "General", entries: entriesCopy }];
597
559
  }
598
560
  }
599
561
 
600
562
  // src/llm/tasks/enhance.ts
601
- var ENHANCE_PROMPT = `You are improving changelog entries for a software project.
563
+ var DEFAULT_ENHANCE_PROMPT = `You are improving changelog entries for a software project.
602
564
  Given a technical commit message, rewrite it as a clear, user-friendly changelog entry.
603
565
 
604
566
  Rules:
@@ -614,9 +576,10 @@ Type: {{type}}
614
576
  Description: {{description}}
615
577
 
616
578
  Rewritten description (only output the new description, nothing else):`;
617
- async function enhanceEntry(provider, entry, _context) {
618
- const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
619
- const prompt = ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
579
+ async function enhanceEntry(provider, entry, context) {
580
+ const styleText = context.style ? `- ${context.style}` : '- Use present tense ("Add feature" not "Added feature")';
581
+ const defaultPrompt = DEFAULT_ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
582
+ const prompt = resolvePrompt("enhance", defaultPrompt, context.prompts);
620
583
  const response = await provider.complete(prompt);
621
584
  return response.trim();
622
585
  }
@@ -672,7 +635,23 @@ function buildPrompt(entries, categories, style) {
672
635
  const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
673
636
  const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
674
637
  const categorySection = categories ? `Categories (use ONLY these):
675
- ${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
638
+ ${categories.map((c) => {
639
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
640
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
641
+ }).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
642
+ let scopeInstruction = "";
643
+ if (categories) {
644
+ const scopeMap = getAllowedScopesFromCategories(categories);
645
+ if (scopeMap.size > 0) {
646
+ const parts = [];
647
+ for (const [catName, scopes] of scopeMap) {
648
+ parts.push(`For "${catName}" entries, assign a scope from: ${scopes.join(", ")}.`);
649
+ }
650
+ scopeInstruction = `
651
+ ${parts.join("\n")}
652
+ Only use scopes from these predefined lists. Set scope to null if no scope applies.`;
653
+ }
654
+ }
676
655
  return `You are generating release notes for a software project. Given the following changelog entries, do two things:
677
656
 
678
657
  1. **Rewrite** each entry as a clear, user-friendly description
@@ -683,9 +662,7 @@ Style guidelines:
683
662
  - Be concise (1 short sentence per entry)
684
663
  - Focus on what changed, not implementation details
685
664
 
686
- ${categorySection}
687
-
688
- ${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
665
+ ${categorySection}${scopeInstruction}
689
666
 
690
667
  Entries:
691
668
  ${entriesText}
@@ -702,7 +679,8 @@ async function enhanceAndCategorize(provider, entries, context) {
702
679
  const retryOpts = LLM_DEFAULTS.retry;
703
680
  try {
704
681
  return await withRetry(async () => {
705
- const prompt = buildPrompt(entries, context.categories, context.style);
682
+ const defaultPrompt = buildPrompt(entries, context.categories, context.style);
683
+ const prompt = resolvePrompt("enhanceAndCategorize", defaultPrompt, context.prompts);
706
684
  const response = await provider.complete(prompt);
707
685
  const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
708
686
  const parsed = JSON.parse(cleaned);
@@ -718,22 +696,23 @@ async function enhanceAndCategorize(provider, entries, context) {
718
696
  scope: result.scope || original.scope
719
697
  };
720
698
  });
699
+ const validatedEntries = validateEntryScopes(enhancedEntries, context.scopes, context.categories);
721
700
  const categoryMap = /* @__PURE__ */ new Map();
722
701
  for (let i = 0; i < parsed.entries.length; i++) {
723
702
  const result = parsed.entries[i];
724
703
  const category = result?.category || "General";
725
- const entry = enhancedEntries[i];
704
+ const entry = validatedEntries[i];
726
705
  if (!entry) continue;
727
706
  if (!categoryMap.has(category)) {
728
707
  categoryMap.set(category, []);
729
708
  }
730
- categoryMap.get(category).push(entry);
709
+ categoryMap.get(category)?.push(entry);
731
710
  }
732
711
  const categories = [];
733
712
  for (const [category, catEntries] of categoryMap) {
734
713
  categories.push({ category, entries: catEntries });
735
714
  }
736
- return { enhancedEntries, categories };
715
+ return { enhancedEntries: validatedEntries, categories };
737
716
  }, retryOpts);
738
717
  } catch (error) {
739
718
  warn2(
@@ -747,7 +726,7 @@ async function enhanceAndCategorize(provider, entries, context) {
747
726
  }
748
727
 
749
728
  // src/llm/tasks/release-notes.ts
750
- var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
729
+ var DEFAULT_RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
751
730
 
752
731
  Create engaging, user-friendly release notes for the following changes.
753
732
 
@@ -780,16 +759,17 @@ No notable changes in this release.`;
780
759
  if (e.breaking) line += " **BREAKING**";
781
760
  return line;
782
761
  }).join("\n");
783
- const prompt = RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
762
+ const defaultPrompt = DEFAULT_RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
784
763
  "{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}",
785
764
  context.previousVersion ? `Previous version: ${context.previousVersion}` : ""
786
765
  ).replace("{{date}}", context.date ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "").replace("{{entries}}", entriesText);
766
+ const prompt = resolvePrompt("releaseNotes", defaultPrompt, context.prompts);
787
767
  const response = await provider.complete(prompt);
788
768
  return response.trim();
789
769
  }
790
770
 
791
771
  // src/llm/tasks/summarize.ts
792
- var SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
772
+ var DEFAULT_SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
793
773
 
794
774
  Given the following changelog entries, create a brief summary (2-3 sentences) that captures the main themes of this release.
795
775
 
@@ -797,12 +777,13 @@ Entries:
797
777
  {{entries}}
798
778
 
799
779
  Summary (only output the summary, nothing else):`;
800
- async function summarizeEntries(provider, entries, _context) {
780
+ async function summarizeEntries(provider, entries, context) {
801
781
  if (entries.length === 0) {
802
782
  return "";
803
783
  }
804
784
  const entriesText = entries.map((e) => `- [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
805
- const prompt = SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
785
+ const defaultPrompt = DEFAULT_SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
786
+ const prompt = resolvePrompt("summarize", defaultPrompt, context.prompts);
806
787
  const response = await provider.complete(prompt);
807
788
  return response.trim();
808
789
  }
@@ -846,7 +827,7 @@ function createProvider(config) {
846
827
 
847
828
  // src/output/github-release.ts
848
829
  import { Octokit } from "@octokit/rest";
849
- import { info as info3, success as success3 } from "@releasekit/core";
830
+ import { info as info2, success as success2 } from "@releasekit/core";
850
831
  var GitHubClient = class {
851
832
  octokit;
852
833
  owner;
@@ -868,7 +849,7 @@ var GitHubClient = class {
868
849
  } else {
869
850
  body = renderMarkdown([context]);
870
851
  }
871
- info3(`Creating GitHub release for ${tagName}`);
852
+ info2(`Creating GitHub release for ${tagName}`);
872
853
  try {
873
854
  const response = await this.octokit.repos.createRelease({
874
855
  owner: this.owner,
@@ -880,7 +861,7 @@ var GitHubClient = class {
880
861
  prerelease: options.prerelease ?? false,
881
862
  generate_release_notes: options.generateNotes ?? false
882
863
  });
883
- success3(`Release created: ${response.data.html_url}`);
864
+ success2(`Release created: ${response.data.html_url}`);
884
865
  return {
885
866
  id: response.data.id,
886
867
  htmlUrl: response.data.html_url,
@@ -898,7 +879,7 @@ var GitHubClient = class {
898
879
  } else {
899
880
  body = renderMarkdown([context]);
900
881
  }
901
- info3(`Updating GitHub release ${releaseId}`);
882
+ info2(`Updating GitHub release ${releaseId}`);
902
883
  try {
903
884
  const response = await this.octokit.repos.updateRelease({
904
885
  owner: this.owner,
@@ -910,7 +891,7 @@ var GitHubClient = class {
910
891
  draft: options.draft ?? false,
911
892
  prerelease: options.prerelease ?? false
912
893
  });
913
- success3(`Release updated: ${response.data.html_url}`);
894
+ success2(`Release updated: ${response.data.html_url}`);
914
895
  return {
915
896
  id: response.data.id,
916
897
  htmlUrl: response.data.html_url,
@@ -960,7 +941,7 @@ async function createGitHubRelease(context, options) {
960
941
  }
961
942
 
962
943
  // src/templates/ejs.ts
963
- import * as fs4 from "fs";
944
+ import * as fs3 from "fs";
964
945
  import ejs from "ejs";
965
946
  function renderEjs(template, context) {
966
947
  try {
@@ -970,16 +951,16 @@ function renderEjs(template, context) {
970
951
  }
971
952
  }
972
953
  function renderEjsFile(filePath, context) {
973
- if (!fs4.existsSync(filePath)) {
954
+ if (!fs3.existsSync(filePath)) {
974
955
  throw new TemplateError(`Template file not found: ${filePath}`);
975
956
  }
976
- const template = fs4.readFileSync(filePath, "utf-8");
957
+ const template = fs3.readFileSync(filePath, "utf-8");
977
958
  return renderEjs(template, context);
978
959
  }
979
960
 
980
961
  // src/templates/handlebars.ts
981
- import * as fs5 from "fs";
982
- import * as path3 from "path";
962
+ import * as fs4 from "fs";
963
+ import * as path2 from "path";
983
964
  import Handlebars from "handlebars";
984
965
  function registerHandlebarsHelpers() {
985
966
  Handlebars.registerHelper("capitalize", (str) => {
@@ -1005,28 +986,28 @@ function renderHandlebars(template, context) {
1005
986
  }
1006
987
  }
1007
988
  function renderHandlebarsFile(filePath, context) {
1008
- if (!fs5.existsSync(filePath)) {
989
+ if (!fs4.existsSync(filePath)) {
1009
990
  throw new TemplateError(`Template file not found: ${filePath}`);
1010
991
  }
1011
- const template = fs5.readFileSync(filePath, "utf-8");
992
+ const template = fs4.readFileSync(filePath, "utf-8");
1012
993
  return renderHandlebars(template, context);
1013
994
  }
1014
995
  function renderHandlebarsComposable(templateDir, context) {
1015
996
  registerHandlebarsHelpers();
1016
- const versionPath = path3.join(templateDir, "version.hbs");
1017
- const entryPath = path3.join(templateDir, "entry.hbs");
1018
- const documentPath = path3.join(templateDir, "document.hbs");
1019
- if (!fs5.existsSync(documentPath)) {
997
+ const versionPath = path2.join(templateDir, "version.hbs");
998
+ const entryPath = path2.join(templateDir, "entry.hbs");
999
+ const documentPath = path2.join(templateDir, "document.hbs");
1000
+ if (!fs4.existsSync(documentPath)) {
1020
1001
  throw new TemplateError(`Document template not found: ${documentPath}`);
1021
1002
  }
1022
- if (fs5.existsSync(versionPath)) {
1023
- Handlebars.registerPartial("version", fs5.readFileSync(versionPath, "utf-8"));
1003
+ if (fs4.existsSync(versionPath)) {
1004
+ Handlebars.registerPartial("version", fs4.readFileSync(versionPath, "utf-8"));
1024
1005
  }
1025
- if (fs5.existsSync(entryPath)) {
1026
- Handlebars.registerPartial("entry", fs5.readFileSync(entryPath, "utf-8"));
1006
+ if (fs4.existsSync(entryPath)) {
1007
+ Handlebars.registerPartial("entry", fs4.readFileSync(entryPath, "utf-8"));
1027
1008
  }
1028
1009
  try {
1029
- const compiled = Handlebars.compile(fs5.readFileSync(documentPath, "utf-8"));
1010
+ const compiled = Handlebars.compile(fs4.readFileSync(documentPath, "utf-8"));
1030
1011
  return compiled(context);
1031
1012
  } catch (error) {
1032
1013
  throw new TemplateError(`Handlebars render error: ${error instanceof Error ? error.message : String(error)}`);
@@ -1034,8 +1015,8 @@ function renderHandlebarsComposable(templateDir, context) {
1034
1015
  }
1035
1016
 
1036
1017
  // src/templates/liquid.ts
1037
- import * as fs6 from "fs";
1038
- import * as path4 from "path";
1018
+ import * as fs5 from "fs";
1019
+ import * as path3 from "path";
1039
1020
  import { Liquid } from "liquidjs";
1040
1021
  function createLiquidEngine(root) {
1041
1022
  return new Liquid({
@@ -1053,15 +1034,15 @@ function renderLiquid(template, context) {
1053
1034
  }
1054
1035
  }
1055
1036
  function renderLiquidFile(filePath, context) {
1056
- if (!fs6.existsSync(filePath)) {
1037
+ if (!fs5.existsSync(filePath)) {
1057
1038
  throw new TemplateError(`Template file not found: ${filePath}`);
1058
1039
  }
1059
- const template = fs6.readFileSync(filePath, "utf-8");
1040
+ const template = fs5.readFileSync(filePath, "utf-8");
1060
1041
  return renderLiquid(template, context);
1061
1042
  }
1062
1043
  function renderLiquidComposable(templateDir, context) {
1063
- const documentPath = path4.join(templateDir, "document.liquid");
1064
- if (!fs6.existsSync(documentPath)) {
1044
+ const documentPath = path3.join(templateDir, "document.liquid");
1045
+ if (!fs5.existsSync(documentPath)) {
1065
1046
  throw new TemplateError(`Document template not found: ${documentPath}`);
1066
1047
  }
1067
1048
  const engine = createLiquidEngine(templateDir);
@@ -1073,10 +1054,10 @@ function renderLiquidComposable(templateDir, context) {
1073
1054
  }
1074
1055
 
1075
1056
  // src/templates/loader.ts
1076
- import * as fs7 from "fs";
1077
- import * as path5 from "path";
1057
+ import * as fs6 from "fs";
1058
+ import * as path4 from "path";
1078
1059
  function getEngineFromFile(filePath) {
1079
- const ext = path5.extname(filePath).toLowerCase();
1060
+ const ext = path4.extname(filePath).toLowerCase();
1080
1061
  switch (ext) {
1081
1062
  case ".liquid":
1082
1063
  return "liquid";
@@ -1110,10 +1091,10 @@ function getRenderFileFn(engine) {
1110
1091
  }
1111
1092
  }
1112
1093
  function detectTemplateMode(templatePath) {
1113
- if (!fs7.existsSync(templatePath)) {
1094
+ if (!fs6.existsSync(templatePath)) {
1114
1095
  throw new TemplateError(`Template path not found: ${templatePath}`);
1115
1096
  }
1116
- const stat = fs7.statSync(templatePath);
1097
+ const stat = fs6.statSync(templatePath);
1117
1098
  if (stat.isFile()) {
1118
1099
  return "single";
1119
1100
  }
@@ -1131,7 +1112,7 @@ function renderSingleFile(templatePath, context, engine) {
1131
1112
  };
1132
1113
  }
1133
1114
  function renderComposable(templateDir, context, engine) {
1134
- const files = fs7.readdirSync(templateDir);
1115
+ const files = fs6.readdirSync(templateDir);
1135
1116
  const engineMap = {
1136
1117
  liquid: { document: "document.liquid", version: "version.liquid", entry: "entry.liquid" },
1137
1118
  handlebars: { document: "document.hbs", version: "version.hbs", entry: "entry.hbs" },
@@ -1154,15 +1135,15 @@ function renderComposable(templateDir, context, engine) {
1154
1135
  return { content: renderHandlebarsComposable(templateDir, context), engine: resolvedEngine };
1155
1136
  }
1156
1137
  const expectedFiles = engineMap[resolvedEngine];
1157
- const documentPath = path5.join(templateDir, expectedFiles.document);
1158
- if (!fs7.existsSync(documentPath)) {
1138
+ const documentPath = path4.join(templateDir, expectedFiles.document);
1139
+ if (!fs6.existsSync(documentPath)) {
1159
1140
  throw new TemplateError(`Document template not found: ${expectedFiles.document}`);
1160
1141
  }
1161
- const versionPath = path5.join(templateDir, expectedFiles.version);
1162
- const entryPath = path5.join(templateDir, expectedFiles.entry);
1142
+ const versionPath = path4.join(templateDir, expectedFiles.version);
1143
+ const entryPath = path4.join(templateDir, expectedFiles.entry);
1163
1144
  const render = getRenderFn(resolvedEngine);
1164
- const entryTemplate = fs7.existsSync(entryPath) ? fs7.readFileSync(entryPath, "utf-8") : null;
1165
- const versionTemplate = fs7.existsSync(versionPath) ? fs7.readFileSync(versionPath, "utf-8") : null;
1145
+ const entryTemplate = fs6.existsSync(entryPath) ? fs6.readFileSync(entryPath, "utf-8") : null;
1146
+ const versionTemplate = fs6.existsSync(versionPath) ? fs6.readFileSync(versionPath, "utf-8") : null;
1166
1147
  if (entryTemplate && versionTemplate) {
1167
1148
  const versionsWithEntries = context.versions.map((versionCtx) => {
1168
1149
  const entries = versionCtx.entries.map((entry) => {
@@ -1173,7 +1154,7 @@ function renderComposable(templateDir, context, engine) {
1173
1154
  });
1174
1155
  const docContext = { ...context, renderedVersions: versionsWithEntries };
1175
1156
  return {
1176
- content: render(fs7.readFileSync(documentPath, "utf-8"), docContext),
1157
+ content: render(fs6.readFileSync(documentPath, "utf-8"), docContext),
1177
1158
  engine: resolvedEngine
1178
1159
  };
1179
1160
  }
@@ -1214,17 +1195,27 @@ function renderTemplate(templatePath, context, engine) {
1214
1195
  }
1215
1196
 
1216
1197
  // src/core/pipeline.ts
1217
- function generateCompareUrl(repoUrl, from, to) {
1198
+ function generateCompareUrl(repoUrl, from, to, packageName) {
1199
+ const isPackageSpecific = from.includes("@") && packageName && from.includes(packageName);
1200
+ let fromVersion;
1201
+ let toVersion;
1202
+ if (isPackageSpecific) {
1203
+ fromVersion = from;
1204
+ toVersion = `${packageName}@${to.startsWith("v") ? "" : "v"}${to}`;
1205
+ } else {
1206
+ fromVersion = from.replace(/^v/, "");
1207
+ toVersion = to.replace(/^v/, "");
1208
+ }
1218
1209
  if (/gitlab\.com/i.test(repoUrl)) {
1219
- return `${repoUrl}/-/compare/${from}...${to}`;
1210
+ return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
1220
1211
  }
1221
1212
  if (/bitbucket\.org/i.test(repoUrl)) {
1222
- return `${repoUrl}/branches/compare/${from}..${to}`;
1213
+ return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
1223
1214
  }
1224
- return `${repoUrl}/compare/${from}...${to}`;
1215
+ return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
1225
1216
  }
1226
1217
  function createTemplateContext(pkg) {
1227
- const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
1218
+ const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version, pkg.packageName) : void 0;
1228
1219
  return {
1229
1220
  packageName: pkg.packageName,
1230
1221
  version: pkg.version,
@@ -1262,15 +1253,17 @@ async function processWithLLM(context, config) {
1262
1253
  previousVersion: context.previousVersion ?? void 0,
1263
1254
  date: context.date,
1264
1255
  categories: config.llm.categories,
1265
- style: config.llm.style
1256
+ style: config.llm.style,
1257
+ scopes: config.llm.scopes,
1258
+ prompts: config.llm.prompts
1266
1259
  };
1267
1260
  const enhanced = {
1268
1261
  entries: context.entries
1269
1262
  };
1270
1263
  try {
1271
- info4(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1264
+ info3(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1272
1265
  if (config.llm.baseURL) {
1273
- info4(`LLM base URL: ${config.llm.baseURL}`);
1266
+ info3(`LLM base URL: ${config.llm.baseURL}`);
1274
1267
  }
1275
1268
  const rawProvider = createProvider(config.llm);
1276
1269
  const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
@@ -1279,54 +1272,53 @@ async function processWithLLM(context, config) {
1279
1272
  complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
1280
1273
  };
1281
1274
  const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
1282
- info4(`Running LLM tasks: ${activeTasks.join(", ")}`);
1275
+ info3(`Running LLM tasks: ${activeTasks.join(", ")}`);
1283
1276
  if (tasks.enhance && tasks.categorize) {
1284
- info4("Enhancing and categorizing entries with LLM...");
1277
+ info3("Enhancing and categorizing entries with LLM...");
1285
1278
  const result = await enhanceAndCategorize(provider, context.entries, llmContext);
1286
1279
  enhanced.entries = result.enhancedEntries;
1287
1280
  enhanced.categories = {};
1288
1281
  for (const cat of result.categories) {
1289
1282
  enhanced.categories[cat.category] = cat.entries;
1290
1283
  }
1291
- info4(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1284
+ info3(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1292
1285
  } else {
1293
1286
  if (tasks.enhance) {
1294
- info4("Enhancing entries with LLM...");
1287
+ info3("Enhancing entries with LLM...");
1295
1288
  enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1296
- info4(`Enhanced ${enhanced.entries.length} entries`);
1289
+ info3(`Enhanced ${enhanced.entries.length} entries`);
1297
1290
  }
1298
1291
  if (tasks.categorize) {
1299
- info4("Categorizing entries with LLM...");
1292
+ info3("Categorizing entries with LLM...");
1300
1293
  const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1301
1294
  enhanced.categories = {};
1302
1295
  for (const cat of categorized) {
1303
1296
  enhanced.categories[cat.category] = cat.entries;
1304
1297
  }
1305
- info4(`Created ${categorized.length} categories`);
1298
+ info3(`Created ${categorized.length} categories`);
1306
1299
  }
1307
1300
  }
1308
1301
  if (tasks.summarize) {
1309
- info4("Summarizing entries with LLM...");
1302
+ info3("Summarizing entries with LLM...");
1310
1303
  enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
1311
1304
  if (enhanced.summary) {
1312
- info4("Summary generated successfully");
1313
- debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1305
+ info3("Summary generated successfully");
1306
+ debug2(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1314
1307
  } else {
1315
1308
  warn3("Summary generation returned empty result");
1316
1309
  }
1317
1310
  }
1318
1311
  if (tasks.releaseNotes) {
1319
- info4("Generating release notes with LLM...");
1312
+ info3("Generating release notes with LLM...");
1320
1313
  enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
1321
1314
  if (enhanced.releaseNotes) {
1322
- info4("Release notes generated successfully");
1315
+ info3("Release notes generated successfully");
1323
1316
  } else {
1324
1317
  warn3("Release notes generation returned empty result");
1325
1318
  }
1326
1319
  }
1327
1320
  return {
1328
1321
  ...context,
1329
- entries: enhanced.entries,
1330
1322
  enhanced
1331
1323
  };
1332
1324
  } catch (error) {
@@ -1339,17 +1331,17 @@ function getBuiltinTemplatePath(style) {
1339
1331
  let packageRoot;
1340
1332
  try {
1341
1333
  const currentUrl = import.meta.url;
1342
- packageRoot = path6.dirname(new URL(currentUrl).pathname);
1343
- packageRoot = path6.join(packageRoot, "..", "..");
1334
+ packageRoot = path5.dirname(new URL(currentUrl).pathname);
1335
+ packageRoot = path5.join(packageRoot, "..", "..");
1344
1336
  } catch {
1345
1337
  packageRoot = __dirname;
1346
1338
  }
1347
- return path6.join(packageRoot, "templates", style);
1339
+ return path5.join(packageRoot, "templates", style);
1348
1340
  }
1349
1341
  async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1350
1342
  let templatePath;
1351
1343
  if (config.templates?.path) {
1352
- templatePath = path6.resolve(config.templates.path);
1344
+ templatePath = path5.resolve(config.templates.path);
1353
1345
  } else {
1354
1346
  templatePath = getBuiltinTemplatePath("keep-a-changelog");
1355
1347
  }
@@ -1359,49 +1351,63 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1359
1351
  );
1360
1352
  const result = renderTemplate(templatePath, documentContext, config.templates?.engine);
1361
1353
  if (dryRun) {
1362
- info4("--- Changelog Preview ---");
1363
- console.log(result.content);
1364
- info4("--- End Preview ---");
1354
+ info3(`Would write templated output to ${outputPath}`);
1355
+ debug2("--- Changelog Preview ---");
1356
+ debug2(result.content);
1357
+ debug2("--- End Preview ---");
1365
1358
  return;
1366
1359
  }
1367
1360
  if (outputPath === "-") {
1368
1361
  process.stdout.write(result.content);
1369
1362
  return;
1370
1363
  }
1371
- const dir = path6.dirname(outputPath);
1372
- if (!fs8.existsSync(dir)) {
1373
- fs8.mkdirSync(dir, { recursive: true });
1364
+ const dir = path5.dirname(outputPath);
1365
+ if (!fs7.existsSync(dir)) {
1366
+ fs7.mkdirSync(dir, { recursive: true });
1374
1367
  }
1375
- fs8.writeFileSync(outputPath, result.content, "utf-8");
1376
- success4(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1368
+ fs7.writeFileSync(outputPath, result.content, "utf-8");
1369
+ success3(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1377
1370
  }
1378
1371
  async function runPipeline(input, config, dryRun) {
1379
- debug(`Processing ${input.packages.length} package(s)`);
1372
+ debug2(`Processing ${input.packages.length} package(s)`);
1380
1373
  let contexts = input.packages.map(createTemplateContext);
1381
1374
  if (config.llm && !process.env.CHANGELOG_NO_LLM) {
1382
- info4("Processing with LLM enhancement");
1375
+ info3("Processing with LLM enhancement");
1383
1376
  contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
1384
1377
  }
1378
+ const files = [];
1385
1379
  for (const output of config.output) {
1386
- info4(`Generating ${output.format} output`);
1380
+ info3(`Generating ${output.format} output`);
1387
1381
  switch (output.format) {
1388
1382
  case "markdown": {
1389
1383
  const file = output.file ?? "CHANGELOG.md";
1390
- if (config.templates?.path || output.options?.template) {
1391
- await generateWithTemplate(contexts, config, file, dryRun);
1392
- } else {
1393
- writeMarkdown(file, contexts, config, dryRun);
1384
+ try {
1385
+ const effectiveTemplateConfig = output.templates ?? config.templates;
1386
+ if (effectiveTemplateConfig?.path || output.options?.template) {
1387
+ const configWithTemplate = { ...config, templates: effectiveTemplateConfig };
1388
+ await generateWithTemplate(contexts, configWithTemplate, file, dryRun);
1389
+ } else {
1390
+ writeMarkdown(file, contexts, config, dryRun);
1391
+ }
1392
+ if (!dryRun) files.push(file);
1393
+ } catch (error) {
1394
+ warn3(`Failed to write ${file}: ${error instanceof Error ? error.message : String(error)}`);
1394
1395
  }
1395
1396
  break;
1396
1397
  }
1397
1398
  case "json": {
1398
1399
  const file = output.file ?? "changelog.json";
1399
- writeJson(file, contexts, dryRun);
1400
+ try {
1401
+ writeJson(file, contexts, dryRun);
1402
+ if (!dryRun) files.push(file);
1403
+ } catch (error) {
1404
+ warn3(`Failed to write ${file}: ${error instanceof Error ? error.message : String(error)}`);
1405
+ }
1400
1406
  break;
1401
1407
  }
1402
1408
  case "github-release": {
1403
1409
  if (dryRun) {
1404
- info4("[DRY RUN] Would create GitHub release");
1410
+ info3("[DRY RUN] Would create GitHub release");
1405
1411
  break;
1406
1412
  }
1407
1413
  const firstContext = contexts[0];
@@ -1429,139 +1435,33 @@ async function runPipeline(input, config, dryRun) {
1429
1435
  }
1430
1436
  }
1431
1437
  }
1432
- }
1433
- async function processInput(inputJson, config, dryRun) {
1434
- const input = parsePackageVersioner(inputJson);
1435
- await runPipeline(input, config, dryRun);
1436
- }
1437
-
1438
- // src/monorepo/aggregator.ts
1439
- import * as fs9 from "fs";
1440
- import * as path7 from "path";
1441
- import { info as info5, success as success5 } from "@releasekit/core";
1442
-
1443
- // src/monorepo/splitter.ts
1444
- function splitByPackage(contexts) {
1445
- const byPackage = /* @__PURE__ */ new Map();
1446
- for (const ctx of contexts) {
1447
- byPackage.set(ctx.packageName, ctx);
1448
- }
1449
- return byPackage;
1450
- }
1451
-
1452
- // src/monorepo/aggregator.ts
1453
- function writeFile(outputPath, content, dryRun) {
1454
- if (dryRun) {
1455
- info5(`[DRY RUN] Would write to ${outputPath}`);
1456
- console.log(content);
1457
- return;
1458
- }
1459
- const dir = path7.dirname(outputPath);
1460
- if (!fs9.existsSync(dir)) {
1461
- fs9.mkdirSync(dir, { recursive: true });
1462
- }
1463
- fs9.writeFileSync(outputPath, content, "utf-8");
1464
- success5(`Changelog written to ${outputPath}`);
1465
- }
1466
- function aggregateToRoot(contexts) {
1467
- const aggregated = {
1468
- packageName: "monorepo",
1469
- version: contexts[0]?.version ?? "0.0.0",
1470
- previousVersion: contexts[0]?.previousVersion ?? null,
1471
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
1472
- repoUrl: contexts[0]?.repoUrl ?? null,
1473
- entries: []
1474
- };
1475
- for (const ctx of contexts) {
1476
- for (const entry of ctx.entries) {
1477
- aggregated.entries.push({
1478
- ...entry,
1479
- scope: entry.scope ? `${ctx.packageName}/${entry.scope}` : ctx.packageName
1480
- });
1481
- }
1482
- }
1483
- return aggregated;
1484
- }
1485
- function writeMonorepoChangelogs(contexts, options, config, dryRun) {
1486
- if (options.mode === "root" || options.mode === "both") {
1487
- const aggregated = aggregateToRoot(contexts);
1488
- const rootPath = path7.join(options.rootPath, "CHANGELOG.md");
1489
- info5(`Writing root changelog to ${rootPath}`);
1490
- const rootContent = config.updateStrategy === "prepend" && fs9.existsSync(rootPath) ? prependVersion(rootPath, aggregated) : renderMarkdown([aggregated]);
1491
- writeFile(rootPath, rootContent, dryRun);
1492
- }
1493
- if (options.mode === "packages" || options.mode === "both") {
1494
- const byPackage = splitByPackage(contexts);
1495
- const packageDirMap = buildPackageDirMap(options.rootPath, options.packagesPath);
1496
- for (const [packageName, ctx] of byPackage) {
1497
- const simpleName = packageName.split("/").pop();
1498
- const packageDir = packageDirMap.get(packageName) ?? (simpleName ? packageDirMap.get(simpleName) : void 0) ?? null;
1499
- if (packageDir) {
1500
- const changelogPath = path7.join(packageDir, "CHANGELOG.md");
1501
- info5(`Writing changelog for ${packageName} to ${changelogPath}`);
1502
- const pkgContent = config.updateStrategy === "prepend" && fs9.existsSync(changelogPath) ? prependVersion(changelogPath, ctx) : renderMarkdown([ctx]);
1503
- writeFile(changelogPath, pkgContent, dryRun);
1504
- } else {
1505
- info5(`Could not find directory for package ${packageName}, skipping`);
1506
- }
1438
+ if (config.monorepo?.mode) {
1439
+ const { detectMonorepo, writeMonorepoChangelogs } = await import("./aggregator-BDTUZWOA.js");
1440
+ const cwd = process.cwd();
1441
+ const detected = detectMonorepo(cwd);
1442
+ if (detected.isMonorepo) {
1443
+ const monoFiles = writeMonorepoChangelogs(
1444
+ contexts,
1445
+ {
1446
+ rootPath: config.monorepo.rootPath ?? cwd,
1447
+ packagesPath: config.monorepo.packagesPath ?? detected.packagesPath,
1448
+ mode: config.monorepo.mode
1449
+ },
1450
+ config,
1451
+ dryRun
1452
+ );
1453
+ files.push(...monoFiles);
1507
1454
  }
1508
1455
  }
1509
- }
1510
- function buildPackageDirMap(rootPath, packagesPath) {
1511
- const map = /* @__PURE__ */ new Map();
1512
- const packagesDir = path7.join(rootPath, packagesPath);
1513
- if (!fs9.existsSync(packagesDir)) {
1514
- return map;
1515
- }
1516
- for (const entry of fs9.readdirSync(packagesDir, { withFileTypes: true })) {
1517
- if (!entry.isDirectory()) continue;
1518
- const dirPath = path7.join(packagesDir, entry.name);
1519
- map.set(entry.name, dirPath);
1520
- const packageJsonPath = path7.join(dirPath, "package.json");
1521
- if (fs9.existsSync(packageJsonPath)) {
1522
- try {
1523
- const pkg = JSON.parse(fs9.readFileSync(packageJsonPath, "utf-8"));
1524
- if (pkg.name) {
1525
- map.set(pkg.name, dirPath);
1526
- }
1527
- } catch {
1528
- }
1529
- }
1456
+ const packageNotes = {};
1457
+ for (const ctx of contexts) {
1458
+ packageNotes[ctx.packageName] = formatVersion(ctx);
1530
1459
  }
1531
- return map;
1460
+ return { packageNotes, files };
1532
1461
  }
1533
- function detectMonorepo(cwd) {
1534
- const pnpmWorkspacesPath = path7.join(cwd, "pnpm-workspace.yaml");
1535
- const packageJsonPath = path7.join(cwd, "package.json");
1536
- if (fs9.existsSync(pnpmWorkspacesPath)) {
1537
- const content = fs9.readFileSync(pnpmWorkspacesPath, "utf-8");
1538
- const packagesMatch = content.match(/packages:\s*\n\s*-\s*['"]([^'"]+)['"]/);
1539
- if (packagesMatch?.[1]) {
1540
- const packagesGlob = packagesMatch[1];
1541
- const packagesPath = packagesGlob.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1542
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1543
- }
1544
- return { isMonorepo: true, packagesPath: "packages" };
1545
- }
1546
- if (fs9.existsSync(packageJsonPath)) {
1547
- try {
1548
- const content = fs9.readFileSync(packageJsonPath, "utf-8");
1549
- const pkg = JSON.parse(content);
1550
- if (pkg.workspaces) {
1551
- const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
1552
- if (workspaces?.length) {
1553
- const firstWorkspace = workspaces[0];
1554
- if (firstWorkspace) {
1555
- const packagesPath = firstWorkspace.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1556
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1557
- }
1558
- }
1559
- }
1560
- } catch {
1561
- return { isMonorepo: false, packagesPath: "" };
1562
- }
1563
- }
1564
- return { isMonorepo: false, packagesPath: "" };
1462
+ async function processInput(inputJson, config, dryRun) {
1463
+ const input = parsePackageVersioner(inputJson);
1464
+ return runPipeline(input, config, dryRun);
1565
1465
  }
1566
1466
 
1567
1467
  export {
@@ -1580,14 +1480,9 @@ export {
1580
1480
  parsePackageVersioner,
1581
1481
  parsePackageVersionerFile,
1582
1482
  parsePackageVersionerStdin,
1583
- renderMarkdown,
1584
- writeMarkdown,
1585
1483
  renderJson,
1586
1484
  writeJson,
1587
1485
  createTemplateContext,
1588
1486
  runPipeline,
1589
- processInput,
1590
- aggregateToRoot,
1591
- writeMonorepoChangelogs,
1592
- detectMonorepo
1487
+ processInput
1593
1488
  };