@releasekit/notes 0.2.0-next.0 → 0.2.0-next.10

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.
@@ -256,6 +256,10 @@ function writeMarkdown(outputPath, contexts, config, dryRun) {
256
256
  if (!fs2.existsSync(dir)) {
257
257
  fs2.mkdirSync(dir, { recursive: true });
258
258
  }
259
+ if (outputPath === "-") {
260
+ process.stdout.write(content);
261
+ return;
262
+ }
259
263
  if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
260
264
  const firstContext = contexts[0];
261
265
  if (firstContext) {
@@ -307,7 +311,7 @@ function writeJson(outputPath, contexts, dryRun) {
307
311
  // src/core/pipeline.ts
308
312
  import * as fs8 from "fs";
309
313
  import * as path6 from "path";
310
- import { debug, info as info4, success as success4, warn as warn2 } from "@releasekit/core";
314
+ import { debug, info as info4, success as success4, warn as warn3 } from "@releasekit/core";
311
315
 
312
316
  // src/llm/defaults.ts
313
317
  var LLM_DEFAULTS = {
@@ -516,15 +520,26 @@ Entries:
516
520
  Output only valid JSON, nothing else:`;
517
521
  function buildCustomCategorizePrompt(categories) {
518
522
  const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
523
+ const developerCategory = categories.find((c) => c.name === "Developer");
524
+ let scopeInstructions = "";
525
+ if (developerCategory) {
526
+ const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
527
+ if (scopeMatch?.[1]) {
528
+ const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
529
+ if (scopes.length > 0) {
530
+ scopeInstructions = `
531
+
532
+ For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
533
+ `;
534
+ }
535
+ }
536
+ }
519
537
  return `You are categorizing changelog entries for a software release.
520
538
 
521
- Given the following entries, group them into the specified categories. Only use the categories listed below.
539
+ Given the following entries, group them into the specified categories. Only use the categories listed below in this exact order:
522
540
 
523
541
  Categories:
524
- ${categoryList}
525
-
526
- For entries in categories that involve internal/developer changes, set a "scope" field on those entries with a short subcategory label (e.g., "CI", "Dependencies", "Testing", "Code Quality", "Build System").
527
-
542
+ ${categoryList}${scopeInstructions}
528
543
  Output a JSON object with two fields:
529
544
  - "categories": an object where keys are category names and values are arrays of entry indices (0-based)
530
545
  - "scopes": an object where keys are entry indices (as strings) and values are scope labels
@@ -556,7 +571,8 @@ async function categorizeEntries(provider, entries, context) {
556
571
  entries[idx] = { ...entries[idx], scope };
557
572
  }
558
573
  }
559
- for (const [category, indices] of Object.entries(categoryMap)) {
574
+ for (const [category, rawIndices] of Object.entries(categoryMap)) {
575
+ const indices = Array.isArray(rawIndices) ? rawIndices : [];
560
576
  const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
561
577
  if (categoryEntries.length > 0) {
562
578
  result.push({ category, entries: categoryEntries });
@@ -564,7 +580,8 @@ async function categorizeEntries(provider, entries, context) {
564
580
  }
565
581
  } else {
566
582
  const categoryMap = parsed;
567
- for (const [category, indices] of Object.entries(categoryMap)) {
583
+ for (const [category, rawIndices] of Object.entries(categoryMap)) {
584
+ const indices = Array.isArray(rawIndices) ? rawIndices : [];
568
585
  const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
569
586
  if (categoryEntries.length > 0) {
570
587
  result.push({ category, entries: categoryEntries });
@@ -622,6 +639,113 @@ async function enhanceEntries(provider, entries, context, concurrency = LLM_DEFA
622
639
  return results;
623
640
  }
624
641
 
642
+ // src/llm/tasks/enhance-and-categorize.ts
643
+ import { warn as warn2 } from "@releasekit/core";
644
+
645
+ // src/utils/retry.ts
646
+ function sleep(ms) {
647
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
648
+ }
649
+ async function withRetry(fn, options = {}) {
650
+ const maxAttempts = options.maxAttempts ?? 3;
651
+ const initialDelay = options.initialDelay ?? 1e3;
652
+ const maxDelay = options.maxDelay ?? 3e4;
653
+ const backoffFactor = options.backoffFactor ?? 2;
654
+ let lastError;
655
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
656
+ try {
657
+ return await fn();
658
+ } catch (error) {
659
+ lastError = error;
660
+ if (attempt < maxAttempts - 1) {
661
+ const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
662
+ const jitter = base * 0.2 * (Math.random() * 2 - 1);
663
+ await sleep(Math.max(0, base + jitter));
664
+ }
665
+ }
666
+ }
667
+ throw lastError;
668
+ }
669
+
670
+ // src/llm/tasks/enhance-and-categorize.ts
671
+ function buildPrompt(entries, categories, style) {
672
+ const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
673
+ const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
674
+ const categorySection = categories ? `Categories (use ONLY these):
675
+ ${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
676
+ return `You are generating release notes for a software project. Given the following changelog entries, do two things:
677
+
678
+ 1. **Rewrite** each entry as a clear, user-friendly description
679
+ 2. **Categorize** each entry into the appropriate category
680
+
681
+ Style guidelines:
682
+ - ${styleText}
683
+ - Be concise (1 short sentence per entry)
684
+ - Focus on what changed, not implementation details
685
+
686
+ ${categorySection}
687
+
688
+ ${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
689
+
690
+ Entries:
691
+ ${entriesText}
692
+
693
+ Output a JSON object with:
694
+ - "entries": array of objects, one per input entry (same order), each with: { "description": "rewritten text", "category": "CategoryName", "scope": "optional subcategory label or null" }
695
+
696
+ Output only valid JSON, nothing else:`;
697
+ }
698
+ async function enhanceAndCategorize(provider, entries, context) {
699
+ if (entries.length === 0) {
700
+ return { enhancedEntries: [], categories: [] };
701
+ }
702
+ const retryOpts = LLM_DEFAULTS.retry;
703
+ try {
704
+ return await withRetry(async () => {
705
+ const prompt = buildPrompt(entries, context.categories, context.style);
706
+ const response = await provider.complete(prompt);
707
+ const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
708
+ const parsed = JSON.parse(cleaned);
709
+ if (!Array.isArray(parsed.entries)) {
710
+ throw new Error('Response missing "entries" array');
711
+ }
712
+ const enhancedEntries = entries.map((original, i) => {
713
+ const result = parsed.entries[i];
714
+ if (!result) return original;
715
+ return {
716
+ ...original,
717
+ description: result.description || original.description,
718
+ scope: result.scope || original.scope
719
+ };
720
+ });
721
+ const categoryMap = /* @__PURE__ */ new Map();
722
+ for (let i = 0; i < parsed.entries.length; i++) {
723
+ const result = parsed.entries[i];
724
+ const category = result?.category || "General";
725
+ const entry = enhancedEntries[i];
726
+ if (!entry) continue;
727
+ if (!categoryMap.has(category)) {
728
+ categoryMap.set(category, []);
729
+ }
730
+ categoryMap.get(category).push(entry);
731
+ }
732
+ const categories = [];
733
+ for (const [category, catEntries] of categoryMap) {
734
+ categories.push({ category, entries: catEntries });
735
+ }
736
+ return { enhancedEntries, categories };
737
+ }, retryOpts);
738
+ } catch (error) {
739
+ warn2(
740
+ `Combined enhance+categorize failed after ${retryOpts.maxAttempts} attempts: ${error instanceof Error ? error.message : String(error)}`
741
+ );
742
+ return {
743
+ enhancedEntries: entries,
744
+ categories: [{ category: "General", entries }]
745
+ };
746
+ }
747
+ }
748
+
625
749
  // src/llm/tasks/release-notes.ts
626
750
  var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
627
751
 
@@ -1089,40 +1213,23 @@ function renderTemplate(templatePath, context, engine) {
1089
1213
  return renderComposable(templatePath, context, engine);
1090
1214
  }
1091
1215
 
1092
- // src/utils/retry.ts
1093
- function sleep(ms) {
1094
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1095
- }
1096
- async function withRetry(fn, options = {}) {
1097
- const maxAttempts = options.maxAttempts ?? 3;
1098
- const initialDelay = options.initialDelay ?? 1e3;
1099
- const maxDelay = options.maxDelay ?? 3e4;
1100
- const backoffFactor = options.backoffFactor ?? 2;
1101
- let lastError;
1102
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1103
- try {
1104
- return await fn();
1105
- } catch (error) {
1106
- lastError = error;
1107
- if (attempt < maxAttempts - 1) {
1108
- const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
1109
- const jitter = base * 0.2 * (Math.random() * 2 - 1);
1110
- await sleep(Math.max(0, base + jitter));
1111
- }
1112
- }
1216
+ // src/core/pipeline.ts
1217
+ function extractVersionFromTag(tag) {
1218
+ if (tag.includes("@") && !tag.startsWith("@")) {
1219
+ return tag;
1113
1220
  }
1114
- throw lastError;
1221
+ return tag.replace(/^v/, "");
1115
1222
  }
1116
-
1117
- // src/core/pipeline.ts
1118
1223
  function generateCompareUrl(repoUrl, from, to) {
1224
+ const fromVersion = extractVersionFromTag(from);
1225
+ const toVersion = extractVersionFromTag(to);
1119
1226
  if (/gitlab\.com/i.test(repoUrl)) {
1120
- return `${repoUrl}/-/compare/${from}...${to}`;
1227
+ return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
1121
1228
  }
1122
1229
  if (/bitbucket\.org/i.test(repoUrl)) {
1123
- return `${repoUrl}/branches/compare/${from}..${to}`;
1230
+ return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
1124
1231
  }
1125
- return `${repoUrl}/compare/${from}...${to}`;
1232
+ return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
1126
1233
  }
1127
1234
  function createTemplateContext(pkg) {
1128
1235
  const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
@@ -1181,10 +1288,30 @@ async function processWithLLM(context, config) {
1181
1288
  };
1182
1289
  const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
1183
1290
  info4(`Running LLM tasks: ${activeTasks.join(", ")}`);
1184
- if (tasks.enhance) {
1185
- info4("Enhancing entries with LLM...");
1186
- enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1187
- info4(`Enhanced ${enhanced.entries.length} entries`);
1291
+ if (tasks.enhance && tasks.categorize) {
1292
+ info4("Enhancing and categorizing entries with LLM...");
1293
+ const result = await enhanceAndCategorize(provider, context.entries, llmContext);
1294
+ enhanced.entries = result.enhancedEntries;
1295
+ enhanced.categories = {};
1296
+ for (const cat of result.categories) {
1297
+ enhanced.categories[cat.category] = cat.entries;
1298
+ }
1299
+ info4(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1300
+ } else {
1301
+ if (tasks.enhance) {
1302
+ info4("Enhancing entries with LLM...");
1303
+ enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1304
+ info4(`Enhanced ${enhanced.entries.length} entries`);
1305
+ }
1306
+ if (tasks.categorize) {
1307
+ info4("Categorizing entries with LLM...");
1308
+ const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1309
+ enhanced.categories = {};
1310
+ for (const cat of categorized) {
1311
+ enhanced.categories[cat.category] = cat.entries;
1312
+ }
1313
+ info4(`Created ${categorized.length} categories`);
1314
+ }
1188
1315
  }
1189
1316
  if (tasks.summarize) {
1190
1317
  info4("Summarizing entries with LLM...");
@@ -1193,25 +1320,16 @@ async function processWithLLM(context, config) {
1193
1320
  info4("Summary generated successfully");
1194
1321
  debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1195
1322
  } else {
1196
- warn2("Summary generation returned empty result");
1323
+ warn3("Summary generation returned empty result");
1197
1324
  }
1198
1325
  }
1199
- if (tasks.categorize) {
1200
- info4("Categorizing entries with LLM...");
1201
- const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1202
- enhanced.categories = {};
1203
- for (const cat of categorized) {
1204
- enhanced.categories[cat.category] = cat.entries;
1205
- }
1206
- info4(`Created ${categorized.length} categories`);
1207
- }
1208
1326
  if (tasks.releaseNotes) {
1209
1327
  info4("Generating release notes with LLM...");
1210
1328
  enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
1211
1329
  if (enhanced.releaseNotes) {
1212
1330
  info4("Release notes generated successfully");
1213
1331
  } else {
1214
- warn2("Release notes generation returned empty result");
1332
+ warn3("Release notes generation returned empty result");
1215
1333
  }
1216
1334
  }
1217
1335
  return {
@@ -1220,8 +1338,8 @@ async function processWithLLM(context, config) {
1220
1338
  enhanced
1221
1339
  };
1222
1340
  } catch (error) {
1223
- warn2(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
1224
- warn2("Falling back to raw entries");
1341
+ warn3(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
1342
+ warn3("Falling back to raw entries");
1225
1343
  return context;
1226
1344
  }
1227
1345
  }
@@ -1254,6 +1372,10 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1254
1372
  info4("--- End Preview ---");
1255
1373
  return;
1256
1374
  }
1375
+ if (outputPath === "-") {
1376
+ process.stdout.write(result.content);
1377
+ return;
1378
+ }
1257
1379
  const dir = path6.dirname(outputPath);
1258
1380
  if (!fs8.existsSync(dir)) {
1259
1381
  fs8.mkdirSync(dir, { recursive: true });
@@ -1292,17 +1414,17 @@ async function runPipeline(input, config, dryRun) {
1292
1414
  }
1293
1415
  const firstContext = contexts[0];
1294
1416
  if (!firstContext) {
1295
- warn2("No context available for GitHub release");
1417
+ warn3("No context available for GitHub release");
1296
1418
  break;
1297
1419
  }
1298
1420
  const repoUrl = firstContext.repoUrl;
1299
1421
  if (!repoUrl) {
1300
- warn2("No repo URL available, cannot create GitHub release");
1422
+ warn3("No repo URL available, cannot create GitHub release");
1301
1423
  break;
1302
1424
  }
1303
1425
  const parsed = parseRepoUrl(repoUrl);
1304
1426
  if (!parsed) {
1305
- warn2(`Could not parse repo URL: ${repoUrl}`);
1427
+ warn3(`Could not parse repo URL: ${repoUrl}`);
1306
1428
  break;
1307
1429
  }
1308
1430
  await createGitHubRelease(firstContext, {