@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.
- package/dist/{chunk-W7DVGQ7D.js → chunk-3VS3PBTN.js} +178 -56
- package/dist/cli.cjs +239 -108
- package/dist/cli.js +11 -2
- package/dist/index.cjs +212 -90
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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/${
|
|
1229
|
+
return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
|
|
1123
1230
|
}
|
|
1124
1231
|
if (/bitbucket\.org/i.test(repoUrl)) {
|
|
1125
|
-
return `${repoUrl}/branches/compare/${
|
|
1232
|
+
return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
|
|
1126
1233
|
}
|
|
1127
|
-
return `${repoUrl}/compare/${
|
|
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
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1419
|
+
warn3("No context available for GitHub release");
|
|
1298
1420
|
break;
|
|
1299
1421
|
}
|
|
1300
1422
|
const repoUrl = firstContext.repoUrl;
|
|
1301
1423
|
if (!repoUrl) {
|
|
1302
|
-
|
|
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
|
-
|
|
1429
|
+
warn3(`Could not parse repo URL: ${repoUrl}`);
|
|
1308
1430
|
break;
|
|
1309
1431
|
}
|
|
1310
1432
|
await createGitHubRelease(firstContext, {
|