@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.
package/dist/cli.cjs CHANGED
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,6 +30,295 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
33
+ // src/output/markdown.ts
34
+ function groupEntriesByType(entries) {
35
+ const grouped = /* @__PURE__ */ new Map();
36
+ for (const type of TYPE_ORDER) {
37
+ grouped.set(type, []);
38
+ }
39
+ for (const entry of entries) {
40
+ const existing = grouped.get(entry.type) ?? [];
41
+ existing.push(entry);
42
+ grouped.set(entry.type, existing);
43
+ }
44
+ return grouped;
45
+ }
46
+ function formatEntry(entry) {
47
+ let line;
48
+ if (entry.breaking && entry.scope) {
49
+ line = `- **BREAKING** **${entry.scope}**: ${entry.description}`;
50
+ } else if (entry.breaking) {
51
+ line = `- **BREAKING** ${entry.description}`;
52
+ } else if (entry.scope) {
53
+ line = `- **${entry.scope}**: ${entry.description}`;
54
+ } else {
55
+ line = `- ${entry.description}`;
56
+ }
57
+ if (entry.issueIds && entry.issueIds.length > 0) {
58
+ line += ` (${entry.issueIds.join(", ")})`;
59
+ }
60
+ return line;
61
+ }
62
+ function formatVersion(context) {
63
+ const lines = [];
64
+ const versionHeader = context.previousVersion ? `## [${context.version}]` : `## ${context.version}`;
65
+ lines.push(`${versionHeader} - ${context.date}`);
66
+ lines.push("");
67
+ if (context.compareUrl) {
68
+ lines.push(`[Full Changelog](${context.compareUrl})`);
69
+ lines.push("");
70
+ }
71
+ if (context.enhanced?.summary) {
72
+ lines.push(context.enhanced.summary);
73
+ lines.push("");
74
+ }
75
+ const grouped = groupEntriesByType(context.entries);
76
+ for (const [type, entries] of grouped) {
77
+ if (entries.length === 0) continue;
78
+ lines.push(`### ${TYPE_LABELS[type]}`);
79
+ for (const entry of entries) {
80
+ lines.push(formatEntry(entry));
81
+ }
82
+ lines.push("");
83
+ }
84
+ return lines.join("\n");
85
+ }
86
+ function formatHeader() {
87
+ return `# Changelog
88
+
89
+ All notable changes to this project will be documented in this file.
90
+
91
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
92
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
93
+
94
+ `;
95
+ }
96
+ function renderMarkdown(contexts) {
97
+ const sections = [formatHeader()];
98
+ for (const context of contexts) {
99
+ sections.push(formatVersion(context));
100
+ }
101
+ return sections.join("\n");
102
+ }
103
+ function prependVersion(existingPath, context) {
104
+ let existing = "";
105
+ if (fs2.existsSync(existingPath)) {
106
+ existing = fs2.readFileSync(existingPath, "utf-8");
107
+ const headerEnd = existing.indexOf("\n## ");
108
+ if (headerEnd >= 0) {
109
+ const header = existing.slice(0, headerEnd);
110
+ const body = existing.slice(headerEnd + 1);
111
+ const newVersion = formatVersion(context);
112
+ return `${header}
113
+
114
+ ${newVersion}
115
+ ${body}`;
116
+ }
117
+ }
118
+ return renderMarkdown([context]);
119
+ }
120
+ function writeMarkdown(outputPath, contexts, config, dryRun) {
121
+ const content = renderMarkdown(contexts);
122
+ if (dryRun) {
123
+ (0, import_core5.info)(`Would write changelog to ${outputPath}`);
124
+ (0, import_core5.debug)("--- Changelog Preview ---");
125
+ (0, import_core5.debug)(content);
126
+ (0, import_core5.debug)("--- End Preview ---");
127
+ return;
128
+ }
129
+ const dir = path.dirname(outputPath);
130
+ if (!fs2.existsSync(dir)) {
131
+ fs2.mkdirSync(dir, { recursive: true });
132
+ }
133
+ if (outputPath === "-") {
134
+ process.stdout.write(content);
135
+ return;
136
+ }
137
+ if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
138
+ const firstContext = contexts[0];
139
+ if (firstContext) {
140
+ const updated = prependVersion(outputPath, firstContext);
141
+ fs2.writeFileSync(outputPath, updated, "utf-8");
142
+ }
143
+ } else {
144
+ fs2.writeFileSync(outputPath, content, "utf-8");
145
+ }
146
+ (0, import_core5.success)(`Changelog written to ${outputPath}`);
147
+ }
148
+ var fs2, path, import_core5, TYPE_ORDER, TYPE_LABELS;
149
+ var init_markdown = __esm({
150
+ "src/output/markdown.ts"() {
151
+ "use strict";
152
+ fs2 = __toESM(require("fs"), 1);
153
+ path = __toESM(require("path"), 1);
154
+ import_core5 = require("@releasekit/core");
155
+ TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
156
+ TYPE_LABELS = {
157
+ added: "Added",
158
+ changed: "Changed",
159
+ deprecated: "Deprecated",
160
+ removed: "Removed",
161
+ fixed: "Fixed",
162
+ security: "Security"
163
+ };
164
+ }
165
+ });
166
+
167
+ // src/monorepo/splitter.ts
168
+ function splitByPackage(contexts) {
169
+ const byPackage = /* @__PURE__ */ new Map();
170
+ for (const ctx of contexts) {
171
+ byPackage.set(ctx.packageName, ctx);
172
+ }
173
+ return byPackage;
174
+ }
175
+ var init_splitter = __esm({
176
+ "src/monorepo/splitter.ts"() {
177
+ "use strict";
178
+ }
179
+ });
180
+
181
+ // src/monorepo/aggregator.ts
182
+ var aggregator_exports = {};
183
+ __export(aggregator_exports, {
184
+ aggregateToRoot: () => aggregateToRoot,
185
+ detectMonorepo: () => detectMonorepo,
186
+ splitByPackage: () => splitByPackage,
187
+ writeMonorepoChangelogs: () => writeMonorepoChangelogs
188
+ });
189
+ function writeFile(outputPath, content, dryRun) {
190
+ if (dryRun) {
191
+ (0, import_core8.info)(`Would write to ${outputPath}`);
192
+ (0, import_core8.debug)(content);
193
+ return false;
194
+ }
195
+ const dir = path6.dirname(outputPath);
196
+ if (!fs8.existsSync(dir)) {
197
+ fs8.mkdirSync(dir, { recursive: true });
198
+ }
199
+ fs8.writeFileSync(outputPath, content, "utf-8");
200
+ (0, import_core8.success)(`Changelog written to ${outputPath}`);
201
+ return true;
202
+ }
203
+ function aggregateToRoot(contexts) {
204
+ const aggregated = {
205
+ packageName: "monorepo",
206
+ version: contexts[0]?.version ?? "0.0.0",
207
+ previousVersion: contexts[0]?.previousVersion ?? null,
208
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
209
+ repoUrl: contexts[0]?.repoUrl ?? null,
210
+ entries: []
211
+ };
212
+ for (const ctx of contexts) {
213
+ for (const entry of ctx.entries) {
214
+ aggregated.entries.push({
215
+ ...entry,
216
+ scope: entry.scope ? `${ctx.packageName}/${entry.scope}` : ctx.packageName
217
+ });
218
+ }
219
+ }
220
+ return aggregated;
221
+ }
222
+ function writeMonorepoChangelogs(contexts, options, config, dryRun) {
223
+ const files = [];
224
+ if (options.mode === "root" || options.mode === "both") {
225
+ const aggregated = aggregateToRoot(contexts);
226
+ const rootPath = path6.join(options.rootPath, "CHANGELOG.md");
227
+ (0, import_core8.info)(`Writing root changelog to ${rootPath}`);
228
+ const rootContent = config.updateStrategy === "prepend" && fs8.existsSync(rootPath) ? prependVersion(rootPath, aggregated) : renderMarkdown([aggregated]);
229
+ if (writeFile(rootPath, rootContent, dryRun)) {
230
+ files.push(rootPath);
231
+ }
232
+ }
233
+ if (options.mode === "packages" || options.mode === "both") {
234
+ const byPackage = splitByPackage(contexts);
235
+ const packageDirMap = buildPackageDirMap(options.rootPath, options.packagesPath);
236
+ for (const [packageName, ctx] of byPackage) {
237
+ const simpleName = packageName.split("/").pop();
238
+ const packageDir = packageDirMap.get(packageName) ?? (simpleName ? packageDirMap.get(simpleName) : void 0) ?? null;
239
+ if (packageDir) {
240
+ const changelogPath = path6.join(packageDir, "CHANGELOG.md");
241
+ (0, import_core8.info)(`Writing changelog for ${packageName} to ${changelogPath}`);
242
+ const pkgContent = config.updateStrategy === "prepend" && fs8.existsSync(changelogPath) ? prependVersion(changelogPath, ctx) : renderMarkdown([ctx]);
243
+ if (writeFile(changelogPath, pkgContent, dryRun)) {
244
+ files.push(changelogPath);
245
+ }
246
+ } else {
247
+ (0, import_core8.info)(`Could not find directory for package ${packageName}, skipping`);
248
+ }
249
+ }
250
+ }
251
+ return files;
252
+ }
253
+ function buildPackageDirMap(rootPath, packagesPath) {
254
+ const map = /* @__PURE__ */ new Map();
255
+ const packagesDir = path6.join(rootPath, packagesPath);
256
+ if (!fs8.existsSync(packagesDir)) {
257
+ return map;
258
+ }
259
+ for (const entry of fs8.readdirSync(packagesDir, { withFileTypes: true })) {
260
+ if (!entry.isDirectory()) continue;
261
+ const dirPath = path6.join(packagesDir, entry.name);
262
+ map.set(entry.name, dirPath);
263
+ const packageJsonPath = path6.join(dirPath, "package.json");
264
+ if (fs8.existsSync(packageJsonPath)) {
265
+ try {
266
+ const pkg = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
267
+ if (pkg.name) {
268
+ map.set(pkg.name, dirPath);
269
+ }
270
+ } catch {
271
+ }
272
+ }
273
+ }
274
+ return map;
275
+ }
276
+ function detectMonorepo(cwd) {
277
+ const pnpmWorkspacesPath = path6.join(cwd, "pnpm-workspace.yaml");
278
+ const packageJsonPath = path6.join(cwd, "package.json");
279
+ if (fs8.existsSync(pnpmWorkspacesPath)) {
280
+ const content = fs8.readFileSync(pnpmWorkspacesPath, "utf-8");
281
+ const packagesMatch = content.match(/packages:\s*\n\s*-\s*['"]([^'"]+)['"]/);
282
+ if (packagesMatch?.[1]) {
283
+ const packagesGlob = packagesMatch[1];
284
+ const packagesPath = packagesGlob.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
285
+ return { isMonorepo: true, packagesPath: packagesPath || "packages" };
286
+ }
287
+ return { isMonorepo: true, packagesPath: "packages" };
288
+ }
289
+ if (fs8.existsSync(packageJsonPath)) {
290
+ try {
291
+ const content = fs8.readFileSync(packageJsonPath, "utf-8");
292
+ const pkg = JSON.parse(content);
293
+ if (pkg.workspaces) {
294
+ const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
295
+ if (workspaces?.length) {
296
+ const firstWorkspace = workspaces[0];
297
+ if (firstWorkspace) {
298
+ const packagesPath = firstWorkspace.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
299
+ return { isMonorepo: true, packagesPath: packagesPath || "packages" };
300
+ }
301
+ }
302
+ }
303
+ } catch {
304
+ return { isMonorepo: false, packagesPath: "" };
305
+ }
306
+ }
307
+ return { isMonorepo: false, packagesPath: "" };
308
+ }
309
+ var fs8, path6, import_core8;
310
+ var init_aggregator = __esm({
311
+ "src/monorepo/aggregator.ts"() {
312
+ "use strict";
313
+ fs8 = __toESM(require("fs"), 1);
314
+ path6 = __toESM(require("path"), 1);
315
+ import_core8 = require("@releasekit/core");
316
+ init_markdown();
317
+ init_splitter();
318
+ init_splitter();
319
+ }
320
+ });
321
+
26
322
  // src/cli.ts
27
323
  var fs10 = __toESM(require("fs"), 1);
28
324
  var readline = __toESM(require("readline"), 1);
@@ -43,9 +339,9 @@ function getDefaultConfig() {
43
339
  }
44
340
 
45
341
  // src/core/pipeline.ts
46
- var fs8 = __toESM(require("fs"), 1);
47
- var path6 = __toESM(require("path"), 1);
48
- var import_core8 = require("@releasekit/core");
342
+ var fs9 = __toESM(require("fs"), 1);
343
+ var path7 = __toESM(require("path"), 1);
344
+ var import_core9 = require("@releasekit/core");
49
345
 
50
346
  // src/input/package-versioner.ts
51
347
  var fs = __toESM(require("fs"), 1);
@@ -354,6 +650,83 @@ var OpenAICompatibleProvider = class extends BaseLLMProvider {
354
650
 
355
651
  // src/llm/tasks/categorize.ts
356
652
  var import_core3 = require("@releasekit/core");
653
+
654
+ // src/llm/prompts.ts
655
+ function resolvePrompt(taskName, defaultPrompt, promptsConfig) {
656
+ if (!promptsConfig) return defaultPrompt;
657
+ const fullTemplate = promptsConfig.templates?.[taskName];
658
+ if (fullTemplate) return fullTemplate;
659
+ const additionalInstructions = promptsConfig.instructions?.[taskName];
660
+ if (additionalInstructions) {
661
+ const insertionPoint = defaultPrompt.lastIndexOf("Output only valid JSON");
662
+ if (insertionPoint !== -1) {
663
+ return `${defaultPrompt.slice(0, insertionPoint)}Additional instructions:
664
+ ${additionalInstructions}
665
+
666
+ ${defaultPrompt.slice(insertionPoint)}`;
667
+ }
668
+ return `${defaultPrompt}
669
+
670
+ Additional instructions:
671
+ ${additionalInstructions}`;
672
+ }
673
+ return defaultPrompt;
674
+ }
675
+
676
+ // src/llm/scopes.ts
677
+ function getAllowedScopesFromCategories(categories) {
678
+ const scopeMap = /* @__PURE__ */ new Map();
679
+ for (const cat of categories) {
680
+ if (cat.scopes && cat.scopes.length > 0) {
681
+ scopeMap.set(cat.name, cat.scopes);
682
+ }
683
+ }
684
+ return scopeMap;
685
+ }
686
+ function resolveAllowedScopes(scopeConfig, categories, packageNames) {
687
+ if (!scopeConfig || scopeConfig.mode === "unrestricted") return null;
688
+ if (scopeConfig.mode === "none") return [];
689
+ if (scopeConfig.mode === "packages") return packageNames ?? [];
690
+ if (scopeConfig.mode === "restricted") {
691
+ const explicit = scopeConfig.rules?.allowed ?? [];
692
+ const all = new Set(explicit);
693
+ if (categories) {
694
+ const fromCategories = getAllowedScopesFromCategories(categories);
695
+ for (const scopes of fromCategories.values()) {
696
+ for (const s of scopes) all.add(s);
697
+ }
698
+ }
699
+ return [...all];
700
+ }
701
+ return null;
702
+ }
703
+ function validateScope(scope, allowedScopes, rules) {
704
+ if (!scope || allowedScopes === null) return scope;
705
+ if (allowedScopes.length === 0) return void 0;
706
+ const caseSensitive = rules?.caseSensitive ?? false;
707
+ const normalise = (s) => caseSensitive ? s : s.toLowerCase();
708
+ const isAllowed = allowedScopes.some((a) => normalise(a) === normalise(scope));
709
+ if (isAllowed) return scope;
710
+ switch (rules?.invalidScopeAction ?? "remove") {
711
+ case "keep":
712
+ return scope;
713
+ case "fallback":
714
+ return rules?.fallbackScope;
715
+ case "remove":
716
+ default:
717
+ return void 0;
718
+ }
719
+ }
720
+ function validateEntryScopes(entries, scopeConfig, categories) {
721
+ const allowedScopes = resolveAllowedScopes(scopeConfig, categories);
722
+ if (allowedScopes === null) return entries;
723
+ return entries.map((entry) => ({
724
+ ...entry,
725
+ scope: validateScope(entry.scope, allowedScopes, scopeConfig?.rules)
726
+ }));
727
+ }
728
+
729
+ // src/llm/tasks/categorize.ts
357
730
  var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
358
731
 
359
732
  Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
@@ -365,20 +738,21 @@ Entries:
365
738
 
366
739
  Output only valid JSON, nothing else:`;
367
740
  function buildCustomCategorizePrompt(categories) {
368
- const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
369
- const developerCategory = categories.find((c) => c.name === "Developer");
741
+ const categoryList = categories.map((c) => {
742
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
743
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
744
+ }).join("\n");
745
+ const scopeMap = getAllowedScopesFromCategories(categories);
370
746
  let scopeInstructions = "";
371
- if (developerCategory) {
372
- const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
373
- if (scopeMatch?.[1]) {
374
- const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
375
- if (scopes.length > 0) {
376
- scopeInstructions = `
377
-
378
- For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
379
- `;
380
- }
747
+ if (scopeMap.size > 0) {
748
+ const entries = [];
749
+ for (const [catName, scopes] of scopeMap) {
750
+ entries.push(`For "${catName}", assign a scope from: ${scopes.join(", ")}.`);
381
751
  }
752
+ scopeInstructions = `
753
+
754
+ ${entries.join("\n")}
755
+ Only use scopes from these predefined lists. If an entry does not fit any scope, set scope to null.`;
382
756
  }
383
757
  return `You are categorizing changelog entries for a software release.
384
758
 
@@ -386,9 +760,10 @@ Given the following entries, group them into the specified categories. Only use
386
760
 
387
761
  Categories:
388
762
  ${categoryList}${scopeInstructions}
763
+
389
764
  Output a JSON object with two fields:
390
765
  - "categories": an object where keys are category names and values are arrays of entry indices (0-based)
391
- - "scopes": an object where keys are entry indices (as strings) and values are scope labels
766
+ - "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.
392
767
 
393
768
  Entries:
394
769
  {{entries}}
@@ -399,9 +774,11 @@ async function categorizeEntries(provider, entries, context) {
399
774
  if (entries.length === 0) {
400
775
  return [];
401
776
  }
402
- const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
777
+ const entriesCopy = entries.map((e) => ({ ...e, scope: void 0 }));
778
+ const entriesText = entriesCopy.map((e, i) => `${i}. [${e.type}]: ${e.description}`).join("\n");
403
779
  const hasCustomCategories = context.categories && context.categories.length > 0;
404
- const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
780
+ const defaultPrompt = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
781
+ const promptTemplate = resolvePrompt("categorize", defaultPrompt, context.prompts);
405
782
  const prompt = promptTemplate.replace("{{entries}}", entriesText);
406
783
  try {
407
784
  const response = await provider.complete(prompt);
@@ -413,13 +790,14 @@ async function categorizeEntries(provider, entries, context) {
413
790
  const scopeMap = parsed.scopes || {};
414
791
  for (const [indexStr, scope] of Object.entries(scopeMap)) {
415
792
  const idx = Number.parseInt(indexStr, 10);
416
- if (entries[idx] && scope) {
417
- entries[idx] = { ...entries[idx], scope };
793
+ if (entriesCopy[idx] && scope && scope.trim()) {
794
+ entriesCopy[idx] = { ...entriesCopy[idx], scope: scope.trim() };
418
795
  }
419
796
  }
797
+ const validatedEntries = validateEntryScopes(entriesCopy, context.scopes, context.categories);
420
798
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
421
799
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
422
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
800
+ const categoryEntries = indices.map((i) => validatedEntries[i]).filter((e) => e !== void 0);
423
801
  if (categoryEntries.length > 0) {
424
802
  result.push({ category, entries: categoryEntries });
425
803
  }
@@ -428,7 +806,7 @@ async function categorizeEntries(provider, entries, context) {
428
806
  const categoryMap = parsed;
429
807
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
430
808
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
431
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
809
+ const categoryEntries = indices.map((i) => entriesCopy[i]).filter((e) => e !== void 0);
432
810
  if (categoryEntries.length > 0) {
433
811
  result.push({ category, entries: categoryEntries });
434
812
  }
@@ -439,12 +817,12 @@ async function categorizeEntries(provider, entries, context) {
439
817
  (0, import_core3.warn)(
440
818
  `LLM categorization failed, falling back to General: ${error2 instanceof Error ? error2.message : String(error2)}`
441
819
  );
442
- return [{ category: "General", entries }];
820
+ return [{ category: "General", entries: entriesCopy }];
443
821
  }
444
822
  }
445
823
 
446
824
  // src/llm/tasks/enhance.ts
447
- var ENHANCE_PROMPT = `You are improving changelog entries for a software project.
825
+ var DEFAULT_ENHANCE_PROMPT = `You are improving changelog entries for a software project.
448
826
  Given a technical commit message, rewrite it as a clear, user-friendly changelog entry.
449
827
 
450
828
  Rules:
@@ -460,9 +838,10 @@ Type: {{type}}
460
838
  Description: {{description}}
461
839
 
462
840
  Rewritten description (only output the new description, nothing else):`;
463
- async function enhanceEntry(provider, entry, _context) {
464
- const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
465
- 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);
841
+ async function enhanceEntry(provider, entry, context) {
842
+ const styleText = context.style ? `- ${context.style}` : '- Use present tense ("Add feature" not "Added feature")';
843
+ 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);
844
+ const prompt = resolvePrompt("enhance", defaultPrompt, context.prompts);
466
845
  const response = await provider.complete(prompt);
467
846
  return response.trim();
468
847
  }
@@ -518,7 +897,23 @@ function buildPrompt(entries, categories, style) {
518
897
  const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
519
898
  const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
520
899
  const categorySection = categories ? `Categories (use ONLY these):
521
- ${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
900
+ ${categories.map((c) => {
901
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
902
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
903
+ }).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
904
+ let scopeInstruction = "";
905
+ if (categories) {
906
+ const scopeMap = getAllowedScopesFromCategories(categories);
907
+ if (scopeMap.size > 0) {
908
+ const parts = [];
909
+ for (const [catName, scopes] of scopeMap) {
910
+ parts.push(`For "${catName}" entries, assign a scope from: ${scopes.join(", ")}.`);
911
+ }
912
+ scopeInstruction = `
913
+ ${parts.join("\n")}
914
+ Only use scopes from these predefined lists. Set scope to null if no scope applies.`;
915
+ }
916
+ }
522
917
  return `You are generating release notes for a software project. Given the following changelog entries, do two things:
523
918
 
524
919
  1. **Rewrite** each entry as a clear, user-friendly description
@@ -529,9 +924,7 @@ Style guidelines:
529
924
  - Be concise (1 short sentence per entry)
530
925
  - Focus on what changed, not implementation details
531
926
 
532
- ${categorySection}
533
-
534
- ${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
927
+ ${categorySection}${scopeInstruction}
535
928
 
536
929
  Entries:
537
930
  ${entriesText}
@@ -548,7 +941,8 @@ async function enhanceAndCategorize(provider, entries, context) {
548
941
  const retryOpts = LLM_DEFAULTS.retry;
549
942
  try {
550
943
  return await withRetry(async () => {
551
- const prompt = buildPrompt(entries, context.categories, context.style);
944
+ const defaultPrompt = buildPrompt(entries, context.categories, context.style);
945
+ const prompt = resolvePrompt("enhanceAndCategorize", defaultPrompt, context.prompts);
552
946
  const response = await provider.complete(prompt);
553
947
  const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
554
948
  const parsed = JSON.parse(cleaned);
@@ -564,22 +958,23 @@ async function enhanceAndCategorize(provider, entries, context) {
564
958
  scope: result.scope || original.scope
565
959
  };
566
960
  });
961
+ const validatedEntries = validateEntryScopes(enhancedEntries, context.scopes, context.categories);
567
962
  const categoryMap = /* @__PURE__ */ new Map();
568
963
  for (let i = 0; i < parsed.entries.length; i++) {
569
964
  const result = parsed.entries[i];
570
965
  const category = result?.category || "General";
571
- const entry = enhancedEntries[i];
966
+ const entry = validatedEntries[i];
572
967
  if (!entry) continue;
573
968
  if (!categoryMap.has(category)) {
574
969
  categoryMap.set(category, []);
575
970
  }
576
- categoryMap.get(category).push(entry);
971
+ categoryMap.get(category)?.push(entry);
577
972
  }
578
973
  const categories = [];
579
974
  for (const [category, catEntries] of categoryMap) {
580
975
  categories.push({ category, entries: catEntries });
581
976
  }
582
- return { enhancedEntries, categories };
977
+ return { enhancedEntries: validatedEntries, categories };
583
978
  }, retryOpts);
584
979
  } catch (error2) {
585
980
  (0, import_core4.warn)(
@@ -593,7 +988,7 @@ async function enhanceAndCategorize(provider, entries, context) {
593
988
  }
594
989
 
595
990
  // src/llm/tasks/release-notes.ts
596
- var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
991
+ var DEFAULT_RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
597
992
 
598
993
  Create engaging, user-friendly release notes for the following changes.
599
994
 
@@ -626,16 +1021,17 @@ No notable changes in this release.`;
626
1021
  if (e.breaking) line += " **BREAKING**";
627
1022
  return line;
628
1023
  }).join("\n");
629
- const prompt = RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
1024
+ const defaultPrompt = DEFAULT_RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
630
1025
  "{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}",
631
1026
  context.previousVersion ? `Previous version: ${context.previousVersion}` : ""
632
1027
  ).replace("{{date}}", context.date ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "").replace("{{entries}}", entriesText);
1028
+ const prompt = resolvePrompt("releaseNotes", defaultPrompt, context.prompts);
633
1029
  const response = await provider.complete(prompt);
634
1030
  return response.trim();
635
1031
  }
636
1032
 
637
1033
  // src/llm/tasks/summarize.ts
638
- var SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
1034
+ var DEFAULT_SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
639
1035
 
640
1036
  Given the following changelog entries, create a brief summary (2-3 sentences) that captures the main themes of this release.
641
1037
 
@@ -643,12 +1039,13 @@ Entries:
643
1039
  {{entries}}
644
1040
 
645
1041
  Summary (only output the summary, nothing else):`;
646
- async function summarizeEntries(provider, entries, _context) {
1042
+ async function summarizeEntries(provider, entries, context) {
647
1043
  if (entries.length === 0) {
648
1044
  return "";
649
1045
  }
650
1046
  const entriesText = entries.map((e) => `- [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
651
- const prompt = SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
1047
+ const defaultPrompt = DEFAULT_SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
1048
+ const prompt = resolvePrompt("summarize", defaultPrompt, context.prompts);
652
1049
  const response = await provider.complete(prompt);
653
1050
  return response.trim();
654
1051
  }
@@ -693,135 +1090,7 @@ function createProvider(config) {
693
1090
  // src/output/github-release.ts
694
1091
  var import_rest = require("@octokit/rest");
695
1092
  var import_core6 = require("@releasekit/core");
696
-
697
- // src/output/markdown.ts
698
- var fs2 = __toESM(require("fs"), 1);
699
- var path = __toESM(require("path"), 1);
700
- var import_core5 = require("@releasekit/core");
701
- var TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
702
- var TYPE_LABELS = {
703
- added: "Added",
704
- changed: "Changed",
705
- deprecated: "Deprecated",
706
- removed: "Removed",
707
- fixed: "Fixed",
708
- security: "Security"
709
- };
710
- function groupEntriesByType(entries) {
711
- const grouped = /* @__PURE__ */ new Map();
712
- for (const type of TYPE_ORDER) {
713
- grouped.set(type, []);
714
- }
715
- for (const entry of entries) {
716
- const existing = grouped.get(entry.type) ?? [];
717
- existing.push(entry);
718
- grouped.set(entry.type, existing);
719
- }
720
- return grouped;
721
- }
722
- function formatEntry(entry) {
723
- let line;
724
- if (entry.breaking && entry.scope) {
725
- line = `- **BREAKING** **${entry.scope}**: ${entry.description}`;
726
- } else if (entry.breaking) {
727
- line = `- **BREAKING** ${entry.description}`;
728
- } else if (entry.scope) {
729
- line = `- **${entry.scope}**: ${entry.description}`;
730
- } else {
731
- line = `- ${entry.description}`;
732
- }
733
- if (entry.issueIds && entry.issueIds.length > 0) {
734
- line += ` (${entry.issueIds.join(", ")})`;
735
- }
736
- return line;
737
- }
738
- function formatVersion(context) {
739
- const lines = [];
740
- const versionHeader = context.previousVersion ? `## [${context.version}]` : `## ${context.version}`;
741
- lines.push(`${versionHeader} - ${context.date}`);
742
- lines.push("");
743
- if (context.compareUrl) {
744
- lines.push(`[Full Changelog](${context.compareUrl})`);
745
- lines.push("");
746
- }
747
- if (context.enhanced?.summary) {
748
- lines.push(context.enhanced.summary);
749
- lines.push("");
750
- }
751
- const grouped = groupEntriesByType(context.entries);
752
- for (const [type, entries] of grouped) {
753
- if (entries.length === 0) continue;
754
- lines.push(`### ${TYPE_LABELS[type]}`);
755
- for (const entry of entries) {
756
- lines.push(formatEntry(entry));
757
- }
758
- lines.push("");
759
- }
760
- return lines.join("\n");
761
- }
762
- function formatHeader() {
763
- return `# Changelog
764
-
765
- All notable changes to this project will be documented in this file.
766
-
767
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
768
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
769
-
770
- `;
771
- }
772
- function renderMarkdown(contexts) {
773
- const sections = [formatHeader()];
774
- for (const context of contexts) {
775
- sections.push(formatVersion(context));
776
- }
777
- return sections.join("\n");
778
- }
779
- function prependVersion(existingPath, context) {
780
- let existing = "";
781
- if (fs2.existsSync(existingPath)) {
782
- existing = fs2.readFileSync(existingPath, "utf-8");
783
- const headerEnd = existing.indexOf("\n## ");
784
- if (headerEnd >= 0) {
785
- const header = existing.slice(0, headerEnd);
786
- const body = existing.slice(headerEnd + 1);
787
- const newVersion = formatVersion(context);
788
- return `${header}
789
-
790
- ${newVersion}
791
- ${body}`;
792
- }
793
- }
794
- return renderMarkdown([context]);
795
- }
796
- function writeMarkdown(outputPath, contexts, config, dryRun) {
797
- const content = renderMarkdown(contexts);
798
- if (dryRun) {
799
- (0, import_core5.info)("--- Changelog Preview ---");
800
- console.log(content);
801
- (0, import_core5.info)("--- End Preview ---");
802
- return;
803
- }
804
- const dir = path.dirname(outputPath);
805
- if (!fs2.existsSync(dir)) {
806
- fs2.mkdirSync(dir, { recursive: true });
807
- }
808
- if (outputPath === "-") {
809
- process.stdout.write(content);
810
- return;
811
- }
812
- if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
813
- const firstContext = contexts[0];
814
- if (firstContext) {
815
- const updated = prependVersion(outputPath, firstContext);
816
- fs2.writeFileSync(outputPath, updated, "utf-8");
817
- }
818
- } else {
819
- fs2.writeFileSync(outputPath, content, "utf-8");
820
- }
821
- (0, import_core5.success)(`Changelog written to ${outputPath}`);
822
- }
823
-
824
- // src/output/github-release.ts
1093
+ init_markdown();
825
1094
  var GitHubClient = class {
826
1095
  octokit;
827
1096
  owner;
@@ -957,9 +1226,10 @@ function renderJson(contexts) {
957
1226
  function writeJson(outputPath, contexts, dryRun) {
958
1227
  const content = renderJson(contexts);
959
1228
  if (dryRun) {
960
- (0, import_core7.info)("--- JSON Output Preview ---");
961
- console.log(content);
962
- (0, import_core7.info)("--- End Preview ---");
1229
+ (0, import_core7.info)(`Would write JSON output to ${outputPath}`);
1230
+ (0, import_core7.debug)("--- JSON Output Preview ---");
1231
+ (0, import_core7.debug)(content);
1232
+ (0, import_core7.debug)("--- End Preview ---");
963
1233
  return;
964
1234
  }
965
1235
  const dir = path2.dirname(outputPath);
@@ -970,6 +1240,9 @@ function writeJson(outputPath, contexts, dryRun) {
970
1240
  (0, import_core7.success)(`JSON output written to ${outputPath}`);
971
1241
  }
972
1242
 
1243
+ // src/core/pipeline.ts
1244
+ init_markdown();
1245
+
973
1246
  // src/templates/ejs.ts
974
1247
  var fs4 = __toESM(require("fs"), 1);
975
1248
  var import_ejs = __toESM(require("ejs"), 1);
@@ -1226,17 +1499,27 @@ function renderTemplate(templatePath, context, engine) {
1226
1499
 
1227
1500
  // src/core/pipeline.ts
1228
1501
  var import_meta = {};
1229
- function generateCompareUrl(repoUrl, from, to) {
1502
+ function generateCompareUrl(repoUrl, from, to, packageName) {
1503
+ const isPackageSpecific = from.includes("@") && packageName && from.includes(packageName);
1504
+ let fromVersion;
1505
+ let toVersion;
1506
+ if (isPackageSpecific) {
1507
+ fromVersion = from;
1508
+ toVersion = `${packageName}@${to.startsWith("v") ? "" : "v"}${to}`;
1509
+ } else {
1510
+ fromVersion = from.replace(/^v/, "");
1511
+ toVersion = to.replace(/^v/, "");
1512
+ }
1230
1513
  if (/gitlab\.com/i.test(repoUrl)) {
1231
- return `${repoUrl}/-/compare/${from}...${to}`;
1514
+ return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
1232
1515
  }
1233
1516
  if (/bitbucket\.org/i.test(repoUrl)) {
1234
- return `${repoUrl}/branches/compare/${from}..${to}`;
1517
+ return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
1235
1518
  }
1236
- return `${repoUrl}/compare/${from}...${to}`;
1519
+ return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
1237
1520
  }
1238
1521
  function createTemplateContext(pkg) {
1239
- const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
1522
+ const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version, pkg.packageName) : void 0;
1240
1523
  return {
1241
1524
  packageName: pkg.packageName,
1242
1525
  version: pkg.version,
@@ -1274,15 +1557,17 @@ async function processWithLLM(context, config) {
1274
1557
  previousVersion: context.previousVersion ?? void 0,
1275
1558
  date: context.date,
1276
1559
  categories: config.llm.categories,
1277
- style: config.llm.style
1560
+ style: config.llm.style,
1561
+ scopes: config.llm.scopes,
1562
+ prompts: config.llm.prompts
1278
1563
  };
1279
1564
  const enhanced = {
1280
1565
  entries: context.entries
1281
1566
  };
1282
1567
  try {
1283
- (0, import_core8.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1568
+ (0, import_core9.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1284
1569
  if (config.llm.baseURL) {
1285
- (0, import_core8.info)(`LLM base URL: ${config.llm.baseURL}`);
1570
+ (0, import_core9.info)(`LLM base URL: ${config.llm.baseURL}`);
1286
1571
  }
1287
1572
  const rawProvider = createProvider(config.llm);
1288
1573
  const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
@@ -1291,59 +1576,58 @@ async function processWithLLM(context, config) {
1291
1576
  complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
1292
1577
  };
1293
1578
  const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
1294
- (0, import_core8.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
1579
+ (0, import_core9.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
1295
1580
  if (tasks.enhance && tasks.categorize) {
1296
- (0, import_core8.info)("Enhancing and categorizing entries with LLM...");
1581
+ (0, import_core9.info)("Enhancing and categorizing entries with LLM...");
1297
1582
  const result = await enhanceAndCategorize(provider, context.entries, llmContext);
1298
1583
  enhanced.entries = result.enhancedEntries;
1299
1584
  enhanced.categories = {};
1300
1585
  for (const cat of result.categories) {
1301
1586
  enhanced.categories[cat.category] = cat.entries;
1302
1587
  }
1303
- (0, import_core8.info)(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1588
+ (0, import_core9.info)(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1304
1589
  } else {
1305
1590
  if (tasks.enhance) {
1306
- (0, import_core8.info)("Enhancing entries with LLM...");
1591
+ (0, import_core9.info)("Enhancing entries with LLM...");
1307
1592
  enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1308
- (0, import_core8.info)(`Enhanced ${enhanced.entries.length} entries`);
1593
+ (0, import_core9.info)(`Enhanced ${enhanced.entries.length} entries`);
1309
1594
  }
1310
1595
  if (tasks.categorize) {
1311
- (0, import_core8.info)("Categorizing entries with LLM...");
1596
+ (0, import_core9.info)("Categorizing entries with LLM...");
1312
1597
  const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1313
1598
  enhanced.categories = {};
1314
1599
  for (const cat of categorized) {
1315
1600
  enhanced.categories[cat.category] = cat.entries;
1316
1601
  }
1317
- (0, import_core8.info)(`Created ${categorized.length} categories`);
1602
+ (0, import_core9.info)(`Created ${categorized.length} categories`);
1318
1603
  }
1319
1604
  }
1320
1605
  if (tasks.summarize) {
1321
- (0, import_core8.info)("Summarizing entries with LLM...");
1606
+ (0, import_core9.info)("Summarizing entries with LLM...");
1322
1607
  enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
1323
1608
  if (enhanced.summary) {
1324
- (0, import_core8.info)("Summary generated successfully");
1325
- (0, import_core8.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1609
+ (0, import_core9.info)("Summary generated successfully");
1610
+ (0, import_core9.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1326
1611
  } else {
1327
- (0, import_core8.warn)("Summary generation returned empty result");
1612
+ (0, import_core9.warn)("Summary generation returned empty result");
1328
1613
  }
1329
1614
  }
1330
1615
  if (tasks.releaseNotes) {
1331
- (0, import_core8.info)("Generating release notes with LLM...");
1616
+ (0, import_core9.info)("Generating release notes with LLM...");
1332
1617
  enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
1333
1618
  if (enhanced.releaseNotes) {
1334
- (0, import_core8.info)("Release notes generated successfully");
1619
+ (0, import_core9.info)("Release notes generated successfully");
1335
1620
  } else {
1336
- (0, import_core8.warn)("Release notes generation returned empty result");
1621
+ (0, import_core9.warn)("Release notes generation returned empty result");
1337
1622
  }
1338
1623
  }
1339
1624
  return {
1340
1625
  ...context,
1341
- entries: enhanced.entries,
1342
1626
  enhanced
1343
1627
  };
1344
1628
  } catch (error2) {
1345
- (0, import_core8.warn)(`LLM processing failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
1346
- (0, import_core8.warn)("Falling back to raw entries");
1629
+ (0, import_core9.warn)(`LLM processing failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
1630
+ (0, import_core9.warn)("Falling back to raw entries");
1347
1631
  return context;
1348
1632
  }
1349
1633
  }
@@ -1351,17 +1635,17 @@ function getBuiltinTemplatePath(style) {
1351
1635
  let packageRoot;
1352
1636
  try {
1353
1637
  const currentUrl = import_meta.url;
1354
- packageRoot = path6.dirname(new URL(currentUrl).pathname);
1355
- packageRoot = path6.join(packageRoot, "..", "..");
1638
+ packageRoot = path7.dirname(new URL(currentUrl).pathname);
1639
+ packageRoot = path7.join(packageRoot, "..", "..");
1356
1640
  } catch {
1357
1641
  packageRoot = __dirname;
1358
1642
  }
1359
- return path6.join(packageRoot, "templates", style);
1643
+ return path7.join(packageRoot, "templates", style);
1360
1644
  }
1361
1645
  async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1362
1646
  let templatePath;
1363
1647
  if (config.templates?.path) {
1364
- templatePath = path6.resolve(config.templates.path);
1648
+ templatePath = path7.resolve(config.templates.path);
1365
1649
  } else {
1366
1650
  templatePath = getBuiltinTemplatePath("keep-a-changelog");
1367
1651
  }
@@ -1371,64 +1655,78 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1371
1655
  );
1372
1656
  const result = renderTemplate(templatePath, documentContext, config.templates?.engine);
1373
1657
  if (dryRun) {
1374
- (0, import_core8.info)("--- Changelog Preview ---");
1375
- console.log(result.content);
1376
- (0, import_core8.info)("--- End Preview ---");
1658
+ (0, import_core9.info)(`Would write templated output to ${outputPath}`);
1659
+ (0, import_core9.debug)("--- Changelog Preview ---");
1660
+ (0, import_core9.debug)(result.content);
1661
+ (0, import_core9.debug)("--- End Preview ---");
1377
1662
  return;
1378
1663
  }
1379
1664
  if (outputPath === "-") {
1380
1665
  process.stdout.write(result.content);
1381
1666
  return;
1382
1667
  }
1383
- const dir = path6.dirname(outputPath);
1384
- if (!fs8.existsSync(dir)) {
1385
- fs8.mkdirSync(dir, { recursive: true });
1668
+ const dir = path7.dirname(outputPath);
1669
+ if (!fs9.existsSync(dir)) {
1670
+ fs9.mkdirSync(dir, { recursive: true });
1386
1671
  }
1387
- fs8.writeFileSync(outputPath, result.content, "utf-8");
1388
- (0, import_core8.success)(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1672
+ fs9.writeFileSync(outputPath, result.content, "utf-8");
1673
+ (0, import_core9.success)(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1389
1674
  }
1390
1675
  async function runPipeline(input, config, dryRun) {
1391
- (0, import_core8.debug)(`Processing ${input.packages.length} package(s)`);
1676
+ (0, import_core9.debug)(`Processing ${input.packages.length} package(s)`);
1392
1677
  let contexts = input.packages.map(createTemplateContext);
1393
1678
  if (config.llm && !process.env.CHANGELOG_NO_LLM) {
1394
- (0, import_core8.info)("Processing with LLM enhancement");
1679
+ (0, import_core9.info)("Processing with LLM enhancement");
1395
1680
  contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
1396
1681
  }
1682
+ const files = [];
1397
1683
  for (const output of config.output) {
1398
- (0, import_core8.info)(`Generating ${output.format} output`);
1684
+ (0, import_core9.info)(`Generating ${output.format} output`);
1399
1685
  switch (output.format) {
1400
1686
  case "markdown": {
1401
1687
  const file = output.file ?? "CHANGELOG.md";
1402
- if (config.templates?.path || output.options?.template) {
1403
- await generateWithTemplate(contexts, config, file, dryRun);
1404
- } else {
1405
- writeMarkdown(file, contexts, config, dryRun);
1688
+ try {
1689
+ const effectiveTemplateConfig = output.templates ?? config.templates;
1690
+ if (effectiveTemplateConfig?.path || output.options?.template) {
1691
+ const configWithTemplate = { ...config, templates: effectiveTemplateConfig };
1692
+ await generateWithTemplate(contexts, configWithTemplate, file, dryRun);
1693
+ } else {
1694
+ writeMarkdown(file, contexts, config, dryRun);
1695
+ }
1696
+ if (!dryRun) files.push(file);
1697
+ } catch (error2) {
1698
+ (0, import_core9.warn)(`Failed to write ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
1406
1699
  }
1407
1700
  break;
1408
1701
  }
1409
1702
  case "json": {
1410
1703
  const file = output.file ?? "changelog.json";
1411
- writeJson(file, contexts, dryRun);
1704
+ try {
1705
+ writeJson(file, contexts, dryRun);
1706
+ if (!dryRun) files.push(file);
1707
+ } catch (error2) {
1708
+ (0, import_core9.warn)(`Failed to write ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
1709
+ }
1412
1710
  break;
1413
1711
  }
1414
1712
  case "github-release": {
1415
1713
  if (dryRun) {
1416
- (0, import_core8.info)("[DRY RUN] Would create GitHub release");
1714
+ (0, import_core9.info)("[DRY RUN] Would create GitHub release");
1417
1715
  break;
1418
1716
  }
1419
1717
  const firstContext = contexts[0];
1420
1718
  if (!firstContext) {
1421
- (0, import_core8.warn)("No context available for GitHub release");
1719
+ (0, import_core9.warn)("No context available for GitHub release");
1422
1720
  break;
1423
1721
  }
1424
1722
  const repoUrl = firstContext.repoUrl;
1425
1723
  if (!repoUrl) {
1426
- (0, import_core8.warn)("No repo URL available, cannot create GitHub release");
1724
+ (0, import_core9.warn)("No repo URL available, cannot create GitHub release");
1427
1725
  break;
1428
1726
  }
1429
1727
  const parsed = parseRepoUrl(repoUrl);
1430
1728
  if (!parsed) {
1431
- (0, import_core8.warn)(`Could not parse repo URL: ${repoUrl}`);
1729
+ (0, import_core9.warn)(`Could not parse repo URL: ${repoUrl}`);
1432
1730
  break;
1433
1731
  }
1434
1732
  await createGitHubRelease(firstContext, {
@@ -1441,135 +1739,29 @@ async function runPipeline(input, config, dryRun) {
1441
1739
  }
1442
1740
  }
1443
1741
  }
1444
- }
1445
-
1446
- // src/monorepo/aggregator.ts
1447
- var fs9 = __toESM(require("fs"), 1);
1448
- var path7 = __toESM(require("path"), 1);
1449
- var import_core9 = require("@releasekit/core");
1450
-
1451
- // src/monorepo/splitter.ts
1452
- function splitByPackage(contexts) {
1453
- const byPackage = /* @__PURE__ */ new Map();
1454
- for (const ctx of contexts) {
1455
- byPackage.set(ctx.packageName, ctx);
1456
- }
1457
- return byPackage;
1458
- }
1459
-
1460
- // src/monorepo/aggregator.ts
1461
- function writeFile(outputPath, content, dryRun) {
1462
- if (dryRun) {
1463
- (0, import_core9.info)(`[DRY RUN] Would write to ${outputPath}`);
1464
- console.log(content);
1465
- return;
1466
- }
1467
- const dir = path7.dirname(outputPath);
1468
- if (!fs9.existsSync(dir)) {
1469
- fs9.mkdirSync(dir, { recursive: true });
1470
- }
1471
- fs9.writeFileSync(outputPath, content, "utf-8");
1472
- (0, import_core9.success)(`Changelog written to ${outputPath}`);
1473
- }
1474
- function aggregateToRoot(contexts) {
1475
- const aggregated = {
1476
- packageName: "monorepo",
1477
- version: contexts[0]?.version ?? "0.0.0",
1478
- previousVersion: contexts[0]?.previousVersion ?? null,
1479
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
1480
- repoUrl: contexts[0]?.repoUrl ?? null,
1481
- entries: []
1482
- };
1483
- for (const ctx of contexts) {
1484
- for (const entry of ctx.entries) {
1485
- aggregated.entries.push({
1486
- ...entry,
1487
- scope: entry.scope ? `${ctx.packageName}/${entry.scope}` : ctx.packageName
1488
- });
1742
+ if (config.monorepo?.mode) {
1743
+ const { detectMonorepo: detectMonorepo2, writeMonorepoChangelogs: writeMonorepoChangelogs2 } = await Promise.resolve().then(() => (init_aggregator(), aggregator_exports));
1744
+ const cwd = process.cwd();
1745
+ const detected = detectMonorepo2(cwd);
1746
+ if (detected.isMonorepo) {
1747
+ const monoFiles = writeMonorepoChangelogs2(
1748
+ contexts,
1749
+ {
1750
+ rootPath: config.monorepo.rootPath ?? cwd,
1751
+ packagesPath: config.monorepo.packagesPath ?? detected.packagesPath,
1752
+ mode: config.monorepo.mode
1753
+ },
1754
+ config,
1755
+ dryRun
1756
+ );
1757
+ files.push(...monoFiles);
1489
1758
  }
1490
1759
  }
1491
- return aggregated;
1492
- }
1493
- function writeMonorepoChangelogs(contexts, options, config, dryRun) {
1494
- if (options.mode === "root" || options.mode === "both") {
1495
- const aggregated = aggregateToRoot(contexts);
1496
- const rootPath = path7.join(options.rootPath, "CHANGELOG.md");
1497
- (0, import_core9.info)(`Writing root changelog to ${rootPath}`);
1498
- const rootContent = config.updateStrategy === "prepend" && fs9.existsSync(rootPath) ? prependVersion(rootPath, aggregated) : renderMarkdown([aggregated]);
1499
- writeFile(rootPath, rootContent, dryRun);
1500
- }
1501
- if (options.mode === "packages" || options.mode === "both") {
1502
- const byPackage = splitByPackage(contexts);
1503
- const packageDirMap = buildPackageDirMap(options.rootPath, options.packagesPath);
1504
- for (const [packageName, ctx] of byPackage) {
1505
- const simpleName = packageName.split("/").pop();
1506
- const packageDir = packageDirMap.get(packageName) ?? (simpleName ? packageDirMap.get(simpleName) : void 0) ?? null;
1507
- if (packageDir) {
1508
- const changelogPath = path7.join(packageDir, "CHANGELOG.md");
1509
- (0, import_core9.info)(`Writing changelog for ${packageName} to ${changelogPath}`);
1510
- const pkgContent = config.updateStrategy === "prepend" && fs9.existsSync(changelogPath) ? prependVersion(changelogPath, ctx) : renderMarkdown([ctx]);
1511
- writeFile(changelogPath, pkgContent, dryRun);
1512
- } else {
1513
- (0, import_core9.info)(`Could not find directory for package ${packageName}, skipping`);
1514
- }
1515
- }
1516
- }
1517
- }
1518
- function buildPackageDirMap(rootPath, packagesPath) {
1519
- const map = /* @__PURE__ */ new Map();
1520
- const packagesDir = path7.join(rootPath, packagesPath);
1521
- if (!fs9.existsSync(packagesDir)) {
1522
- return map;
1523
- }
1524
- for (const entry of fs9.readdirSync(packagesDir, { withFileTypes: true })) {
1525
- if (!entry.isDirectory()) continue;
1526
- const dirPath = path7.join(packagesDir, entry.name);
1527
- map.set(entry.name, dirPath);
1528
- const packageJsonPath = path7.join(dirPath, "package.json");
1529
- if (fs9.existsSync(packageJsonPath)) {
1530
- try {
1531
- const pkg = JSON.parse(fs9.readFileSync(packageJsonPath, "utf-8"));
1532
- if (pkg.name) {
1533
- map.set(pkg.name, dirPath);
1534
- }
1535
- } catch {
1536
- }
1537
- }
1538
- }
1539
- return map;
1540
- }
1541
- function detectMonorepo(cwd) {
1542
- const pnpmWorkspacesPath = path7.join(cwd, "pnpm-workspace.yaml");
1543
- const packageJsonPath = path7.join(cwd, "package.json");
1544
- if (fs9.existsSync(pnpmWorkspacesPath)) {
1545
- const content = fs9.readFileSync(pnpmWorkspacesPath, "utf-8");
1546
- const packagesMatch = content.match(/packages:\s*\n\s*-\s*['"]([^'"]+)['"]/);
1547
- if (packagesMatch?.[1]) {
1548
- const packagesGlob = packagesMatch[1];
1549
- const packagesPath = packagesGlob.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1550
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1551
- }
1552
- return { isMonorepo: true, packagesPath: "packages" };
1553
- }
1554
- if (fs9.existsSync(packageJsonPath)) {
1555
- try {
1556
- const content = fs9.readFileSync(packageJsonPath, "utf-8");
1557
- const pkg = JSON.parse(content);
1558
- if (pkg.workspaces) {
1559
- const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
1560
- if (workspaces?.length) {
1561
- const firstWorkspace = workspaces[0];
1562
- if (firstWorkspace) {
1563
- const packagesPath = firstWorkspace.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1564
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1565
- }
1566
- }
1567
- }
1568
- } catch {
1569
- return { isMonorepo: false, packagesPath: "" };
1570
- }
1760
+ const packageNotes = {};
1761
+ for (const ctx of contexts) {
1762
+ packageNotes[ctx.packageName] = formatVersion(ctx);
1571
1763
  }
1572
- return { isMonorepo: false, packagesPath: "" };
1764
+ return { packageNotes, files };
1573
1765
  }
1574
1766
 
1575
1767
  // src/cli.ts
@@ -1637,29 +1829,10 @@ program.command("generate", { isDefault: true }).description("Generate changelog
1637
1829
  }
1638
1830
  (0, import_core10.info)(`Filtered to package: ${options.target}`);
1639
1831
  }
1640
- if (options.monorepo || config.monorepo) {
1641
- const monorepoMode = options.monorepo ?? config.monorepo?.mode ?? "both";
1642
- const detected = detectMonorepo(process.cwd());
1643
- if (!detected.isMonorepo) {
1644
- (0, import_core10.info)("No monorepo detected, using single package mode");
1645
- await runPipeline(input, config, options.dryRun ?? false);
1646
- } else {
1647
- (0, import_core10.info)(`Monorepo detected with packages at ${detected.packagesPath}`);
1648
- const contexts = input.packages.map(createTemplateContext);
1649
- writeMonorepoChangelogs(
1650
- contexts,
1651
- {
1652
- rootPath: config.monorepo?.rootPath ?? process.cwd(),
1653
- packagesPath: config.monorepo?.packagesPath ?? detected.packagesPath,
1654
- mode: monorepoMode
1655
- },
1656
- config,
1657
- options.dryRun ?? false
1658
- );
1659
- }
1660
- } else {
1661
- await runPipeline(input, config, options.dryRun ?? false);
1832
+ if (options.monorepo) {
1833
+ config.monorepo = { ...config.monorepo, mode: options.monorepo };
1662
1834
  }
1835
+ await runPipeline(input, config, options.dryRun ?? false);
1663
1836
  if (options.dryRun) {
1664
1837
  (0, import_core10.info)("Dry run complete - no files were written");
1665
1838
  } else {