@releasekit/notes 0.2.0-next.1 → 0.2.0-next.11

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
@@ -624,6 +639,113 @@ async function enhanceEntries(provider, entries, context, concurrency = LLM_DEFA
624
639
  return results;
625
640
  }
626
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
+
627
749
  // src/llm/tasks/release-notes.ts
628
750
  var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
629
751
 
@@ -1091,43 +1213,28 @@ function renderTemplate(templatePath, context, engine) {
1091
1213
  return renderComposable(templatePath, context, engine);
1092
1214
  }
1093
1215
 
1094
- // src/utils/retry.ts
1095
- function sleep(ms) {
1096
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1097
- }
1098
- async function withRetry(fn, options = {}) {
1099
- const maxAttempts = options.maxAttempts ?? 3;
1100
- const initialDelay = options.initialDelay ?? 1e3;
1101
- const maxDelay = options.maxDelay ?? 3e4;
1102
- const backoffFactor = options.backoffFactor ?? 2;
1103
- let lastError;
1104
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1105
- try {
1106
- return await fn();
1107
- } catch (error) {
1108
- lastError = error;
1109
- if (attempt < maxAttempts - 1) {
1110
- const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
1111
- const jitter = base * 0.2 * (Math.random() * 2 - 1);
1112
- await sleep(Math.max(0, base + jitter));
1113
- }
1114
- }
1115
- }
1116
- throw lastError;
1117
- }
1118
-
1119
1216
  // src/core/pipeline.ts
1120
- function generateCompareUrl(repoUrl, from, to) {
1217
+ function generateCompareUrl(repoUrl, from, to, packageName) {
1218
+ const isPackageSpecific = from.includes("@") && packageName && from.includes(packageName);
1219
+ let fromVersion;
1220
+ let toVersion;
1221
+ if (isPackageSpecific) {
1222
+ fromVersion = from;
1223
+ toVersion = `${packageName}@${to.startsWith("v") ? "" : "v"}${to}`;
1224
+ } else {
1225
+ fromVersion = from.replace(/^v/, "");
1226
+ toVersion = to.replace(/^v/, "");
1227
+ }
1121
1228
  if (/gitlab\.com/i.test(repoUrl)) {
1122
- return `${repoUrl}/-/compare/${from}...${to}`;
1229
+ return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
1123
1230
  }
1124
1231
  if (/bitbucket\.org/i.test(repoUrl)) {
1125
- return `${repoUrl}/branches/compare/${from}..${to}`;
1232
+ return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
1126
1233
  }
1127
- return `${repoUrl}/compare/${from}...${to}`;
1234
+ return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
1128
1235
  }
1129
1236
  function createTemplateContext(pkg) {
1130
- const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
1237
+ const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version, pkg.packageName) : void 0;
1131
1238
  return {
1132
1239
  packageName: pkg.packageName,
1133
1240
  version: pkg.version,
@@ -1183,10 +1290,30 @@ async function processWithLLM(context, config) {
1183
1290
  };
1184
1291
  const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
1185
1292
  info4(`Running LLM tasks: ${activeTasks.join(", ")}`);
1186
- if (tasks.enhance) {
1187
- info4("Enhancing entries with LLM...");
1188
- enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1189
- info4(`Enhanced ${enhanced.entries.length} entries`);
1293
+ if (tasks.enhance && tasks.categorize) {
1294
+ info4("Enhancing and categorizing entries with LLM...");
1295
+ const result = await enhanceAndCategorize(provider, context.entries, llmContext);
1296
+ enhanced.entries = result.enhancedEntries;
1297
+ enhanced.categories = {};
1298
+ for (const cat of result.categories) {
1299
+ enhanced.categories[cat.category] = cat.entries;
1300
+ }
1301
+ info4(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1302
+ } else {
1303
+ if (tasks.enhance) {
1304
+ info4("Enhancing entries with LLM...");
1305
+ enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1306
+ info4(`Enhanced ${enhanced.entries.length} entries`);
1307
+ }
1308
+ if (tasks.categorize) {
1309
+ info4("Categorizing entries with LLM...");
1310
+ const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1311
+ enhanced.categories = {};
1312
+ for (const cat of categorized) {
1313
+ enhanced.categories[cat.category] = cat.entries;
1314
+ }
1315
+ info4(`Created ${categorized.length} categories`);
1316
+ }
1190
1317
  }
1191
1318
  if (tasks.summarize) {
1192
1319
  info4("Summarizing entries with LLM...");
@@ -1195,25 +1322,16 @@ async function processWithLLM(context, config) {
1195
1322
  info4("Summary generated successfully");
1196
1323
  debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1197
1324
  } else {
1198
- warn2("Summary generation returned empty result");
1325
+ warn3("Summary generation returned empty result");
1199
1326
  }
1200
1327
  }
1201
- if (tasks.categorize) {
1202
- info4("Categorizing entries with LLM...");
1203
- const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1204
- enhanced.categories = {};
1205
- for (const cat of categorized) {
1206
- enhanced.categories[cat.category] = cat.entries;
1207
- }
1208
- info4(`Created ${categorized.length} categories`);
1209
- }
1210
1328
  if (tasks.releaseNotes) {
1211
1329
  info4("Generating release notes with LLM...");
1212
1330
  enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
1213
1331
  if (enhanced.releaseNotes) {
1214
1332
  info4("Release notes generated successfully");
1215
1333
  } else {
1216
- warn2("Release notes generation returned empty result");
1334
+ warn3("Release notes generation returned empty result");
1217
1335
  }
1218
1336
  }
1219
1337
  return {
@@ -1222,8 +1340,8 @@ async function processWithLLM(context, config) {
1222
1340
  enhanced
1223
1341
  };
1224
1342
  } catch (error) {
1225
- warn2(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
1226
- warn2("Falling back to raw entries");
1343
+ warn3(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
1344
+ warn3("Falling back to raw entries");
1227
1345
  return context;
1228
1346
  }
1229
1347
  }
@@ -1256,6 +1374,10 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1256
1374
  info4("--- End Preview ---");
1257
1375
  return;
1258
1376
  }
1377
+ if (outputPath === "-") {
1378
+ process.stdout.write(result.content);
1379
+ return;
1380
+ }
1259
1381
  const dir = path6.dirname(outputPath);
1260
1382
  if (!fs8.existsSync(dir)) {
1261
1383
  fs8.mkdirSync(dir, { recursive: true });
@@ -1294,17 +1416,17 @@ async function runPipeline(input, config, dryRun) {
1294
1416
  }
1295
1417
  const firstContext = contexts[0];
1296
1418
  if (!firstContext) {
1297
- warn2("No context available for GitHub release");
1419
+ warn3("No context available for GitHub release");
1298
1420
  break;
1299
1421
  }
1300
1422
  const repoUrl = firstContext.repoUrl;
1301
1423
  if (!repoUrl) {
1302
- warn2("No repo URL available, cannot create GitHub release");
1424
+ warn3("No repo URL available, cannot create GitHub release");
1303
1425
  break;
1304
1426
  }
1305
1427
  const parsed = parseRepoUrl(repoUrl);
1306
1428
  if (!parsed) {
1307
- warn2(`Could not parse repo URL: ${repoUrl}`);
1429
+ warn3(`Could not parse repo URL: ${repoUrl}`);
1308
1430
  break;
1309
1431
  }
1310
1432
  await createGitHubRelease(firstContext, {