@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/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,295 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
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
+
30
322
  // src/index.ts
31
323
  var index_exports = {};
32
324
  __export(index_exports, {
@@ -41,6 +333,7 @@ __export(index_exports, {
41
333
  aggregateToRoot: () => aggregateToRoot,
42
334
  createTemplateContext: () => createTemplateContext,
43
335
  detectMonorepo: () => detectMonorepo,
336
+ formatVersion: () => formatVersion,
44
337
  getDefaultConfig: () => getDefaultConfig,
45
338
  getExitCode: () => getExitCode,
46
339
  loadAuth: () => import_config.loadAuth,
@@ -73,9 +366,9 @@ function getDefaultConfig() {
73
366
  }
74
367
 
75
368
  // src/core/pipeline.ts
76
- var fs8 = __toESM(require("fs"), 1);
77
- var path6 = __toESM(require("path"), 1);
78
- var import_core8 = require("@releasekit/core");
369
+ var fs9 = __toESM(require("fs"), 1);
370
+ var path7 = __toESM(require("path"), 1);
371
+ var import_core9 = require("@releasekit/core");
79
372
 
80
373
  // src/input/package-versioner.ts
81
374
  var fs = __toESM(require("fs"), 1);
@@ -404,6 +697,83 @@ var OpenAICompatibleProvider = class extends BaseLLMProvider {
404
697
 
405
698
  // src/llm/tasks/categorize.ts
406
699
  var import_core3 = require("@releasekit/core");
700
+
701
+ // src/llm/prompts.ts
702
+ function resolvePrompt(taskName, defaultPrompt, promptsConfig) {
703
+ if (!promptsConfig) return defaultPrompt;
704
+ const fullTemplate = promptsConfig.templates?.[taskName];
705
+ if (fullTemplate) return fullTemplate;
706
+ const additionalInstructions = promptsConfig.instructions?.[taskName];
707
+ if (additionalInstructions) {
708
+ const insertionPoint = defaultPrompt.lastIndexOf("Output only valid JSON");
709
+ if (insertionPoint !== -1) {
710
+ return `${defaultPrompt.slice(0, insertionPoint)}Additional instructions:
711
+ ${additionalInstructions}
712
+
713
+ ${defaultPrompt.slice(insertionPoint)}`;
714
+ }
715
+ return `${defaultPrompt}
716
+
717
+ Additional instructions:
718
+ ${additionalInstructions}`;
719
+ }
720
+ return defaultPrompt;
721
+ }
722
+
723
+ // src/llm/scopes.ts
724
+ function getAllowedScopesFromCategories(categories) {
725
+ const scopeMap = /* @__PURE__ */ new Map();
726
+ for (const cat of categories) {
727
+ if (cat.scopes && cat.scopes.length > 0) {
728
+ scopeMap.set(cat.name, cat.scopes);
729
+ }
730
+ }
731
+ return scopeMap;
732
+ }
733
+ function resolveAllowedScopes(scopeConfig, categories, packageNames) {
734
+ if (!scopeConfig || scopeConfig.mode === "unrestricted") return null;
735
+ if (scopeConfig.mode === "none") return [];
736
+ if (scopeConfig.mode === "packages") return packageNames ?? [];
737
+ if (scopeConfig.mode === "restricted") {
738
+ const explicit = scopeConfig.rules?.allowed ?? [];
739
+ const all = new Set(explicit);
740
+ if (categories) {
741
+ const fromCategories = getAllowedScopesFromCategories(categories);
742
+ for (const scopes of fromCategories.values()) {
743
+ for (const s of scopes) all.add(s);
744
+ }
745
+ }
746
+ return [...all];
747
+ }
748
+ return null;
749
+ }
750
+ function validateScope(scope, allowedScopes, rules) {
751
+ if (!scope || allowedScopes === null) return scope;
752
+ if (allowedScopes.length === 0) return void 0;
753
+ const caseSensitive = rules?.caseSensitive ?? false;
754
+ const normalise = (s) => caseSensitive ? s : s.toLowerCase();
755
+ const isAllowed = allowedScopes.some((a) => normalise(a) === normalise(scope));
756
+ if (isAllowed) return scope;
757
+ switch (rules?.invalidScopeAction ?? "remove") {
758
+ case "keep":
759
+ return scope;
760
+ case "fallback":
761
+ return rules?.fallbackScope;
762
+ case "remove":
763
+ default:
764
+ return void 0;
765
+ }
766
+ }
767
+ function validateEntryScopes(entries, scopeConfig, categories) {
768
+ const allowedScopes = resolveAllowedScopes(scopeConfig, categories);
769
+ if (allowedScopes === null) return entries;
770
+ return entries.map((entry) => ({
771
+ ...entry,
772
+ scope: validateScope(entry.scope, allowedScopes, scopeConfig?.rules)
773
+ }));
774
+ }
775
+
776
+ // src/llm/tasks/categorize.ts
407
777
  var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
408
778
 
409
779
  Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
@@ -415,20 +785,21 @@ Entries:
415
785
 
416
786
  Output only valid JSON, nothing else:`;
417
787
  function buildCustomCategorizePrompt(categories) {
418
- const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
419
- const developerCategory = categories.find((c) => c.name === "Developer");
788
+ const categoryList = categories.map((c) => {
789
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
790
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
791
+ }).join("\n");
792
+ const scopeMap = getAllowedScopesFromCategories(categories);
420
793
  let scopeInstructions = "";
421
- if (developerCategory) {
422
- const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
423
- if (scopeMatch?.[1]) {
424
- const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
425
- if (scopes.length > 0) {
426
- scopeInstructions = `
427
-
428
- For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
429
- `;
430
- }
794
+ if (scopeMap.size > 0) {
795
+ const entries = [];
796
+ for (const [catName, scopes] of scopeMap) {
797
+ entries.push(`For "${catName}", assign a scope from: ${scopes.join(", ")}.`);
431
798
  }
799
+ scopeInstructions = `
800
+
801
+ ${entries.join("\n")}
802
+ Only use scopes from these predefined lists. If an entry does not fit any scope, set scope to null.`;
432
803
  }
433
804
  return `You are categorizing changelog entries for a software release.
434
805
 
@@ -436,9 +807,10 @@ Given the following entries, group them into the specified categories. Only use
436
807
 
437
808
  Categories:
438
809
  ${categoryList}${scopeInstructions}
810
+
439
811
  Output a JSON object with two fields:
440
812
  - "categories": an object where keys are category names and values are arrays of entry indices (0-based)
441
- - "scopes": an object where keys are entry indices (as strings) and values are scope labels
813
+ - "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.
442
814
 
443
815
  Entries:
444
816
  {{entries}}
@@ -449,9 +821,11 @@ async function categorizeEntries(provider, entries, context) {
449
821
  if (entries.length === 0) {
450
822
  return [];
451
823
  }
452
- const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
824
+ const entriesCopy = entries.map((e) => ({ ...e, scope: void 0 }));
825
+ const entriesText = entriesCopy.map((e, i) => `${i}. [${e.type}]: ${e.description}`).join("\n");
453
826
  const hasCustomCategories = context.categories && context.categories.length > 0;
454
- const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
827
+ const defaultPrompt = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
828
+ const promptTemplate = resolvePrompt("categorize", defaultPrompt, context.prompts);
455
829
  const prompt = promptTemplate.replace("{{entries}}", entriesText);
456
830
  try {
457
831
  const response = await provider.complete(prompt);
@@ -463,13 +837,14 @@ async function categorizeEntries(provider, entries, context) {
463
837
  const scopeMap = parsed.scopes || {};
464
838
  for (const [indexStr, scope] of Object.entries(scopeMap)) {
465
839
  const idx = Number.parseInt(indexStr, 10);
466
- if (entries[idx] && scope) {
467
- entries[idx] = { ...entries[idx], scope };
840
+ if (entriesCopy[idx] && scope && scope.trim()) {
841
+ entriesCopy[idx] = { ...entriesCopy[idx], scope: scope.trim() };
468
842
  }
469
843
  }
844
+ const validatedEntries = validateEntryScopes(entriesCopy, context.scopes, context.categories);
470
845
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
471
846
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
472
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
847
+ const categoryEntries = indices.map((i) => validatedEntries[i]).filter((e) => e !== void 0);
473
848
  if (categoryEntries.length > 0) {
474
849
  result.push({ category, entries: categoryEntries });
475
850
  }
@@ -478,7 +853,7 @@ async function categorizeEntries(provider, entries, context) {
478
853
  const categoryMap = parsed;
479
854
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
480
855
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
481
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
856
+ const categoryEntries = indices.map((i) => entriesCopy[i]).filter((e) => e !== void 0);
482
857
  if (categoryEntries.length > 0) {
483
858
  result.push({ category, entries: categoryEntries });
484
859
  }
@@ -489,12 +864,12 @@ async function categorizeEntries(provider, entries, context) {
489
864
  (0, import_core3.warn)(
490
865
  `LLM categorization failed, falling back to General: ${error instanceof Error ? error.message : String(error)}`
491
866
  );
492
- return [{ category: "General", entries }];
867
+ return [{ category: "General", entries: entriesCopy }];
493
868
  }
494
869
  }
495
870
 
496
871
  // src/llm/tasks/enhance.ts
497
- var ENHANCE_PROMPT = `You are improving changelog entries for a software project.
872
+ var DEFAULT_ENHANCE_PROMPT = `You are improving changelog entries for a software project.
498
873
  Given a technical commit message, rewrite it as a clear, user-friendly changelog entry.
499
874
 
500
875
  Rules:
@@ -510,9 +885,10 @@ Type: {{type}}
510
885
  Description: {{description}}
511
886
 
512
887
  Rewritten description (only output the new description, nothing else):`;
513
- async function enhanceEntry(provider, entry, _context) {
514
- const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
515
- 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);
888
+ async function enhanceEntry(provider, entry, context) {
889
+ const styleText = context.style ? `- ${context.style}` : '- Use present tense ("Add feature" not "Added feature")';
890
+ 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);
891
+ const prompt = resolvePrompt("enhance", defaultPrompt, context.prompts);
516
892
  const response = await provider.complete(prompt);
517
893
  return response.trim();
518
894
  }
@@ -568,7 +944,23 @@ function buildPrompt(entries, categories, style) {
568
944
  const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
569
945
  const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
570
946
  const categorySection = categories ? `Categories (use ONLY these):
571
- ${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
947
+ ${categories.map((c) => {
948
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
949
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
950
+ }).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
951
+ let scopeInstruction = "";
952
+ if (categories) {
953
+ const scopeMap = getAllowedScopesFromCategories(categories);
954
+ if (scopeMap.size > 0) {
955
+ const parts = [];
956
+ for (const [catName, scopes] of scopeMap) {
957
+ parts.push(`For "${catName}" entries, assign a scope from: ${scopes.join(", ")}.`);
958
+ }
959
+ scopeInstruction = `
960
+ ${parts.join("\n")}
961
+ Only use scopes from these predefined lists. Set scope to null if no scope applies.`;
962
+ }
963
+ }
572
964
  return `You are generating release notes for a software project. Given the following changelog entries, do two things:
573
965
 
574
966
  1. **Rewrite** each entry as a clear, user-friendly description
@@ -579,9 +971,7 @@ Style guidelines:
579
971
  - Be concise (1 short sentence per entry)
580
972
  - Focus on what changed, not implementation details
581
973
 
582
- ${categorySection}
583
-
584
- ${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
974
+ ${categorySection}${scopeInstruction}
585
975
 
586
976
  Entries:
587
977
  ${entriesText}
@@ -598,7 +988,8 @@ async function enhanceAndCategorize(provider, entries, context) {
598
988
  const retryOpts = LLM_DEFAULTS.retry;
599
989
  try {
600
990
  return await withRetry(async () => {
601
- const prompt = buildPrompt(entries, context.categories, context.style);
991
+ const defaultPrompt = buildPrompt(entries, context.categories, context.style);
992
+ const prompt = resolvePrompt("enhanceAndCategorize", defaultPrompt, context.prompts);
602
993
  const response = await provider.complete(prompt);
603
994
  const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
604
995
  const parsed = JSON.parse(cleaned);
@@ -614,22 +1005,23 @@ async function enhanceAndCategorize(provider, entries, context) {
614
1005
  scope: result.scope || original.scope
615
1006
  };
616
1007
  });
1008
+ const validatedEntries = validateEntryScopes(enhancedEntries, context.scopes, context.categories);
617
1009
  const categoryMap = /* @__PURE__ */ new Map();
618
1010
  for (let i = 0; i < parsed.entries.length; i++) {
619
1011
  const result = parsed.entries[i];
620
1012
  const category = result?.category || "General";
621
- const entry = enhancedEntries[i];
1013
+ const entry = validatedEntries[i];
622
1014
  if (!entry) continue;
623
1015
  if (!categoryMap.has(category)) {
624
1016
  categoryMap.set(category, []);
625
1017
  }
626
- categoryMap.get(category).push(entry);
1018
+ categoryMap.get(category)?.push(entry);
627
1019
  }
628
1020
  const categories = [];
629
1021
  for (const [category, catEntries] of categoryMap) {
630
1022
  categories.push({ category, entries: catEntries });
631
1023
  }
632
- return { enhancedEntries, categories };
1024
+ return { enhancedEntries: validatedEntries, categories };
633
1025
  }, retryOpts);
634
1026
  } catch (error) {
635
1027
  (0, import_core4.warn)(
@@ -643,7 +1035,7 @@ async function enhanceAndCategorize(provider, entries, context) {
643
1035
  }
644
1036
 
645
1037
  // src/llm/tasks/release-notes.ts
646
- var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
1038
+ var DEFAULT_RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
647
1039
 
648
1040
  Create engaging, user-friendly release notes for the following changes.
649
1041
 
@@ -676,16 +1068,17 @@ No notable changes in this release.`;
676
1068
  if (e.breaking) line += " **BREAKING**";
677
1069
  return line;
678
1070
  }).join("\n");
679
- const prompt = RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
1071
+ const defaultPrompt = DEFAULT_RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
680
1072
  "{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}",
681
1073
  context.previousVersion ? `Previous version: ${context.previousVersion}` : ""
682
1074
  ).replace("{{date}}", context.date ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "").replace("{{entries}}", entriesText);
1075
+ const prompt = resolvePrompt("releaseNotes", defaultPrompt, context.prompts);
683
1076
  const response = await provider.complete(prompt);
684
1077
  return response.trim();
685
1078
  }
686
1079
 
687
1080
  // src/llm/tasks/summarize.ts
688
- var SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
1081
+ var DEFAULT_SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
689
1082
 
690
1083
  Given the following changelog entries, create a brief summary (2-3 sentences) that captures the main themes of this release.
691
1084
 
@@ -693,12 +1086,13 @@ Entries:
693
1086
  {{entries}}
694
1087
 
695
1088
  Summary (only output the summary, nothing else):`;
696
- async function summarizeEntries(provider, entries, _context) {
1089
+ async function summarizeEntries(provider, entries, context) {
697
1090
  if (entries.length === 0) {
698
1091
  return "";
699
1092
  }
700
1093
  const entriesText = entries.map((e) => `- [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
701
- const prompt = SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
1094
+ const defaultPrompt = DEFAULT_SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
1095
+ const prompt = resolvePrompt("summarize", defaultPrompt, context.prompts);
702
1096
  const response = await provider.complete(prompt);
703
1097
  return response.trim();
704
1098
  }
@@ -743,135 +1137,7 @@ function createProvider(config) {
743
1137
  // src/output/github-release.ts
744
1138
  var import_rest = require("@octokit/rest");
745
1139
  var import_core6 = require("@releasekit/core");
746
-
747
- // src/output/markdown.ts
748
- var fs2 = __toESM(require("fs"), 1);
749
- var path = __toESM(require("path"), 1);
750
- var import_core5 = require("@releasekit/core");
751
- var TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
752
- var TYPE_LABELS = {
753
- added: "Added",
754
- changed: "Changed",
755
- deprecated: "Deprecated",
756
- removed: "Removed",
757
- fixed: "Fixed",
758
- security: "Security"
759
- };
760
- function groupEntriesByType(entries) {
761
- const grouped = /* @__PURE__ */ new Map();
762
- for (const type of TYPE_ORDER) {
763
- grouped.set(type, []);
764
- }
765
- for (const entry of entries) {
766
- const existing = grouped.get(entry.type) ?? [];
767
- existing.push(entry);
768
- grouped.set(entry.type, existing);
769
- }
770
- return grouped;
771
- }
772
- function formatEntry(entry) {
773
- let line;
774
- if (entry.breaking && entry.scope) {
775
- line = `- **BREAKING** **${entry.scope}**: ${entry.description}`;
776
- } else if (entry.breaking) {
777
- line = `- **BREAKING** ${entry.description}`;
778
- } else if (entry.scope) {
779
- line = `- **${entry.scope}**: ${entry.description}`;
780
- } else {
781
- line = `- ${entry.description}`;
782
- }
783
- if (entry.issueIds && entry.issueIds.length > 0) {
784
- line += ` (${entry.issueIds.join(", ")})`;
785
- }
786
- return line;
787
- }
788
- function formatVersion(context) {
789
- const lines = [];
790
- const versionHeader = context.previousVersion ? `## [${context.version}]` : `## ${context.version}`;
791
- lines.push(`${versionHeader} - ${context.date}`);
792
- lines.push("");
793
- if (context.compareUrl) {
794
- lines.push(`[Full Changelog](${context.compareUrl})`);
795
- lines.push("");
796
- }
797
- if (context.enhanced?.summary) {
798
- lines.push(context.enhanced.summary);
799
- lines.push("");
800
- }
801
- const grouped = groupEntriesByType(context.entries);
802
- for (const [type, entries] of grouped) {
803
- if (entries.length === 0) continue;
804
- lines.push(`### ${TYPE_LABELS[type]}`);
805
- for (const entry of entries) {
806
- lines.push(formatEntry(entry));
807
- }
808
- lines.push("");
809
- }
810
- return lines.join("\n");
811
- }
812
- function formatHeader() {
813
- return `# Changelog
814
-
815
- All notable changes to this project will be documented in this file.
816
-
817
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
818
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
819
-
820
- `;
821
- }
822
- function renderMarkdown(contexts) {
823
- const sections = [formatHeader()];
824
- for (const context of contexts) {
825
- sections.push(formatVersion(context));
826
- }
827
- return sections.join("\n");
828
- }
829
- function prependVersion(existingPath, context) {
830
- let existing = "";
831
- if (fs2.existsSync(existingPath)) {
832
- existing = fs2.readFileSync(existingPath, "utf-8");
833
- const headerEnd = existing.indexOf("\n## ");
834
- if (headerEnd >= 0) {
835
- const header = existing.slice(0, headerEnd);
836
- const body = existing.slice(headerEnd + 1);
837
- const newVersion = formatVersion(context);
838
- return `${header}
839
-
840
- ${newVersion}
841
- ${body}`;
842
- }
843
- }
844
- return renderMarkdown([context]);
845
- }
846
- function writeMarkdown(outputPath, contexts, config, dryRun) {
847
- const content = renderMarkdown(contexts);
848
- if (dryRun) {
849
- (0, import_core5.info)("--- Changelog Preview ---");
850
- console.log(content);
851
- (0, import_core5.info)("--- End Preview ---");
852
- return;
853
- }
854
- const dir = path.dirname(outputPath);
855
- if (!fs2.existsSync(dir)) {
856
- fs2.mkdirSync(dir, { recursive: true });
857
- }
858
- if (outputPath === "-") {
859
- process.stdout.write(content);
860
- return;
861
- }
862
- if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
863
- const firstContext = contexts[0];
864
- if (firstContext) {
865
- const updated = prependVersion(outputPath, firstContext);
866
- fs2.writeFileSync(outputPath, updated, "utf-8");
867
- }
868
- } else {
869
- fs2.writeFileSync(outputPath, content, "utf-8");
870
- }
871
- (0, import_core5.success)(`Changelog written to ${outputPath}`);
872
- }
873
-
874
- // src/output/github-release.ts
1140
+ init_markdown();
875
1141
  var GitHubClient = class {
876
1142
  octokit;
877
1143
  owner;
@@ -1007,9 +1273,10 @@ function renderJson(contexts) {
1007
1273
  function writeJson(outputPath, contexts, dryRun) {
1008
1274
  const content = renderJson(contexts);
1009
1275
  if (dryRun) {
1010
- (0, import_core7.info)("--- JSON Output Preview ---");
1011
- console.log(content);
1012
- (0, import_core7.info)("--- End Preview ---");
1276
+ (0, import_core7.info)(`Would write JSON output to ${outputPath}`);
1277
+ (0, import_core7.debug)("--- JSON Output Preview ---");
1278
+ (0, import_core7.debug)(content);
1279
+ (0, import_core7.debug)("--- End Preview ---");
1013
1280
  return;
1014
1281
  }
1015
1282
  const dir = path2.dirname(outputPath);
@@ -1020,6 +1287,9 @@ function writeJson(outputPath, contexts, dryRun) {
1020
1287
  (0, import_core7.success)(`JSON output written to ${outputPath}`);
1021
1288
  }
1022
1289
 
1290
+ // src/core/pipeline.ts
1291
+ init_markdown();
1292
+
1023
1293
  // src/templates/ejs.ts
1024
1294
  var fs4 = __toESM(require("fs"), 1);
1025
1295
  var import_ejs = __toESM(require("ejs"), 1);
@@ -1276,17 +1546,27 @@ function renderTemplate(templatePath, context, engine) {
1276
1546
 
1277
1547
  // src/core/pipeline.ts
1278
1548
  var import_meta = {};
1279
- function generateCompareUrl(repoUrl, from, to) {
1549
+ function generateCompareUrl(repoUrl, from, to, packageName) {
1550
+ const isPackageSpecific = from.includes("@") && packageName && from.includes(packageName);
1551
+ let fromVersion;
1552
+ let toVersion;
1553
+ if (isPackageSpecific) {
1554
+ fromVersion = from;
1555
+ toVersion = `${packageName}@${to.startsWith("v") ? "" : "v"}${to}`;
1556
+ } else {
1557
+ fromVersion = from.replace(/^v/, "");
1558
+ toVersion = to.replace(/^v/, "");
1559
+ }
1280
1560
  if (/gitlab\.com/i.test(repoUrl)) {
1281
- return `${repoUrl}/-/compare/${from}...${to}`;
1561
+ return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
1282
1562
  }
1283
1563
  if (/bitbucket\.org/i.test(repoUrl)) {
1284
- return `${repoUrl}/branches/compare/${from}..${to}`;
1564
+ return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
1285
1565
  }
1286
- return `${repoUrl}/compare/${from}...${to}`;
1566
+ return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
1287
1567
  }
1288
1568
  function createTemplateContext(pkg) {
1289
- const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
1569
+ const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version, pkg.packageName) : void 0;
1290
1570
  return {
1291
1571
  packageName: pkg.packageName,
1292
1572
  version: pkg.version,
@@ -1324,15 +1604,17 @@ async function processWithLLM(context, config) {
1324
1604
  previousVersion: context.previousVersion ?? void 0,
1325
1605
  date: context.date,
1326
1606
  categories: config.llm.categories,
1327
- style: config.llm.style
1607
+ style: config.llm.style,
1608
+ scopes: config.llm.scopes,
1609
+ prompts: config.llm.prompts
1328
1610
  };
1329
1611
  const enhanced = {
1330
1612
  entries: context.entries
1331
1613
  };
1332
1614
  try {
1333
- (0, import_core8.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1615
+ (0, import_core9.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1334
1616
  if (config.llm.baseURL) {
1335
- (0, import_core8.info)(`LLM base URL: ${config.llm.baseURL}`);
1617
+ (0, import_core9.info)(`LLM base URL: ${config.llm.baseURL}`);
1336
1618
  }
1337
1619
  const rawProvider = createProvider(config.llm);
1338
1620
  const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
@@ -1341,59 +1623,58 @@ async function processWithLLM(context, config) {
1341
1623
  complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
1342
1624
  };
1343
1625
  const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
1344
- (0, import_core8.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
1626
+ (0, import_core9.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
1345
1627
  if (tasks.enhance && tasks.categorize) {
1346
- (0, import_core8.info)("Enhancing and categorizing entries with LLM...");
1628
+ (0, import_core9.info)("Enhancing and categorizing entries with LLM...");
1347
1629
  const result = await enhanceAndCategorize(provider, context.entries, llmContext);
1348
1630
  enhanced.entries = result.enhancedEntries;
1349
1631
  enhanced.categories = {};
1350
1632
  for (const cat of result.categories) {
1351
1633
  enhanced.categories[cat.category] = cat.entries;
1352
1634
  }
1353
- (0, import_core8.info)(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1635
+ (0, import_core9.info)(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1354
1636
  } else {
1355
1637
  if (tasks.enhance) {
1356
- (0, import_core8.info)("Enhancing entries with LLM...");
1638
+ (0, import_core9.info)("Enhancing entries with LLM...");
1357
1639
  enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1358
- (0, import_core8.info)(`Enhanced ${enhanced.entries.length} entries`);
1640
+ (0, import_core9.info)(`Enhanced ${enhanced.entries.length} entries`);
1359
1641
  }
1360
1642
  if (tasks.categorize) {
1361
- (0, import_core8.info)("Categorizing entries with LLM...");
1643
+ (0, import_core9.info)("Categorizing entries with LLM...");
1362
1644
  const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1363
1645
  enhanced.categories = {};
1364
1646
  for (const cat of categorized) {
1365
1647
  enhanced.categories[cat.category] = cat.entries;
1366
1648
  }
1367
- (0, import_core8.info)(`Created ${categorized.length} categories`);
1649
+ (0, import_core9.info)(`Created ${categorized.length} categories`);
1368
1650
  }
1369
1651
  }
1370
1652
  if (tasks.summarize) {
1371
- (0, import_core8.info)("Summarizing entries with LLM...");
1653
+ (0, import_core9.info)("Summarizing entries with LLM...");
1372
1654
  enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
1373
1655
  if (enhanced.summary) {
1374
- (0, import_core8.info)("Summary generated successfully");
1375
- (0, import_core8.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1656
+ (0, import_core9.info)("Summary generated successfully");
1657
+ (0, import_core9.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1376
1658
  } else {
1377
- (0, import_core8.warn)("Summary generation returned empty result");
1659
+ (0, import_core9.warn)("Summary generation returned empty result");
1378
1660
  }
1379
1661
  }
1380
1662
  if (tasks.releaseNotes) {
1381
- (0, import_core8.info)("Generating release notes with LLM...");
1663
+ (0, import_core9.info)("Generating release notes with LLM...");
1382
1664
  enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
1383
1665
  if (enhanced.releaseNotes) {
1384
- (0, import_core8.info)("Release notes generated successfully");
1666
+ (0, import_core9.info)("Release notes generated successfully");
1385
1667
  } else {
1386
- (0, import_core8.warn)("Release notes generation returned empty result");
1668
+ (0, import_core9.warn)("Release notes generation returned empty result");
1387
1669
  }
1388
1670
  }
1389
1671
  return {
1390
1672
  ...context,
1391
- entries: enhanced.entries,
1392
1673
  enhanced
1393
1674
  };
1394
1675
  } catch (error) {
1395
- (0, import_core8.warn)(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
1396
- (0, import_core8.warn)("Falling back to raw entries");
1676
+ (0, import_core9.warn)(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
1677
+ (0, import_core9.warn)("Falling back to raw entries");
1397
1678
  return context;
1398
1679
  }
1399
1680
  }
@@ -1401,17 +1682,17 @@ function getBuiltinTemplatePath(style) {
1401
1682
  let packageRoot;
1402
1683
  try {
1403
1684
  const currentUrl = import_meta.url;
1404
- packageRoot = path6.dirname(new URL(currentUrl).pathname);
1405
- packageRoot = path6.join(packageRoot, "..", "..");
1685
+ packageRoot = path7.dirname(new URL(currentUrl).pathname);
1686
+ packageRoot = path7.join(packageRoot, "..", "..");
1406
1687
  } catch {
1407
1688
  packageRoot = __dirname;
1408
1689
  }
1409
- return path6.join(packageRoot, "templates", style);
1690
+ return path7.join(packageRoot, "templates", style);
1410
1691
  }
1411
1692
  async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1412
1693
  let templatePath;
1413
1694
  if (config.templates?.path) {
1414
- templatePath = path6.resolve(config.templates.path);
1695
+ templatePath = path7.resolve(config.templates.path);
1415
1696
  } else {
1416
1697
  templatePath = getBuiltinTemplatePath("keep-a-changelog");
1417
1698
  }
@@ -1421,64 +1702,78 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1421
1702
  );
1422
1703
  const result = renderTemplate(templatePath, documentContext, config.templates?.engine);
1423
1704
  if (dryRun) {
1424
- (0, import_core8.info)("--- Changelog Preview ---");
1425
- console.log(result.content);
1426
- (0, import_core8.info)("--- End Preview ---");
1705
+ (0, import_core9.info)(`Would write templated output to ${outputPath}`);
1706
+ (0, import_core9.debug)("--- Changelog Preview ---");
1707
+ (0, import_core9.debug)(result.content);
1708
+ (0, import_core9.debug)("--- End Preview ---");
1427
1709
  return;
1428
1710
  }
1429
1711
  if (outputPath === "-") {
1430
1712
  process.stdout.write(result.content);
1431
1713
  return;
1432
1714
  }
1433
- const dir = path6.dirname(outputPath);
1434
- if (!fs8.existsSync(dir)) {
1435
- fs8.mkdirSync(dir, { recursive: true });
1715
+ const dir = path7.dirname(outputPath);
1716
+ if (!fs9.existsSync(dir)) {
1717
+ fs9.mkdirSync(dir, { recursive: true });
1436
1718
  }
1437
- fs8.writeFileSync(outputPath, result.content, "utf-8");
1438
- (0, import_core8.success)(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1719
+ fs9.writeFileSync(outputPath, result.content, "utf-8");
1720
+ (0, import_core9.success)(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1439
1721
  }
1440
1722
  async function runPipeline(input, config, dryRun) {
1441
- (0, import_core8.debug)(`Processing ${input.packages.length} package(s)`);
1723
+ (0, import_core9.debug)(`Processing ${input.packages.length} package(s)`);
1442
1724
  let contexts = input.packages.map(createTemplateContext);
1443
1725
  if (config.llm && !process.env.CHANGELOG_NO_LLM) {
1444
- (0, import_core8.info)("Processing with LLM enhancement");
1726
+ (0, import_core9.info)("Processing with LLM enhancement");
1445
1727
  contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
1446
1728
  }
1729
+ const files = [];
1447
1730
  for (const output of config.output) {
1448
- (0, import_core8.info)(`Generating ${output.format} output`);
1731
+ (0, import_core9.info)(`Generating ${output.format} output`);
1449
1732
  switch (output.format) {
1450
1733
  case "markdown": {
1451
1734
  const file = output.file ?? "CHANGELOG.md";
1452
- if (config.templates?.path || output.options?.template) {
1453
- await generateWithTemplate(contexts, config, file, dryRun);
1454
- } else {
1455
- writeMarkdown(file, contexts, config, dryRun);
1735
+ try {
1736
+ const effectiveTemplateConfig = output.templates ?? config.templates;
1737
+ if (effectiveTemplateConfig?.path || output.options?.template) {
1738
+ const configWithTemplate = { ...config, templates: effectiveTemplateConfig };
1739
+ await generateWithTemplate(contexts, configWithTemplate, file, dryRun);
1740
+ } else {
1741
+ writeMarkdown(file, contexts, config, dryRun);
1742
+ }
1743
+ if (!dryRun) files.push(file);
1744
+ } catch (error) {
1745
+ (0, import_core9.warn)(`Failed to write ${file}: ${error instanceof Error ? error.message : String(error)}`);
1456
1746
  }
1457
1747
  break;
1458
1748
  }
1459
1749
  case "json": {
1460
1750
  const file = output.file ?? "changelog.json";
1461
- writeJson(file, contexts, dryRun);
1751
+ try {
1752
+ writeJson(file, contexts, dryRun);
1753
+ if (!dryRun) files.push(file);
1754
+ } catch (error) {
1755
+ (0, import_core9.warn)(`Failed to write ${file}: ${error instanceof Error ? error.message : String(error)}`);
1756
+ }
1462
1757
  break;
1463
1758
  }
1464
1759
  case "github-release": {
1465
1760
  if (dryRun) {
1466
- (0, import_core8.info)("[DRY RUN] Would create GitHub release");
1761
+ (0, import_core9.info)("[DRY RUN] Would create GitHub release");
1467
1762
  break;
1468
1763
  }
1469
1764
  const firstContext = contexts[0];
1470
1765
  if (!firstContext) {
1471
- (0, import_core8.warn)("No context available for GitHub release");
1766
+ (0, import_core9.warn)("No context available for GitHub release");
1472
1767
  break;
1473
1768
  }
1474
1769
  const repoUrl = firstContext.repoUrl;
1475
1770
  if (!repoUrl) {
1476
- (0, import_core8.warn)("No repo URL available, cannot create GitHub release");
1771
+ (0, import_core9.warn)("No repo URL available, cannot create GitHub release");
1477
1772
  break;
1478
1773
  }
1479
1774
  const parsed = parseRepoUrl(repoUrl);
1480
1775
  if (!parsed) {
1481
- (0, import_core8.warn)(`Could not parse repo URL: ${repoUrl}`);
1776
+ (0, import_core9.warn)(`Could not parse repo URL: ${repoUrl}`);
1482
1777
  break;
1483
1778
  }
1484
1779
  await createGitHubRelease(firstContext, {
@@ -1491,140 +1786,38 @@ async function runPipeline(input, config, dryRun) {
1491
1786
  }
1492
1787
  }
1493
1788
  }
1494
- }
1495
- async function processInput(inputJson, config, dryRun) {
1496
- const input = parsePackageVersioner(inputJson);
1497
- await runPipeline(input, config, dryRun);
1498
- }
1499
-
1500
- // src/monorepo/aggregator.ts
1501
- var fs9 = __toESM(require("fs"), 1);
1502
- var path7 = __toESM(require("path"), 1);
1503
- var import_core9 = require("@releasekit/core");
1504
-
1505
- // src/monorepo/splitter.ts
1506
- function splitByPackage(contexts) {
1507
- const byPackage = /* @__PURE__ */ new Map();
1508
- for (const ctx of contexts) {
1509
- byPackage.set(ctx.packageName, ctx);
1510
- }
1511
- return byPackage;
1512
- }
1513
-
1514
- // src/monorepo/aggregator.ts
1515
- function writeFile(outputPath, content, dryRun) {
1516
- if (dryRun) {
1517
- (0, import_core9.info)(`[DRY RUN] Would write to ${outputPath}`);
1518
- console.log(content);
1519
- return;
1520
- }
1521
- const dir = path7.dirname(outputPath);
1522
- if (!fs9.existsSync(dir)) {
1523
- fs9.mkdirSync(dir, { recursive: true });
1524
- }
1525
- fs9.writeFileSync(outputPath, content, "utf-8");
1526
- (0, import_core9.success)(`Changelog written to ${outputPath}`);
1527
- }
1528
- function aggregateToRoot(contexts) {
1529
- const aggregated = {
1530
- packageName: "monorepo",
1531
- version: contexts[0]?.version ?? "0.0.0",
1532
- previousVersion: contexts[0]?.previousVersion ?? null,
1533
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
1534
- repoUrl: contexts[0]?.repoUrl ?? null,
1535
- entries: []
1536
- };
1537
- for (const ctx of contexts) {
1538
- for (const entry of ctx.entries) {
1539
- aggregated.entries.push({
1540
- ...entry,
1541
- scope: entry.scope ? `${ctx.packageName}/${entry.scope}` : ctx.packageName
1542
- });
1789
+ if (config.monorepo?.mode) {
1790
+ const { detectMonorepo: detectMonorepo2, writeMonorepoChangelogs: writeMonorepoChangelogs2 } = await Promise.resolve().then(() => (init_aggregator(), aggregator_exports));
1791
+ const cwd = process.cwd();
1792
+ const detected = detectMonorepo2(cwd);
1793
+ if (detected.isMonorepo) {
1794
+ const monoFiles = writeMonorepoChangelogs2(
1795
+ contexts,
1796
+ {
1797
+ rootPath: config.monorepo.rootPath ?? cwd,
1798
+ packagesPath: config.monorepo.packagesPath ?? detected.packagesPath,
1799
+ mode: config.monorepo.mode
1800
+ },
1801
+ config,
1802
+ dryRun
1803
+ );
1804
+ files.push(...monoFiles);
1543
1805
  }
1544
1806
  }
1545
- return aggregated;
1546
- }
1547
- function writeMonorepoChangelogs(contexts, options, config, dryRun) {
1548
- if (options.mode === "root" || options.mode === "both") {
1549
- const aggregated = aggregateToRoot(contexts);
1550
- const rootPath = path7.join(options.rootPath, "CHANGELOG.md");
1551
- (0, import_core9.info)(`Writing root changelog to ${rootPath}`);
1552
- const rootContent = config.updateStrategy === "prepend" && fs9.existsSync(rootPath) ? prependVersion(rootPath, aggregated) : renderMarkdown([aggregated]);
1553
- writeFile(rootPath, rootContent, dryRun);
1554
- }
1555
- if (options.mode === "packages" || options.mode === "both") {
1556
- const byPackage = splitByPackage(contexts);
1557
- const packageDirMap = buildPackageDirMap(options.rootPath, options.packagesPath);
1558
- for (const [packageName, ctx] of byPackage) {
1559
- const simpleName = packageName.split("/").pop();
1560
- const packageDir = packageDirMap.get(packageName) ?? (simpleName ? packageDirMap.get(simpleName) : void 0) ?? null;
1561
- if (packageDir) {
1562
- const changelogPath = path7.join(packageDir, "CHANGELOG.md");
1563
- (0, import_core9.info)(`Writing changelog for ${packageName} to ${changelogPath}`);
1564
- const pkgContent = config.updateStrategy === "prepend" && fs9.existsSync(changelogPath) ? prependVersion(changelogPath, ctx) : renderMarkdown([ctx]);
1565
- writeFile(changelogPath, pkgContent, dryRun);
1566
- } else {
1567
- (0, import_core9.info)(`Could not find directory for package ${packageName}, skipping`);
1568
- }
1569
- }
1570
- }
1571
- }
1572
- function buildPackageDirMap(rootPath, packagesPath) {
1573
- const map = /* @__PURE__ */ new Map();
1574
- const packagesDir = path7.join(rootPath, packagesPath);
1575
- if (!fs9.existsSync(packagesDir)) {
1576
- return map;
1577
- }
1578
- for (const entry of fs9.readdirSync(packagesDir, { withFileTypes: true })) {
1579
- if (!entry.isDirectory()) continue;
1580
- const dirPath = path7.join(packagesDir, entry.name);
1581
- map.set(entry.name, dirPath);
1582
- const packageJsonPath = path7.join(dirPath, "package.json");
1583
- if (fs9.existsSync(packageJsonPath)) {
1584
- try {
1585
- const pkg = JSON.parse(fs9.readFileSync(packageJsonPath, "utf-8"));
1586
- if (pkg.name) {
1587
- map.set(pkg.name, dirPath);
1588
- }
1589
- } catch {
1590
- }
1591
- }
1807
+ const packageNotes = {};
1808
+ for (const ctx of contexts) {
1809
+ packageNotes[ctx.packageName] = formatVersion(ctx);
1592
1810
  }
1593
- return map;
1811
+ return { packageNotes, files };
1594
1812
  }
1595
- function detectMonorepo(cwd) {
1596
- const pnpmWorkspacesPath = path7.join(cwd, "pnpm-workspace.yaml");
1597
- const packageJsonPath = path7.join(cwd, "package.json");
1598
- if (fs9.existsSync(pnpmWorkspacesPath)) {
1599
- const content = fs9.readFileSync(pnpmWorkspacesPath, "utf-8");
1600
- const packagesMatch = content.match(/packages:\s*\n\s*-\s*['"]([^'"]+)['"]/);
1601
- if (packagesMatch?.[1]) {
1602
- const packagesGlob = packagesMatch[1];
1603
- const packagesPath = packagesGlob.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1604
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1605
- }
1606
- return { isMonorepo: true, packagesPath: "packages" };
1607
- }
1608
- if (fs9.existsSync(packageJsonPath)) {
1609
- try {
1610
- const content = fs9.readFileSync(packageJsonPath, "utf-8");
1611
- const pkg = JSON.parse(content);
1612
- if (pkg.workspaces) {
1613
- const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
1614
- if (workspaces?.length) {
1615
- const firstWorkspace = workspaces[0];
1616
- if (firstWorkspace) {
1617
- const packagesPath = firstWorkspace.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1618
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1619
- }
1620
- }
1621
- }
1622
- } catch {
1623
- return { isMonorepo: false, packagesPath: "" };
1624
- }
1625
- }
1626
- return { isMonorepo: false, packagesPath: "" };
1813
+ async function processInput(inputJson, config, dryRun) {
1814
+ const input = parsePackageVersioner(inputJson);
1815
+ return runPipeline(input, config, dryRun);
1627
1816
  }
1817
+
1818
+ // src/index.ts
1819
+ init_aggregator();
1820
+ init_markdown();
1628
1821
  // Annotate the CommonJS export names for ESM import in node:
1629
1822
  0 && (module.exports = {
1630
1823
  ChangelogCreatorError,
@@ -1638,6 +1831,7 @@ function detectMonorepo(cwd) {
1638
1831
  aggregateToRoot,
1639
1832
  createTemplateContext,
1640
1833
  detectMonorepo,
1834
+ formatVersion,
1641
1835
  getDefaultConfig,
1642
1836
  getExitCode,
1643
1837
  loadAuth,