@releasekit/notes 0.2.0-next.1 → 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.
- package/dist/{chunk-W7DVGQ7D.js → chunk-ACXCEHQT.js} +173 -53
- package/dist/cli.cjs +235 -106
- package/dist/cli.js +11 -2
- package/dist/index.cjs +208 -88
- 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,40 +1213,23 @@ function renderTemplate(templatePath, context, engine) {
|
|
|
1091
1213
|
return renderComposable(templatePath, context, engine);
|
|
1092
1214
|
}
|
|
1093
1215
|
|
|
1094
|
-
// src/
|
|
1095
|
-
function
|
|
1096
|
-
|
|
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
|
-
}
|
|
1216
|
+
// src/core/pipeline.ts
|
|
1217
|
+
function extractVersionFromTag(tag) {
|
|
1218
|
+
if (tag.includes("@") && !tag.startsWith("@")) {
|
|
1219
|
+
return tag;
|
|
1115
1220
|
}
|
|
1116
|
-
|
|
1221
|
+
return tag.replace(/^v/, "");
|
|
1117
1222
|
}
|
|
1118
|
-
|
|
1119
|
-
// src/core/pipeline.ts
|
|
1120
1223
|
function generateCompareUrl(repoUrl, from, to) {
|
|
1224
|
+
const fromVersion = extractVersionFromTag(from);
|
|
1225
|
+
const toVersion = extractVersionFromTag(to);
|
|
1121
1226
|
if (/gitlab\.com/i.test(repoUrl)) {
|
|
1122
|
-
return `${repoUrl}/-/compare/${
|
|
1227
|
+
return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
|
|
1123
1228
|
}
|
|
1124
1229
|
if (/bitbucket\.org/i.test(repoUrl)) {
|
|
1125
|
-
return `${repoUrl}/branches/compare/${
|
|
1230
|
+
return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
|
|
1126
1231
|
}
|
|
1127
|
-
return `${repoUrl}/compare/${
|
|
1232
|
+
return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
|
|
1128
1233
|
}
|
|
1129
1234
|
function createTemplateContext(pkg) {
|
|
1130
1235
|
const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
|
|
@@ -1183,10 +1288,30 @@ async function processWithLLM(context, config) {
|
|
|
1183
1288
|
};
|
|
1184
1289
|
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1185
1290
|
info4(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1186
|
-
if (tasks.enhance) {
|
|
1187
|
-
info4("Enhancing entries with LLM...");
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
+
}
|
|
1190
1315
|
}
|
|
1191
1316
|
if (tasks.summarize) {
|
|
1192
1317
|
info4("Summarizing entries with LLM...");
|
|
@@ -1195,25 +1320,16 @@ async function processWithLLM(context, config) {
|
|
|
1195
1320
|
info4("Summary generated successfully");
|
|
1196
1321
|
debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1197
1322
|
} else {
|
|
1198
|
-
|
|
1323
|
+
warn3("Summary generation returned empty result");
|
|
1199
1324
|
}
|
|
1200
1325
|
}
|
|
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
1326
|
if (tasks.releaseNotes) {
|
|
1211
1327
|
info4("Generating release notes with LLM...");
|
|
1212
1328
|
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1213
1329
|
if (enhanced.releaseNotes) {
|
|
1214
1330
|
info4("Release notes generated successfully");
|
|
1215
1331
|
} else {
|
|
1216
|
-
|
|
1332
|
+
warn3("Release notes generation returned empty result");
|
|
1217
1333
|
}
|
|
1218
1334
|
}
|
|
1219
1335
|
return {
|
|
@@ -1222,8 +1338,8 @@ async function processWithLLM(context, config) {
|
|
|
1222
1338
|
enhanced
|
|
1223
1339
|
};
|
|
1224
1340
|
} catch (error) {
|
|
1225
|
-
|
|
1226
|
-
|
|
1341
|
+
warn3(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1342
|
+
warn3("Falling back to raw entries");
|
|
1227
1343
|
return context;
|
|
1228
1344
|
}
|
|
1229
1345
|
}
|
|
@@ -1256,6 +1372,10 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
|
1256
1372
|
info4("--- End Preview ---");
|
|
1257
1373
|
return;
|
|
1258
1374
|
}
|
|
1375
|
+
if (outputPath === "-") {
|
|
1376
|
+
process.stdout.write(result.content);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1259
1379
|
const dir = path6.dirname(outputPath);
|
|
1260
1380
|
if (!fs8.existsSync(dir)) {
|
|
1261
1381
|
fs8.mkdirSync(dir, { recursive: true });
|
|
@@ -1294,17 +1414,17 @@ async function runPipeline(input, config, dryRun) {
|
|
|
1294
1414
|
}
|
|
1295
1415
|
const firstContext = contexts[0];
|
|
1296
1416
|
if (!firstContext) {
|
|
1297
|
-
|
|
1417
|
+
warn3("No context available for GitHub release");
|
|
1298
1418
|
break;
|
|
1299
1419
|
}
|
|
1300
1420
|
const repoUrl = firstContext.repoUrl;
|
|
1301
1421
|
if (!repoUrl) {
|
|
1302
|
-
|
|
1422
|
+
warn3("No repo URL available, cannot create GitHub release");
|
|
1303
1423
|
break;
|
|
1304
1424
|
}
|
|
1305
1425
|
const parsed = parseRepoUrl(repoUrl);
|
|
1306
1426
|
if (!parsed) {
|
|
1307
|
-
|
|
1427
|
+
warn3(`Could not parse repo URL: ${repoUrl}`);
|
|
1308
1428
|
break;
|
|
1309
1429
|
}
|
|
1310
1430
|
await createGitHubRelease(firstContext, {
|