@releasekit/notes 0.3.0 → 0.3.1
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/aggregator-XJ2EILO3.js +13 -0
- package/dist/chunk-DCQ32FVH.js +165 -0
- package/dist/chunk-E4454SIS.js +243 -0
- package/dist/chunk-ENAWZXFG.js +1901 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +196 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +58 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXIT_CODES,
|
|
3
|
+
ReleaseKitError,
|
|
4
|
+
debug,
|
|
5
|
+
formatVersion,
|
|
6
|
+
info,
|
|
7
|
+
renderMarkdown,
|
|
8
|
+
success,
|
|
9
|
+
warn,
|
|
10
|
+
writeMarkdown
|
|
11
|
+
} from "./chunk-E4454SIS.js";
|
|
12
|
+
|
|
13
|
+
// ../config/dist/index.js
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import * as TOML from "smol-toml";
|
|
17
|
+
import * as fs3 from "fs";
|
|
18
|
+
import * as path3 from "path";
|
|
19
|
+
import { z as z2 } from "zod";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import * as fs2 from "fs";
|
|
22
|
+
import * as os from "os";
|
|
23
|
+
import * as path2 from "path";
|
|
24
|
+
var ConfigError = class extends ReleaseKitError {
|
|
25
|
+
code = "CONFIG_ERROR";
|
|
26
|
+
suggestions;
|
|
27
|
+
constructor(message, suggestions) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.suggestions = suggestions ?? [
|
|
30
|
+
"Check that releasekit.config.json exists and is valid JSON",
|
|
31
|
+
"Run with --verbose for more details"
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var MAX_JSONC_LENGTH = 1e5;
|
|
36
|
+
function parseJsonc(content) {
|
|
37
|
+
if (content.length > MAX_JSONC_LENGTH) {
|
|
38
|
+
throw new Error(`JSONC content too long: ${content.length} characters (max ${MAX_JSONC_LENGTH})`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
} catch {
|
|
43
|
+
const cleaned = content.replace(/\/\/[^\r\n]{0,10000}$/gm, "").replace(/\/\*[\s\S]{0,50000}?\*\//g, "").trim();
|
|
44
|
+
return JSON.parse(cleaned);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var GitConfigSchema = z.object({
|
|
48
|
+
remote: z.string().default("origin"),
|
|
49
|
+
branch: z.string().default("main"),
|
|
50
|
+
pushMethod: z.enum(["auto", "ssh", "https"]).default("auto"),
|
|
51
|
+
/**
|
|
52
|
+
* Optional env var name containing a GitHub token for HTTPS pushes.
|
|
53
|
+
* When set, publish steps can use this token without mutating git remotes.
|
|
54
|
+
*/
|
|
55
|
+
httpsTokenEnv: z.string().optional(),
|
|
56
|
+
push: z.boolean().optional(),
|
|
57
|
+
skipHooks: z.boolean().optional()
|
|
58
|
+
});
|
|
59
|
+
var MonorepoConfigSchema = z.object({
|
|
60
|
+
mode: z.enum(["root", "packages", "both"]).optional(),
|
|
61
|
+
rootPath: z.string().optional(),
|
|
62
|
+
packagesPath: z.string().optional(),
|
|
63
|
+
mainPackage: z.string().optional()
|
|
64
|
+
});
|
|
65
|
+
var BranchPatternSchema = z.object({
|
|
66
|
+
pattern: z.string(),
|
|
67
|
+
releaseType: z.enum(["major", "minor", "patch", "prerelease"])
|
|
68
|
+
});
|
|
69
|
+
var VersionCargoConfigSchema = z.object({
|
|
70
|
+
enabled: z.boolean().default(true),
|
|
71
|
+
paths: z.array(z.string()).optional()
|
|
72
|
+
});
|
|
73
|
+
var VersionConfigSchema = z.object({
|
|
74
|
+
tagTemplate: z.string().default("v{version}"),
|
|
75
|
+
packageSpecificTags: z.boolean().default(false),
|
|
76
|
+
preset: z.string().default("conventional"),
|
|
77
|
+
sync: z.boolean().default(true),
|
|
78
|
+
packages: z.array(z.string()).default([]),
|
|
79
|
+
mainPackage: z.string().optional(),
|
|
80
|
+
updateInternalDependencies: z.enum(["major", "minor", "patch", "no-internal-update"]).default("minor"),
|
|
81
|
+
skip: z.array(z.string()).optional(),
|
|
82
|
+
commitMessage: z.string().optional(),
|
|
83
|
+
versionStrategy: z.enum(["branchPattern", "commitMessage"]).default("commitMessage"),
|
|
84
|
+
branchPatterns: z.array(BranchPatternSchema).optional(),
|
|
85
|
+
defaultReleaseType: z.enum(["major", "minor", "patch", "prerelease"]).optional(),
|
|
86
|
+
mismatchStrategy: z.enum(["error", "warn", "ignore", "prefer-package", "prefer-git"]).default("warn"),
|
|
87
|
+
versionPrefix: z.string().default(""),
|
|
88
|
+
prereleaseIdentifier: z.string().optional(),
|
|
89
|
+
strictReachable: z.boolean().default(false),
|
|
90
|
+
cargo: VersionCargoConfigSchema.optional()
|
|
91
|
+
});
|
|
92
|
+
var NpmConfigSchema = z.object({
|
|
93
|
+
enabled: z.boolean().default(true),
|
|
94
|
+
auth: z.enum(["auto", "oidc", "token"]).default("auto"),
|
|
95
|
+
provenance: z.boolean().default(true),
|
|
96
|
+
access: z.enum(["public", "restricted"]).default("public"),
|
|
97
|
+
registry: z.string().default("https://registry.npmjs.org"),
|
|
98
|
+
copyFiles: z.array(z.string()).default(["LICENSE"]),
|
|
99
|
+
tag: z.string().default("latest")
|
|
100
|
+
});
|
|
101
|
+
var CargoPublishConfigSchema = z.object({
|
|
102
|
+
enabled: z.boolean().default(false),
|
|
103
|
+
noVerify: z.boolean().default(false),
|
|
104
|
+
publishOrder: z.array(z.string()).default([]),
|
|
105
|
+
clean: z.boolean().default(false)
|
|
106
|
+
});
|
|
107
|
+
var PublishGitConfigSchema = z.object({
|
|
108
|
+
push: z.boolean().default(true),
|
|
109
|
+
pushMethod: z.enum(["auto", "ssh", "https"]).optional(),
|
|
110
|
+
remote: z.string().optional(),
|
|
111
|
+
branch: z.string().optional(),
|
|
112
|
+
httpsTokenEnv: z.string().optional(),
|
|
113
|
+
skipHooks: z.boolean().optional()
|
|
114
|
+
});
|
|
115
|
+
var GitHubReleaseConfigSchema = z.object({
|
|
116
|
+
enabled: z.boolean().default(true),
|
|
117
|
+
draft: z.boolean().default(true),
|
|
118
|
+
perPackage: z.boolean().default(true),
|
|
119
|
+
prerelease: z.union([z.literal("auto"), z.boolean()]).default("auto"),
|
|
120
|
+
/**
|
|
121
|
+
* Controls how release notes are sourced for GitHub releases.
|
|
122
|
+
* - 'auto': Use RELEASE_NOTES.md if it exists, then per-package changelog
|
|
123
|
+
* data from the version output, then GitHub's auto-generated notes.
|
|
124
|
+
* - 'github': Always use GitHub's auto-generated notes.
|
|
125
|
+
* - 'none': No notes body.
|
|
126
|
+
* - Any other string: Treated as a file path to read notes from.
|
|
127
|
+
*/
|
|
128
|
+
releaseNotes: z.union([z.literal("auto"), z.literal("github"), z.literal("none"), z.string()]).default("auto")
|
|
129
|
+
});
|
|
130
|
+
var VerifyRegistryConfigSchema = z.object({
|
|
131
|
+
enabled: z.boolean().default(true),
|
|
132
|
+
maxAttempts: z.number().int().positive().default(5),
|
|
133
|
+
initialDelay: z.number().int().positive().default(15e3),
|
|
134
|
+
backoffMultiplier: z.number().positive().default(2)
|
|
135
|
+
});
|
|
136
|
+
var VerifyConfigSchema = z.object({
|
|
137
|
+
npm: VerifyRegistryConfigSchema.default({
|
|
138
|
+
enabled: true,
|
|
139
|
+
maxAttempts: 5,
|
|
140
|
+
initialDelay: 15e3,
|
|
141
|
+
backoffMultiplier: 2
|
|
142
|
+
}),
|
|
143
|
+
cargo: VerifyRegistryConfigSchema.default({
|
|
144
|
+
enabled: true,
|
|
145
|
+
maxAttempts: 10,
|
|
146
|
+
initialDelay: 3e4,
|
|
147
|
+
backoffMultiplier: 2
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
var PublishConfigSchema = z.object({
|
|
151
|
+
git: PublishGitConfigSchema.optional(),
|
|
152
|
+
npm: NpmConfigSchema.default({
|
|
153
|
+
enabled: true,
|
|
154
|
+
auth: "auto",
|
|
155
|
+
provenance: true,
|
|
156
|
+
access: "public",
|
|
157
|
+
registry: "https://registry.npmjs.org",
|
|
158
|
+
copyFiles: ["LICENSE"],
|
|
159
|
+
tag: "latest"
|
|
160
|
+
}),
|
|
161
|
+
cargo: CargoPublishConfigSchema.default({
|
|
162
|
+
enabled: false,
|
|
163
|
+
noVerify: false,
|
|
164
|
+
publishOrder: [],
|
|
165
|
+
clean: false
|
|
166
|
+
}),
|
|
167
|
+
githubRelease: GitHubReleaseConfigSchema.default({
|
|
168
|
+
enabled: true,
|
|
169
|
+
draft: true,
|
|
170
|
+
perPackage: true,
|
|
171
|
+
prerelease: "auto",
|
|
172
|
+
releaseNotes: "auto"
|
|
173
|
+
}),
|
|
174
|
+
verify: VerifyConfigSchema.default({
|
|
175
|
+
npm: {
|
|
176
|
+
enabled: true,
|
|
177
|
+
maxAttempts: 5,
|
|
178
|
+
initialDelay: 15e3,
|
|
179
|
+
backoffMultiplier: 2
|
|
180
|
+
},
|
|
181
|
+
cargo: {
|
|
182
|
+
enabled: true,
|
|
183
|
+
maxAttempts: 10,
|
|
184
|
+
initialDelay: 3e4,
|
|
185
|
+
backoffMultiplier: 2
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
var TemplateConfigSchema = z.object({
|
|
190
|
+
path: z.string().optional(),
|
|
191
|
+
engine: z.enum(["handlebars", "liquid", "ejs"]).optional()
|
|
192
|
+
});
|
|
193
|
+
var OutputConfigSchema = z.object({
|
|
194
|
+
format: z.enum(["markdown", "github-release", "json"]),
|
|
195
|
+
file: z.string().optional(),
|
|
196
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
197
|
+
templates: TemplateConfigSchema.optional()
|
|
198
|
+
});
|
|
199
|
+
var LLMOptionsSchema = z.object({
|
|
200
|
+
timeout: z.number().optional(),
|
|
201
|
+
maxTokens: z.number().optional(),
|
|
202
|
+
temperature: z.number().optional()
|
|
203
|
+
});
|
|
204
|
+
var LLMRetryConfigSchema = z.object({
|
|
205
|
+
maxAttempts: z.number().int().positive().optional(),
|
|
206
|
+
initialDelay: z.number().nonnegative().optional(),
|
|
207
|
+
maxDelay: z.number().positive().optional(),
|
|
208
|
+
backoffFactor: z.number().positive().optional()
|
|
209
|
+
});
|
|
210
|
+
var LLMTasksConfigSchema = z.object({
|
|
211
|
+
summarize: z.boolean().optional(),
|
|
212
|
+
enhance: z.boolean().optional(),
|
|
213
|
+
categorize: z.boolean().optional(),
|
|
214
|
+
releaseNotes: z.boolean().optional()
|
|
215
|
+
});
|
|
216
|
+
var LLMCategorySchema = z.object({
|
|
217
|
+
name: z.string(),
|
|
218
|
+
description: z.string(),
|
|
219
|
+
scopes: z.array(z.string()).optional()
|
|
220
|
+
});
|
|
221
|
+
var ScopeRulesSchema = z.object({
|
|
222
|
+
allowed: z.array(z.string()).optional(),
|
|
223
|
+
caseSensitive: z.boolean().default(false),
|
|
224
|
+
invalidScopeAction: z.enum(["remove", "keep", "fallback"]).default("remove"),
|
|
225
|
+
fallbackScope: z.string().optional()
|
|
226
|
+
});
|
|
227
|
+
var ScopeConfigSchema = z.object({
|
|
228
|
+
mode: z.enum(["restricted", "packages", "none", "unrestricted"]).default("unrestricted"),
|
|
229
|
+
rules: ScopeRulesSchema.optional()
|
|
230
|
+
});
|
|
231
|
+
var LLMPromptOverridesSchema = z.object({
|
|
232
|
+
enhance: z.string().optional(),
|
|
233
|
+
categorize: z.string().optional(),
|
|
234
|
+
enhanceAndCategorize: z.string().optional(),
|
|
235
|
+
summarize: z.string().optional(),
|
|
236
|
+
releaseNotes: z.string().optional()
|
|
237
|
+
});
|
|
238
|
+
var LLMPromptsConfigSchema = z.object({
|
|
239
|
+
instructions: LLMPromptOverridesSchema.optional(),
|
|
240
|
+
templates: LLMPromptOverridesSchema.optional()
|
|
241
|
+
});
|
|
242
|
+
var LLMConfigSchema = z.object({
|
|
243
|
+
provider: z.string(),
|
|
244
|
+
model: z.string(),
|
|
245
|
+
baseURL: z.string().optional(),
|
|
246
|
+
apiKey: z.string().optional(),
|
|
247
|
+
options: LLMOptionsSchema.optional(),
|
|
248
|
+
concurrency: z.number().int().positive().optional(),
|
|
249
|
+
retry: LLMRetryConfigSchema.optional(),
|
|
250
|
+
tasks: LLMTasksConfigSchema.optional(),
|
|
251
|
+
categories: z.array(LLMCategorySchema).optional(),
|
|
252
|
+
style: z.string().optional(),
|
|
253
|
+
scopes: ScopeConfigSchema.optional(),
|
|
254
|
+
prompts: LLMPromptsConfigSchema.optional()
|
|
255
|
+
});
|
|
256
|
+
var NotesInputConfigSchema = z.object({
|
|
257
|
+
source: z.string().optional(),
|
|
258
|
+
file: z.string().optional()
|
|
259
|
+
});
|
|
260
|
+
var NotesConfigSchema = z.object({
|
|
261
|
+
input: NotesInputConfigSchema.optional(),
|
|
262
|
+
output: z.array(OutputConfigSchema).default([{ format: "markdown", file: "CHANGELOG.md" }]),
|
|
263
|
+
monorepo: MonorepoConfigSchema.optional(),
|
|
264
|
+
templates: TemplateConfigSchema.optional(),
|
|
265
|
+
llm: LLMConfigSchema.optional(),
|
|
266
|
+
updateStrategy: z.enum(["prepend", "regenerate"]).default("prepend")
|
|
267
|
+
});
|
|
268
|
+
var CILabelsConfigSchema = z.object({
|
|
269
|
+
stable: z.string().default("release:stable"),
|
|
270
|
+
prerelease: z.string().default("release:prerelease"),
|
|
271
|
+
skip: z.string().default("release:skip"),
|
|
272
|
+
major: z.string().default("release:major"),
|
|
273
|
+
minor: z.string().default("release:minor"),
|
|
274
|
+
patch: z.string().default("release:patch")
|
|
275
|
+
});
|
|
276
|
+
var CIConfigSchema = z.object({
|
|
277
|
+
releaseStrategy: z.enum(["manual", "direct", "standing-pr", "scheduled"]).default("direct"),
|
|
278
|
+
releaseTrigger: z.enum(["commit", "label"]).default("label"),
|
|
279
|
+
prPreview: z.boolean().default(true),
|
|
280
|
+
autoRelease: z.boolean().default(false),
|
|
281
|
+
/**
|
|
282
|
+
* Commit message prefixes that should not trigger a release.
|
|
283
|
+
* Defaults to `['chore: release ']` to match the release commit template
|
|
284
|
+
* (`chore: release ${packageName} v${version}`) and provide a
|
|
285
|
+
* secondary loop-prevention guard alongside `[skip ci]`.
|
|
286
|
+
*/
|
|
287
|
+
skipPatterns: z.array(z.string()).default(["chore: release "]),
|
|
288
|
+
minChanges: z.number().int().positive().default(1),
|
|
289
|
+
labels: CILabelsConfigSchema.default({
|
|
290
|
+
stable: "release:stable",
|
|
291
|
+
prerelease: "release:prerelease",
|
|
292
|
+
skip: "release:skip",
|
|
293
|
+
major: "release:major",
|
|
294
|
+
minor: "release:minor",
|
|
295
|
+
patch: "release:patch"
|
|
296
|
+
})
|
|
297
|
+
});
|
|
298
|
+
var ReleaseCIConfigSchema = z.object({
|
|
299
|
+
skipPatterns: z.array(z.string().min(1)).optional(),
|
|
300
|
+
minChanges: z.number().int().positive().optional(),
|
|
301
|
+
/** Set to `false` to disable GitHub release creation in CI. */
|
|
302
|
+
githubRelease: z.literal(false).optional(),
|
|
303
|
+
/** Set to `false` to disable changelog generation in CI. */
|
|
304
|
+
notes: z.literal(false).optional()
|
|
305
|
+
});
|
|
306
|
+
var ReleaseConfigSchema = z.object({
|
|
307
|
+
/**
|
|
308
|
+
* Optional steps to enable. The version step always runs; only 'notes' and
|
|
309
|
+
* 'publish' can be opted out. Omitting a step is equivalent to --skip-<step>.
|
|
310
|
+
*/
|
|
311
|
+
steps: z.array(z.enum(["notes", "publish"])).min(1).optional(),
|
|
312
|
+
ci: ReleaseCIConfigSchema.optional()
|
|
313
|
+
});
|
|
314
|
+
var ReleaseKitConfigSchema = z.object({
|
|
315
|
+
git: GitConfigSchema.optional(),
|
|
316
|
+
monorepo: MonorepoConfigSchema.optional(),
|
|
317
|
+
version: VersionConfigSchema.optional(),
|
|
318
|
+
publish: PublishConfigSchema.optional(),
|
|
319
|
+
notes: NotesConfigSchema.optional(),
|
|
320
|
+
ci: CIConfigSchema.optional(),
|
|
321
|
+
release: ReleaseConfigSchema.optional()
|
|
322
|
+
});
|
|
323
|
+
var MAX_INPUT_LENGTH = 1e4;
|
|
324
|
+
function substituteVariables(value) {
|
|
325
|
+
if (value.length > MAX_INPUT_LENGTH) {
|
|
326
|
+
throw new Error(`Input too long: ${value.length} characters (max ${MAX_INPUT_LENGTH})`);
|
|
327
|
+
}
|
|
328
|
+
const envPattern = /\{env:([^}]{1,1000})\}/g;
|
|
329
|
+
const filePattern = /\{file:([^}]{1,1000})\}/g;
|
|
330
|
+
let result = value;
|
|
331
|
+
result = result.replace(envPattern, (_, varName) => {
|
|
332
|
+
return process.env[varName] ?? "";
|
|
333
|
+
});
|
|
334
|
+
result = result.replace(filePattern, (_, filePath) => {
|
|
335
|
+
const expandedPath = filePath.startsWith("~") ? path2.join(os.homedir(), filePath.slice(1)) : filePath;
|
|
336
|
+
try {
|
|
337
|
+
return fs2.readFileSync(expandedPath, "utf-8").trim();
|
|
338
|
+
} catch {
|
|
339
|
+
return "";
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
var SOLE_REFERENCE_PATTERN = /^\{(?:env|file):[^}]+\}$/;
|
|
345
|
+
function substituteInObject(obj) {
|
|
346
|
+
if (typeof obj === "string") {
|
|
347
|
+
const result = substituteVariables(obj);
|
|
348
|
+
if (result === "" && SOLE_REFERENCE_PATTERN.test(obj)) {
|
|
349
|
+
return void 0;
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
if (Array.isArray(obj)) {
|
|
354
|
+
return obj.map((item) => substituteInObject(item));
|
|
355
|
+
}
|
|
356
|
+
if (obj && typeof obj === "object") {
|
|
357
|
+
const result = {};
|
|
358
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
359
|
+
result[key] = substituteInObject(value);
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
return obj;
|
|
364
|
+
}
|
|
365
|
+
var AUTH_DIR = path2.join(os.homedir(), ".config", "releasekit");
|
|
366
|
+
var AUTH_FILE = path2.join(AUTH_DIR, "auth.json");
|
|
367
|
+
function loadAuth() {
|
|
368
|
+
if (fs2.existsSync(AUTH_FILE)) {
|
|
369
|
+
try {
|
|
370
|
+
const content = fs2.readFileSync(AUTH_FILE, "utf-8");
|
|
371
|
+
return JSON.parse(content);
|
|
372
|
+
} catch {
|
|
373
|
+
return {};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {};
|
|
377
|
+
}
|
|
378
|
+
function saveAuth(provider, apiKey) {
|
|
379
|
+
if (!fs2.existsSync(AUTH_DIR)) {
|
|
380
|
+
fs2.mkdirSync(AUTH_DIR, { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
const existing = loadAuth();
|
|
383
|
+
existing[provider] = apiKey;
|
|
384
|
+
fs2.writeFileSync(AUTH_FILE, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
|
|
385
|
+
}
|
|
386
|
+
var CONFIG_FILE = "releasekit.config.json";
|
|
387
|
+
function loadConfigFile(configPath) {
|
|
388
|
+
if (!fs3.existsSync(configPath)) {
|
|
389
|
+
return {};
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const content = fs3.readFileSync(configPath, "utf-8");
|
|
393
|
+
const parsed = parseJsonc(content);
|
|
394
|
+
const substituted = substituteInObject(parsed);
|
|
395
|
+
return ReleaseKitConfigSchema.parse(substituted);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof z2.ZodError) {
|
|
398
|
+
const issues = error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
399
|
+
throw new ConfigError(`Config validation errors:
|
|
400
|
+
${issues}`);
|
|
401
|
+
}
|
|
402
|
+
if (error instanceof SyntaxError) {
|
|
403
|
+
throw new ConfigError(`Invalid JSON in config file: ${error.message}`);
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function loadConfig(options) {
|
|
409
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
410
|
+
const configPath = options?.configPath ?? path3.join(cwd, CONFIG_FILE);
|
|
411
|
+
return loadConfigFile(configPath);
|
|
412
|
+
}
|
|
413
|
+
function loadNotesConfig(options) {
|
|
414
|
+
const config = loadConfig(options);
|
|
415
|
+
return config.notes;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/core/config.ts
|
|
419
|
+
function loadConfig2(projectDir = process.cwd(), configFile) {
|
|
420
|
+
const options = { cwd: projectDir, configPath: configFile };
|
|
421
|
+
return loadNotesConfig(options) ?? getDefaultConfig();
|
|
422
|
+
}
|
|
423
|
+
function getDefaultConfig() {
|
|
424
|
+
return {
|
|
425
|
+
output: [{ format: "markdown", file: "CHANGELOG.md" }],
|
|
426
|
+
updateStrategy: "prepend"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/errors/index.ts
|
|
431
|
+
var NotesError = class extends ReleaseKitError {
|
|
432
|
+
};
|
|
433
|
+
var InputParseError = class extends NotesError {
|
|
434
|
+
code = "INPUT_PARSE_ERROR";
|
|
435
|
+
suggestions = [
|
|
436
|
+
"Ensure input is valid JSON",
|
|
437
|
+
"Check that input matches expected schema",
|
|
438
|
+
"Use --input-source to specify format"
|
|
439
|
+
];
|
|
440
|
+
};
|
|
441
|
+
var TemplateError = class extends NotesError {
|
|
442
|
+
code = "TEMPLATE_ERROR";
|
|
443
|
+
suggestions = [
|
|
444
|
+
"Check template syntax",
|
|
445
|
+
"Ensure all required files exist (document, version, entry)",
|
|
446
|
+
"Verify template engine matches file extension"
|
|
447
|
+
];
|
|
448
|
+
};
|
|
449
|
+
var LLMError = class extends NotesError {
|
|
450
|
+
code = "LLM_ERROR";
|
|
451
|
+
suggestions = [
|
|
452
|
+
"Check API key is configured",
|
|
453
|
+
"Verify model name is correct",
|
|
454
|
+
"Check network connectivity",
|
|
455
|
+
"Try with --no-llm to skip LLM processing"
|
|
456
|
+
];
|
|
457
|
+
};
|
|
458
|
+
var GitHubError = class extends NotesError {
|
|
459
|
+
code = "GITHUB_ERROR";
|
|
460
|
+
suggestions = [
|
|
461
|
+
"Ensure GITHUB_TOKEN is set",
|
|
462
|
+
"Check token has repo scope",
|
|
463
|
+
"Verify repository exists and is accessible"
|
|
464
|
+
];
|
|
465
|
+
};
|
|
466
|
+
var ConfigError2 = class extends NotesError {
|
|
467
|
+
code = "CONFIG_ERROR";
|
|
468
|
+
suggestions = [
|
|
469
|
+
"Check config file syntax",
|
|
470
|
+
"Verify all required fields are present",
|
|
471
|
+
"Run releasekit-notes init to create default config"
|
|
472
|
+
];
|
|
473
|
+
};
|
|
474
|
+
function getExitCode(error) {
|
|
475
|
+
switch (error.code) {
|
|
476
|
+
case "CONFIG_ERROR":
|
|
477
|
+
return EXIT_CODES.CONFIG_ERROR;
|
|
478
|
+
case "INPUT_PARSE_ERROR":
|
|
479
|
+
return EXIT_CODES.INPUT_ERROR;
|
|
480
|
+
case "TEMPLATE_ERROR":
|
|
481
|
+
return EXIT_CODES.TEMPLATE_ERROR;
|
|
482
|
+
case "LLM_ERROR":
|
|
483
|
+
return EXIT_CODES.LLM_ERROR;
|
|
484
|
+
case "GITHUB_ERROR":
|
|
485
|
+
return EXIT_CODES.GITHUB_ERROR;
|
|
486
|
+
default:
|
|
487
|
+
return EXIT_CODES.GENERAL_ERROR;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/input/version-output.ts
|
|
492
|
+
import * as fs4 from "fs";
|
|
493
|
+
function normalizeEntryType(type) {
|
|
494
|
+
const typeMap = {
|
|
495
|
+
added: "added",
|
|
496
|
+
feat: "added",
|
|
497
|
+
feature: "added",
|
|
498
|
+
changed: "changed",
|
|
499
|
+
update: "changed",
|
|
500
|
+
refactor: "changed",
|
|
501
|
+
deprecated: "deprecated",
|
|
502
|
+
removed: "removed",
|
|
503
|
+
fixed: "fixed",
|
|
504
|
+
fix: "fixed",
|
|
505
|
+
security: "security",
|
|
506
|
+
sec: "security"
|
|
507
|
+
};
|
|
508
|
+
return typeMap[type.toLowerCase()] ?? "changed";
|
|
509
|
+
}
|
|
510
|
+
function parseVersionOutput(json) {
|
|
511
|
+
let data;
|
|
512
|
+
try {
|
|
513
|
+
data = JSON.parse(json);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
throw new InputParseError(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
|
|
516
|
+
}
|
|
517
|
+
if (!data.changelogs || !Array.isArray(data.changelogs)) {
|
|
518
|
+
throw new InputParseError('Input must contain a "changelogs" array');
|
|
519
|
+
}
|
|
520
|
+
const packages = data.changelogs.map((changelog) => ({
|
|
521
|
+
packageName: changelog.packageName,
|
|
522
|
+
version: changelog.version,
|
|
523
|
+
previousVersion: changelog.previousVersion,
|
|
524
|
+
revisionRange: changelog.revisionRange,
|
|
525
|
+
repoUrl: changelog.repoUrl,
|
|
526
|
+
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
|
|
527
|
+
entries: changelog.entries.map((entry) => ({
|
|
528
|
+
type: normalizeEntryType(entry.type),
|
|
529
|
+
description: entry.description,
|
|
530
|
+
issueIds: entry.issueIds,
|
|
531
|
+
scope: entry.scope,
|
|
532
|
+
originalType: entry.originalType,
|
|
533
|
+
breaking: entry.breaking ?? entry.originalType?.includes("!") ?? false
|
|
534
|
+
}))
|
|
535
|
+
}));
|
|
536
|
+
const repoUrl = packages[0]?.repoUrl ?? null;
|
|
537
|
+
return {
|
|
538
|
+
source: "version",
|
|
539
|
+
packages,
|
|
540
|
+
metadata: {
|
|
541
|
+
repoUrl: repoUrl ?? void 0
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function parseVersionOutputFile(filePath) {
|
|
546
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
547
|
+
return parseVersionOutput(content);
|
|
548
|
+
}
|
|
549
|
+
async function parseVersionOutputStdin() {
|
|
550
|
+
const chunks = [];
|
|
551
|
+
for await (const chunk of process.stdin) {
|
|
552
|
+
chunks.push(chunk);
|
|
553
|
+
}
|
|
554
|
+
const content = chunks.join("");
|
|
555
|
+
return parseVersionOutput(content);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/output/json.ts
|
|
559
|
+
import * as fs5 from "fs";
|
|
560
|
+
import * as path4 from "path";
|
|
561
|
+
function renderJson(contexts) {
|
|
562
|
+
return JSON.stringify(
|
|
563
|
+
{
|
|
564
|
+
versions: contexts.map((ctx) => ({
|
|
565
|
+
packageName: ctx.packageName,
|
|
566
|
+
version: ctx.version,
|
|
567
|
+
previousVersion: ctx.previousVersion,
|
|
568
|
+
date: ctx.date,
|
|
569
|
+
entries: ctx.entries,
|
|
570
|
+
compareUrl: ctx.compareUrl
|
|
571
|
+
}))
|
|
572
|
+
},
|
|
573
|
+
null,
|
|
574
|
+
2
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
function writeJson(outputPath, contexts, dryRun) {
|
|
578
|
+
const content = renderJson(contexts);
|
|
579
|
+
if (dryRun) {
|
|
580
|
+
info(`Would write JSON output to ${outputPath}`);
|
|
581
|
+
debug("--- JSON Output Preview ---");
|
|
582
|
+
debug(content);
|
|
583
|
+
debug("--- End Preview ---");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const dir = path4.dirname(outputPath);
|
|
587
|
+
if (!fs5.existsSync(dir)) {
|
|
588
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
589
|
+
}
|
|
590
|
+
fs5.writeFileSync(outputPath, content, "utf-8");
|
|
591
|
+
success(`JSON output written to ${outputPath}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/core/pipeline.ts
|
|
595
|
+
import * as fs10 from "fs";
|
|
596
|
+
import * as path8 from "path";
|
|
597
|
+
|
|
598
|
+
// src/llm/defaults.ts
|
|
599
|
+
var LLM_DEFAULTS = {
|
|
600
|
+
timeout: 6e4,
|
|
601
|
+
maxTokens: 4e3,
|
|
602
|
+
temperature: 0.7,
|
|
603
|
+
concurrency: 5,
|
|
604
|
+
retry: {
|
|
605
|
+
maxAttempts: 3,
|
|
606
|
+
initialDelay: 1e3,
|
|
607
|
+
maxDelay: 3e4,
|
|
608
|
+
backoffFactor: 2
|
|
609
|
+
},
|
|
610
|
+
models: {
|
|
611
|
+
openai: "gpt-4o-mini",
|
|
612
|
+
"openai-compatible": "gpt-4o-mini",
|
|
613
|
+
anthropic: "claude-3-5-haiku-latest",
|
|
614
|
+
ollama: "llama3.2"
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/llm/anthropic.ts
|
|
619
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
620
|
+
|
|
621
|
+
// src/llm/base.ts
|
|
622
|
+
var BaseLLMProvider = class {
|
|
623
|
+
getTimeout(options) {
|
|
624
|
+
return options?.timeout ?? LLM_DEFAULTS.timeout;
|
|
625
|
+
}
|
|
626
|
+
getMaxTokens(options) {
|
|
627
|
+
return options?.maxTokens ?? LLM_DEFAULTS.maxTokens;
|
|
628
|
+
}
|
|
629
|
+
getTemperature(options) {
|
|
630
|
+
return options?.temperature ?? LLM_DEFAULTS.temperature;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// src/llm/anthropic.ts
|
|
635
|
+
var AnthropicProvider = class extends BaseLLMProvider {
|
|
636
|
+
name = "anthropic";
|
|
637
|
+
client;
|
|
638
|
+
model;
|
|
639
|
+
constructor(config = {}) {
|
|
640
|
+
super();
|
|
641
|
+
const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
642
|
+
if (!apiKey) {
|
|
643
|
+
throw new LLMError("Anthropic API key not configured. Set ANTHROPIC_API_KEY or use --llm-api-key");
|
|
644
|
+
}
|
|
645
|
+
this.client = new Anthropic({ apiKey });
|
|
646
|
+
this.model = config.model ?? LLM_DEFAULTS.models.anthropic;
|
|
647
|
+
}
|
|
648
|
+
async complete(prompt, options) {
|
|
649
|
+
try {
|
|
650
|
+
const response = await this.client.messages.create({
|
|
651
|
+
model: this.model,
|
|
652
|
+
max_tokens: this.getMaxTokens(options),
|
|
653
|
+
messages: [{ role: "user", content: prompt }]
|
|
654
|
+
});
|
|
655
|
+
const firstBlock = response.content[0];
|
|
656
|
+
if (!firstBlock || firstBlock.type !== "text") {
|
|
657
|
+
throw new LLMError("Unexpected response format from Anthropic");
|
|
658
|
+
}
|
|
659
|
+
return firstBlock.text;
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (error instanceof LLMError) throw error;
|
|
662
|
+
throw new LLMError(`Anthropic API error: ${error instanceof Error ? error.message : String(error)}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// src/llm/ollama.ts
|
|
668
|
+
var OllamaProvider = class extends BaseLLMProvider {
|
|
669
|
+
name = "ollama";
|
|
670
|
+
baseURL;
|
|
671
|
+
model;
|
|
672
|
+
apiKey;
|
|
673
|
+
constructor(config = {}) {
|
|
674
|
+
super();
|
|
675
|
+
this.baseURL = config.baseURL ?? process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
|
|
676
|
+
this.model = config.model ?? LLM_DEFAULTS.models.ollama;
|
|
677
|
+
this.apiKey = config.apiKey ?? process.env.OLLAMA_API_KEY;
|
|
678
|
+
}
|
|
679
|
+
async complete(prompt, options) {
|
|
680
|
+
const requestBody = {
|
|
681
|
+
model: this.model,
|
|
682
|
+
messages: [{ role: "user", content: prompt }],
|
|
683
|
+
stream: false,
|
|
684
|
+
options: {
|
|
685
|
+
num_predict: this.getMaxTokens(options),
|
|
686
|
+
temperature: this.getTemperature(options)
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
try {
|
|
690
|
+
const headers = {
|
|
691
|
+
"Content-Type": "application/json"
|
|
692
|
+
};
|
|
693
|
+
if (this.apiKey) {
|
|
694
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
695
|
+
}
|
|
696
|
+
const baseUrl = this.baseURL.endsWith("/api") ? this.baseURL.slice(0, -4) : this.baseURL;
|
|
697
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
698
|
+
method: "POST",
|
|
699
|
+
headers,
|
|
700
|
+
body: JSON.stringify(requestBody)
|
|
701
|
+
});
|
|
702
|
+
if (!response.ok) {
|
|
703
|
+
const text = await response.text();
|
|
704
|
+
throw new LLMError(`Ollama request failed: ${response.status} ${text}`);
|
|
705
|
+
}
|
|
706
|
+
const data = await response.json();
|
|
707
|
+
if (!data.message?.content) {
|
|
708
|
+
throw new LLMError("Empty response from Ollama");
|
|
709
|
+
}
|
|
710
|
+
return data.message.content;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (error instanceof LLMError) throw error;
|
|
713
|
+
throw new LLMError(`Ollama error: ${error instanceof Error ? error.message : String(error)}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/llm/openai.ts
|
|
719
|
+
import OpenAI from "openai";
|
|
720
|
+
var OpenAIProvider = class extends BaseLLMProvider {
|
|
721
|
+
name = "openai";
|
|
722
|
+
client;
|
|
723
|
+
model;
|
|
724
|
+
constructor(config = {}) {
|
|
725
|
+
super();
|
|
726
|
+
const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
|
|
727
|
+
if (!apiKey) {
|
|
728
|
+
throw new LLMError("OpenAI API key not configured. Set OPENAI_API_KEY or use --llm-api-key");
|
|
729
|
+
}
|
|
730
|
+
this.client = new OpenAI({
|
|
731
|
+
apiKey,
|
|
732
|
+
baseURL: config.baseURL
|
|
733
|
+
});
|
|
734
|
+
this.model = config.model ?? LLM_DEFAULTS.models.openai;
|
|
735
|
+
}
|
|
736
|
+
async complete(prompt, options) {
|
|
737
|
+
try {
|
|
738
|
+
const response = await this.client.chat.completions.create({
|
|
739
|
+
model: this.model,
|
|
740
|
+
messages: [{ role: "user", content: prompt }],
|
|
741
|
+
max_tokens: this.getMaxTokens(options),
|
|
742
|
+
temperature: this.getTemperature(options)
|
|
743
|
+
});
|
|
744
|
+
const content = response.choices[0]?.message?.content;
|
|
745
|
+
if (!content) {
|
|
746
|
+
throw new LLMError("Empty response from OpenAI");
|
|
747
|
+
}
|
|
748
|
+
return content;
|
|
749
|
+
} catch (error) {
|
|
750
|
+
if (error instanceof LLMError) throw error;
|
|
751
|
+
throw new LLMError(`OpenAI API error: ${error instanceof Error ? error.message : String(error)}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/llm/openai-compatible.ts
|
|
757
|
+
import OpenAI2 from "openai";
|
|
758
|
+
var OpenAICompatibleProvider = class extends BaseLLMProvider {
|
|
759
|
+
name = "openai-compatible";
|
|
760
|
+
client;
|
|
761
|
+
model;
|
|
762
|
+
constructor(config) {
|
|
763
|
+
super();
|
|
764
|
+
const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY ?? "dummy";
|
|
765
|
+
this.client = new OpenAI2({
|
|
766
|
+
apiKey,
|
|
767
|
+
baseURL: config.baseURL
|
|
768
|
+
});
|
|
769
|
+
this.model = config.model ?? LLM_DEFAULTS.models["openai-compatible"];
|
|
770
|
+
}
|
|
771
|
+
async complete(prompt, options) {
|
|
772
|
+
try {
|
|
773
|
+
const response = await this.client.chat.completions.create({
|
|
774
|
+
model: this.model,
|
|
775
|
+
messages: [{ role: "user", content: prompt }],
|
|
776
|
+
max_tokens: this.getMaxTokens(options),
|
|
777
|
+
temperature: this.getTemperature(options)
|
|
778
|
+
});
|
|
779
|
+
const content = response.choices[0]?.message?.content;
|
|
780
|
+
if (!content) {
|
|
781
|
+
throw new LLMError("Empty response from LLM");
|
|
782
|
+
}
|
|
783
|
+
return content;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (error instanceof LLMError) throw error;
|
|
786
|
+
throw new LLMError(`LLM API error: ${error instanceof Error ? error.message : String(error)}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/utils/json.ts
|
|
792
|
+
function extractJsonFromResponse(response) {
|
|
793
|
+
const stripped = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
794
|
+
const objectMatch = stripped.match(/\{[\s\S]*\}/);
|
|
795
|
+
if (objectMatch) return objectMatch[0];
|
|
796
|
+
const arrayMatch = stripped.match(/\[[\s\S]*\]/);
|
|
797
|
+
if (arrayMatch) return arrayMatch[0];
|
|
798
|
+
return stripped;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/llm/prompts.ts
|
|
802
|
+
function resolvePrompt(taskName, defaultPrompt, promptsConfig) {
|
|
803
|
+
if (!promptsConfig) return defaultPrompt;
|
|
804
|
+
const fullTemplate = promptsConfig.templates?.[taskName];
|
|
805
|
+
if (fullTemplate) return fullTemplate;
|
|
806
|
+
const additionalInstructions = promptsConfig.instructions?.[taskName];
|
|
807
|
+
if (additionalInstructions) {
|
|
808
|
+
const insertionPoint = defaultPrompt.lastIndexOf("Output only valid JSON");
|
|
809
|
+
if (insertionPoint !== -1) {
|
|
810
|
+
return `${defaultPrompt.slice(0, insertionPoint)}Additional instructions:
|
|
811
|
+
${additionalInstructions}
|
|
812
|
+
|
|
813
|
+
${defaultPrompt.slice(insertionPoint)}`;
|
|
814
|
+
}
|
|
815
|
+
return `${defaultPrompt}
|
|
816
|
+
|
|
817
|
+
Additional instructions:
|
|
818
|
+
${additionalInstructions}`;
|
|
819
|
+
}
|
|
820
|
+
return defaultPrompt;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/llm/scopes.ts
|
|
824
|
+
function getAllowedScopesFromCategories(categories) {
|
|
825
|
+
const scopeMap = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const cat of categories) {
|
|
827
|
+
if (cat.scopes && cat.scopes.length > 0) {
|
|
828
|
+
scopeMap.set(cat.name, cat.scopes);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return scopeMap;
|
|
832
|
+
}
|
|
833
|
+
function resolveAllowedScopes(scopeConfig, categories, packageNames) {
|
|
834
|
+
if (!scopeConfig || scopeConfig.mode === "unrestricted") return null;
|
|
835
|
+
if (scopeConfig.mode === "none") return [];
|
|
836
|
+
if (scopeConfig.mode === "packages") return packageNames ?? [];
|
|
837
|
+
if (scopeConfig.mode === "restricted") {
|
|
838
|
+
const explicit = scopeConfig.rules?.allowed ?? [];
|
|
839
|
+
const all = new Set(explicit);
|
|
840
|
+
if (categories) {
|
|
841
|
+
const fromCategories = getAllowedScopesFromCategories(categories);
|
|
842
|
+
for (const scopes of fromCategories.values()) {
|
|
843
|
+
for (const s of scopes) all.add(s);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return [...all];
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
function validateScope(scope, allowedScopes, rules) {
|
|
851
|
+
if (!scope || allowedScopes === null) return scope;
|
|
852
|
+
if (allowedScopes.length === 0) return void 0;
|
|
853
|
+
const caseSensitive = rules?.caseSensitive ?? false;
|
|
854
|
+
const normalise = (s) => caseSensitive ? s : s.toLowerCase();
|
|
855
|
+
const isAllowed = allowedScopes.some((a) => normalise(a) === normalise(scope));
|
|
856
|
+
if (isAllowed) return scope;
|
|
857
|
+
switch (rules?.invalidScopeAction ?? "remove") {
|
|
858
|
+
case "keep":
|
|
859
|
+
return scope;
|
|
860
|
+
case "fallback":
|
|
861
|
+
return rules?.fallbackScope;
|
|
862
|
+
default:
|
|
863
|
+
return void 0;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function validateEntryScopes(entries, scopeConfig, categories) {
|
|
867
|
+
const allowedScopes = resolveAllowedScopes(scopeConfig, categories);
|
|
868
|
+
if (allowedScopes === null) return entries;
|
|
869
|
+
return entries.map((entry) => ({
|
|
870
|
+
...entry,
|
|
871
|
+
scope: validateScope(entry.scope, allowedScopes, scopeConfig?.rules)
|
|
872
|
+
}));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/llm/tasks/categorize.ts
|
|
876
|
+
var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
|
|
877
|
+
|
|
878
|
+
Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
|
|
879
|
+
|
|
880
|
+
Output a JSON object where keys are category names and values are arrays of entry indices (0-based).
|
|
881
|
+
|
|
882
|
+
Entries:
|
|
883
|
+
{{entries}}
|
|
884
|
+
|
|
885
|
+
Output only valid JSON, nothing else:`;
|
|
886
|
+
function buildCustomCategorizePrompt(categories) {
|
|
887
|
+
const categoryList = categories.map((c) => {
|
|
888
|
+
const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
|
|
889
|
+
return `- "${c.name}": ${c.description}${scopeInfo}`;
|
|
890
|
+
}).join("\n");
|
|
891
|
+
const scopeMap = getAllowedScopesFromCategories(categories);
|
|
892
|
+
let scopeInstructions = "";
|
|
893
|
+
if (scopeMap.size > 0) {
|
|
894
|
+
const entries = [];
|
|
895
|
+
for (const [catName, scopes] of scopeMap) {
|
|
896
|
+
entries.push(`For "${catName}", assign a scope from: ${scopes.join(", ")}.`);
|
|
897
|
+
}
|
|
898
|
+
scopeInstructions = `
|
|
899
|
+
|
|
900
|
+
${entries.join("\n")}
|
|
901
|
+
Only use scopes from these predefined lists. If an entry does not fit any scope, set scope to null.`;
|
|
902
|
+
}
|
|
903
|
+
return `You are categorizing changelog entries for a software release.
|
|
904
|
+
|
|
905
|
+
Given the following entries, group them into the specified categories. Only use the categories listed below in this exact order:
|
|
906
|
+
|
|
907
|
+
Categories:
|
|
908
|
+
${categoryList}${scopeInstructions}
|
|
909
|
+
|
|
910
|
+
Output a JSON object with two fields:
|
|
911
|
+
- "categories": an object where keys are category names and values are arrays of entry indices (0-based)
|
|
912
|
+
- "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.
|
|
913
|
+
|
|
914
|
+
Entries:
|
|
915
|
+
{{entries}}
|
|
916
|
+
|
|
917
|
+
Output only valid JSON, nothing else:`;
|
|
918
|
+
}
|
|
919
|
+
async function categorizeEntries(provider, entries, context) {
|
|
920
|
+
if (entries.length === 0) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
const entriesCopy = entries.map((e) => ({ ...e, scope: void 0 }));
|
|
924
|
+
const entriesText = entriesCopy.map((e, i) => `${i}. [${e.type}]: ${e.description}`).join("\n");
|
|
925
|
+
const hasCustomCategories = context.categories && context.categories.length > 0;
|
|
926
|
+
const defaultPrompt = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
|
|
927
|
+
const promptTemplate = resolvePrompt("categorize", defaultPrompt, context.prompts);
|
|
928
|
+
const prompt = promptTemplate.replace("{{entries}}", entriesText);
|
|
929
|
+
try {
|
|
930
|
+
const response = await provider.complete(prompt);
|
|
931
|
+
const parsed = JSON.parse(extractJsonFromResponse(response));
|
|
932
|
+
const result = [];
|
|
933
|
+
if (hasCustomCategories && parsed.categories) {
|
|
934
|
+
const categoryMap = parsed.categories;
|
|
935
|
+
const scopeMap = parsed.scopes || {};
|
|
936
|
+
for (const [indexStr, scope] of Object.entries(scopeMap)) {
|
|
937
|
+
const idx = Number.parseInt(indexStr, 10);
|
|
938
|
+
if (entriesCopy[idx] && scope && scope.trim()) {
|
|
939
|
+
entriesCopy[idx] = { ...entriesCopy[idx], scope: scope.trim() };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const validatedEntries = validateEntryScopes(entriesCopy, context.scopes, context.categories);
|
|
943
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
944
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
945
|
+
const categoryEntries = indices.map((i) => validatedEntries[i]).filter((e) => e !== void 0);
|
|
946
|
+
if (categoryEntries.length > 0) {
|
|
947
|
+
result.push({ category, entries: categoryEntries });
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
951
|
+
const categoryMap = parsed;
|
|
952
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
953
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
954
|
+
const categoryEntries = indices.map((i) => entriesCopy[i]).filter((e) => e !== void 0);
|
|
955
|
+
if (categoryEntries.length > 0) {
|
|
956
|
+
result.push({ category, entries: categoryEntries });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return result;
|
|
961
|
+
} catch (error) {
|
|
962
|
+
warn(
|
|
963
|
+
`LLM categorization failed, falling back to General: ${error instanceof Error ? error.message : String(error)}`
|
|
964
|
+
);
|
|
965
|
+
return [{ category: "General", entries: entriesCopy }];
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/llm/tasks/enhance.ts
|
|
970
|
+
var DEFAULT_ENHANCE_PROMPT = `You are improving changelog entries for a software project.
|
|
971
|
+
Given a technical commit message, rewrite it as a clear, user-friendly changelog entry.
|
|
972
|
+
|
|
973
|
+
Rules:
|
|
974
|
+
- Be concise (1-2 sentences max)
|
|
975
|
+
- Focus on user impact, not implementation details
|
|
976
|
+
- Don't use technical jargon unless necessary
|
|
977
|
+
- Preserve the scope if mentioned (e.g., "core:", "api:")
|
|
978
|
+
{{style}}
|
|
979
|
+
|
|
980
|
+
Original entry:
|
|
981
|
+
Type: {{type}}
|
|
982
|
+
{{#if scope}}Scope: {{scope}}{{/if}}
|
|
983
|
+
Description: {{description}}
|
|
984
|
+
|
|
985
|
+
Rewritten description (only output the new description, nothing else):`;
|
|
986
|
+
async function enhanceEntry(provider, entry, context) {
|
|
987
|
+
const styleText = context.style ? `- ${context.style}` : '- Use present tense ("Add feature" not "Added feature")';
|
|
988
|
+
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);
|
|
989
|
+
const prompt = resolvePrompt("enhance", defaultPrompt, context.prompts);
|
|
990
|
+
const response = await provider.complete(prompt);
|
|
991
|
+
return response.trim();
|
|
992
|
+
}
|
|
993
|
+
async function enhanceEntries(provider, entries, context, concurrency = LLM_DEFAULTS.concurrency) {
|
|
994
|
+
const results = [];
|
|
995
|
+
for (let i = 0; i < entries.length; i += concurrency) {
|
|
996
|
+
const batch = entries.slice(i, i + concurrency);
|
|
997
|
+
const batchResults = await Promise.all(
|
|
998
|
+
batch.map(async (entry) => {
|
|
999
|
+
try {
|
|
1000
|
+
const newDescription = await enhanceEntry(provider, entry, context);
|
|
1001
|
+
return { ...entry, description: newDescription };
|
|
1002
|
+
} catch {
|
|
1003
|
+
return entry;
|
|
1004
|
+
}
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
results.push(...batchResults);
|
|
1008
|
+
}
|
|
1009
|
+
return results;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/utils/retry.ts
|
|
1013
|
+
function sleep(ms) {
|
|
1014
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1015
|
+
}
|
|
1016
|
+
async function withRetry(fn, options = {}) {
|
|
1017
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
1018
|
+
const initialDelay = options.initialDelay ?? 1e3;
|
|
1019
|
+
const maxDelay = options.maxDelay ?? 3e4;
|
|
1020
|
+
const backoffFactor = options.backoffFactor ?? 2;
|
|
1021
|
+
let lastError;
|
|
1022
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1023
|
+
try {
|
|
1024
|
+
return await fn();
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
lastError = error;
|
|
1027
|
+
if (attempt < maxAttempts - 1) {
|
|
1028
|
+
const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
|
|
1029
|
+
const jitter = base * 0.2 * (Math.random() * 2 - 1);
|
|
1030
|
+
await sleep(Math.max(0, base + jitter));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
throw lastError;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/llm/tasks/enhance-and-categorize.ts
|
|
1038
|
+
function buildPrompt(entries, categories, style) {
|
|
1039
|
+
const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
|
|
1040
|
+
const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
|
|
1041
|
+
const categorySection = categories ? `Categories (use ONLY these):
|
|
1042
|
+
${categories.map((c) => {
|
|
1043
|
+
const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
|
|
1044
|
+
return `- "${c.name}": ${c.description}${scopeInfo}`;
|
|
1045
|
+
}).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
|
|
1046
|
+
let scopeInstruction = "";
|
|
1047
|
+
if (categories) {
|
|
1048
|
+
const scopeMap = getAllowedScopesFromCategories(categories);
|
|
1049
|
+
if (scopeMap.size > 0) {
|
|
1050
|
+
const parts = [];
|
|
1051
|
+
for (const [catName, scopes] of scopeMap) {
|
|
1052
|
+
parts.push(`For "${catName}" entries, assign a scope from: ${scopes.join(", ")}.`);
|
|
1053
|
+
}
|
|
1054
|
+
scopeInstruction = `
|
|
1055
|
+
${parts.join("\n")}
|
|
1056
|
+
Only use scopes from these predefined lists. Set scope to null if no scope applies.`;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return `You are generating release notes for a software project. Given the following changelog entries, do two things:
|
|
1060
|
+
|
|
1061
|
+
1. **Rewrite** each entry as a clear, user-friendly description
|
|
1062
|
+
2. **Categorize** each entry into the appropriate category
|
|
1063
|
+
|
|
1064
|
+
Style guidelines:
|
|
1065
|
+
- ${styleText}
|
|
1066
|
+
- Be concise (1 short sentence per entry)
|
|
1067
|
+
- Focus on what changed, not implementation details
|
|
1068
|
+
|
|
1069
|
+
${categorySection}${scopeInstruction}
|
|
1070
|
+
|
|
1071
|
+
Entries:
|
|
1072
|
+
${entriesText}
|
|
1073
|
+
|
|
1074
|
+
Output a JSON object with:
|
|
1075
|
+
- "entries": array of objects, one per input entry (same order), each with: { "description": "rewritten text", "category": "CategoryName", "scope": "optional subcategory label or null" }
|
|
1076
|
+
|
|
1077
|
+
Output only valid JSON, nothing else:`;
|
|
1078
|
+
}
|
|
1079
|
+
async function enhanceAndCategorize(provider, entries, context) {
|
|
1080
|
+
if (entries.length === 0) {
|
|
1081
|
+
return { enhancedEntries: [], categories: [] };
|
|
1082
|
+
}
|
|
1083
|
+
const retryOpts = LLM_DEFAULTS.retry;
|
|
1084
|
+
try {
|
|
1085
|
+
return await withRetry(async () => {
|
|
1086
|
+
const defaultPrompt = buildPrompt(entries, context.categories, context.style);
|
|
1087
|
+
const prompt = resolvePrompt("enhanceAndCategorize", defaultPrompt, context.prompts);
|
|
1088
|
+
const response = await provider.complete(prompt);
|
|
1089
|
+
const parsed = JSON.parse(extractJsonFromResponse(response));
|
|
1090
|
+
if (!Array.isArray(parsed.entries)) {
|
|
1091
|
+
throw new Error('Response missing "entries" array');
|
|
1092
|
+
}
|
|
1093
|
+
const enhancedEntries = entries.map((original, i) => {
|
|
1094
|
+
const result = parsed.entries[i];
|
|
1095
|
+
if (!result) return original;
|
|
1096
|
+
return {
|
|
1097
|
+
...original,
|
|
1098
|
+
description: result.description || original.description,
|
|
1099
|
+
scope: result.scope || original.scope
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
const validatedEntries = validateEntryScopes(enhancedEntries, context.scopes, context.categories);
|
|
1103
|
+
const categoryMap = /* @__PURE__ */ new Map();
|
|
1104
|
+
for (let i = 0; i < parsed.entries.length; i++) {
|
|
1105
|
+
const result = parsed.entries[i];
|
|
1106
|
+
const category = result?.category || "General";
|
|
1107
|
+
const entry = validatedEntries[i];
|
|
1108
|
+
if (!entry) continue;
|
|
1109
|
+
if (!categoryMap.has(category)) {
|
|
1110
|
+
categoryMap.set(category, []);
|
|
1111
|
+
}
|
|
1112
|
+
categoryMap.get(category)?.push(entry);
|
|
1113
|
+
}
|
|
1114
|
+
const categories = [];
|
|
1115
|
+
for (const [category, catEntries] of categoryMap) {
|
|
1116
|
+
categories.push({ category, entries: catEntries });
|
|
1117
|
+
}
|
|
1118
|
+
return { enhancedEntries: validatedEntries, categories };
|
|
1119
|
+
}, retryOpts);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
warn(
|
|
1122
|
+
`Combined enhance+categorize failed after ${retryOpts.maxAttempts} attempts: ${error instanceof Error ? error.message : String(error)}`
|
|
1123
|
+
);
|
|
1124
|
+
return {
|
|
1125
|
+
enhancedEntries: entries,
|
|
1126
|
+
categories: [{ category: "General", entries }]
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/llm/tasks/release-notes.ts
|
|
1132
|
+
var DEFAULT_RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
|
|
1133
|
+
|
|
1134
|
+
Create engaging, user-friendly release notes for the following changes.
|
|
1135
|
+
|
|
1136
|
+
Rules:
|
|
1137
|
+
- Start with a brief introduction (1-2 sentences)
|
|
1138
|
+
- Group related changes into sections
|
|
1139
|
+
- Use friendly, approachable language
|
|
1140
|
+
- Highlight breaking changes prominently
|
|
1141
|
+
- End with a brief conclusion or call to action
|
|
1142
|
+
- Use markdown formatting
|
|
1143
|
+
|
|
1144
|
+
Version: {{version}}
|
|
1145
|
+
{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}
|
|
1146
|
+
Date: {{date}}
|
|
1147
|
+
|
|
1148
|
+
Changes:
|
|
1149
|
+
{{entries}}
|
|
1150
|
+
|
|
1151
|
+
Release notes (output only the markdown content):`;
|
|
1152
|
+
async function generateReleaseNotes(provider, entries, context) {
|
|
1153
|
+
if (entries.length === 0) {
|
|
1154
|
+
return `## Release ${context.version ?? "v1.0.0"}
|
|
1155
|
+
|
|
1156
|
+
No notable changes in this release.`;
|
|
1157
|
+
}
|
|
1158
|
+
const entriesText = entries.map((e) => {
|
|
1159
|
+
let line = `- [${e.type}]`;
|
|
1160
|
+
if (e.scope) line += ` (${e.scope})`;
|
|
1161
|
+
line += `: ${e.description}`;
|
|
1162
|
+
if (e.breaking) line += " **BREAKING**";
|
|
1163
|
+
return line;
|
|
1164
|
+
}).join("\n");
|
|
1165
|
+
const defaultPrompt = DEFAULT_RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
|
|
1166
|
+
"{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}",
|
|
1167
|
+
context.previousVersion ? `Previous version: ${context.previousVersion}` : ""
|
|
1168
|
+
).replace("{{date}}", context.date ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "").replace("{{entries}}", entriesText);
|
|
1169
|
+
const prompt = resolvePrompt("releaseNotes", defaultPrompt, context.prompts);
|
|
1170
|
+
const response = await provider.complete(prompt);
|
|
1171
|
+
return response.trim();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/llm/tasks/summarize.ts
|
|
1175
|
+
var DEFAULT_SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
|
|
1176
|
+
|
|
1177
|
+
Given the following changelog entries, create a brief summary (2-3 sentences) that captures the main themes of this release.
|
|
1178
|
+
|
|
1179
|
+
Entries:
|
|
1180
|
+
{{entries}}
|
|
1181
|
+
|
|
1182
|
+
Summary (only output the summary, nothing else):`;
|
|
1183
|
+
async function summarizeEntries(provider, entries, context) {
|
|
1184
|
+
if (entries.length === 0) {
|
|
1185
|
+
return "";
|
|
1186
|
+
}
|
|
1187
|
+
const entriesText = entries.map((e) => `- [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
|
|
1188
|
+
const defaultPrompt = DEFAULT_SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
|
|
1189
|
+
const prompt = resolvePrompt("summarize", defaultPrompt, context.prompts);
|
|
1190
|
+
const response = await provider.complete(prompt);
|
|
1191
|
+
return response.trim();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/llm/index.ts
|
|
1195
|
+
function createProvider(config) {
|
|
1196
|
+
const authKeys = loadAuth();
|
|
1197
|
+
const apiKey = config.apiKey ?? authKeys[config.provider];
|
|
1198
|
+
switch (config.provider) {
|
|
1199
|
+
case "openai":
|
|
1200
|
+
return new OpenAIProvider({
|
|
1201
|
+
apiKey,
|
|
1202
|
+
baseURL: config.baseURL,
|
|
1203
|
+
model: config.model
|
|
1204
|
+
});
|
|
1205
|
+
case "anthropic":
|
|
1206
|
+
return new AnthropicProvider({
|
|
1207
|
+
apiKey,
|
|
1208
|
+
model: config.model
|
|
1209
|
+
});
|
|
1210
|
+
case "ollama":
|
|
1211
|
+
return new OllamaProvider({
|
|
1212
|
+
apiKey,
|
|
1213
|
+
baseURL: config.baseURL,
|
|
1214
|
+
model: config.model
|
|
1215
|
+
});
|
|
1216
|
+
case "openai-compatible": {
|
|
1217
|
+
if (!config.baseURL) {
|
|
1218
|
+
throw new LLMError("openai-compatible provider requires baseURL");
|
|
1219
|
+
}
|
|
1220
|
+
return new OpenAICompatibleProvider({
|
|
1221
|
+
apiKey,
|
|
1222
|
+
baseURL: config.baseURL,
|
|
1223
|
+
model: config.model
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
default:
|
|
1227
|
+
throw new LLMError(`Unknown LLM provider: ${config.provider}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/output/github-release.ts
|
|
1232
|
+
import { Octokit } from "@octokit/rest";
|
|
1233
|
+
var GitHubClient = class {
|
|
1234
|
+
octokit;
|
|
1235
|
+
owner;
|
|
1236
|
+
repo;
|
|
1237
|
+
constructor(options) {
|
|
1238
|
+
const token = options.token ?? process.env.GITHUB_TOKEN;
|
|
1239
|
+
if (!token) {
|
|
1240
|
+
throw new GitHubError("GITHUB_TOKEN not set. Set it as an environment variable.");
|
|
1241
|
+
}
|
|
1242
|
+
this.octokit = new Octokit({ auth: token });
|
|
1243
|
+
this.owner = options.owner;
|
|
1244
|
+
this.repo = options.repo;
|
|
1245
|
+
}
|
|
1246
|
+
async createRelease(context, options = {}) {
|
|
1247
|
+
const tagName = `v${context.version}`;
|
|
1248
|
+
let body;
|
|
1249
|
+
if (context.enhanced?.releaseNotes) {
|
|
1250
|
+
body = context.enhanced.releaseNotes;
|
|
1251
|
+
} else {
|
|
1252
|
+
body = renderMarkdown([context]);
|
|
1253
|
+
}
|
|
1254
|
+
info(`Creating GitHub release for ${tagName}`);
|
|
1255
|
+
try {
|
|
1256
|
+
const response = await this.octokit.repos.createRelease({
|
|
1257
|
+
owner: this.owner,
|
|
1258
|
+
repo: this.repo,
|
|
1259
|
+
tag_name: tagName,
|
|
1260
|
+
name: tagName,
|
|
1261
|
+
body,
|
|
1262
|
+
draft: options.draft ?? false,
|
|
1263
|
+
prerelease: options.prerelease ?? false,
|
|
1264
|
+
generate_release_notes: options.generateNotes ?? false
|
|
1265
|
+
});
|
|
1266
|
+
success(`Release created: ${response.data.html_url}`);
|
|
1267
|
+
return {
|
|
1268
|
+
id: response.data.id,
|
|
1269
|
+
htmlUrl: response.data.html_url,
|
|
1270
|
+
tagName
|
|
1271
|
+
};
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
throw new GitHubError(`Failed to create release: ${error instanceof Error ? error.message : String(error)}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async updateRelease(releaseId, context, options = {}) {
|
|
1277
|
+
const tagName = `v${context.version}`;
|
|
1278
|
+
let body;
|
|
1279
|
+
if (context.enhanced?.releaseNotes) {
|
|
1280
|
+
body = context.enhanced.releaseNotes;
|
|
1281
|
+
} else {
|
|
1282
|
+
body = renderMarkdown([context]);
|
|
1283
|
+
}
|
|
1284
|
+
info(`Updating GitHub release ${releaseId}`);
|
|
1285
|
+
try {
|
|
1286
|
+
const response = await this.octokit.repos.updateRelease({
|
|
1287
|
+
owner: this.owner,
|
|
1288
|
+
repo: this.repo,
|
|
1289
|
+
release_id: releaseId,
|
|
1290
|
+
tag_name: tagName,
|
|
1291
|
+
name: tagName,
|
|
1292
|
+
body,
|
|
1293
|
+
draft: options.draft ?? false,
|
|
1294
|
+
prerelease: options.prerelease ?? false
|
|
1295
|
+
});
|
|
1296
|
+
success(`Release updated: ${response.data.html_url}`);
|
|
1297
|
+
return {
|
|
1298
|
+
id: response.data.id,
|
|
1299
|
+
htmlUrl: response.data.html_url,
|
|
1300
|
+
tagName
|
|
1301
|
+
};
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
throw new GitHubError(`Failed to update release: ${error instanceof Error ? error.message : String(error)}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async getReleaseByTag(tag) {
|
|
1307
|
+
try {
|
|
1308
|
+
const response = await this.octokit.repos.getReleaseByTag({
|
|
1309
|
+
owner: this.owner,
|
|
1310
|
+
repo: this.repo,
|
|
1311
|
+
tag
|
|
1312
|
+
});
|
|
1313
|
+
return {
|
|
1314
|
+
id: response.data.id,
|
|
1315
|
+
htmlUrl: response.data.html_url,
|
|
1316
|
+
tagName: response.data.tag_name
|
|
1317
|
+
};
|
|
1318
|
+
} catch {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
function parseRepoUrl(repoUrl) {
|
|
1324
|
+
const patterns = [
|
|
1325
|
+
/^https:\/\/github\.com\/([^/]+)\/([^/]+)/,
|
|
1326
|
+
/^git@github\.com:([^/]+)\/([^/]+)/,
|
|
1327
|
+
/^github\.com\/([^/]+)\/([^/]+)/
|
|
1328
|
+
];
|
|
1329
|
+
for (const pattern of patterns) {
|
|
1330
|
+
const match = repoUrl.match(pattern);
|
|
1331
|
+
if (match?.[1] && match[2]) {
|
|
1332
|
+
return {
|
|
1333
|
+
owner: match[1],
|
|
1334
|
+
repo: match[2].replace(/\.git$/, "")
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
async function createGitHubRelease(context, options) {
|
|
1341
|
+
const client = new GitHubClient(options);
|
|
1342
|
+
return client.createRelease(context, options);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/templates/ejs.ts
|
|
1346
|
+
import * as fs6 from "fs";
|
|
1347
|
+
import ejs from "ejs";
|
|
1348
|
+
function renderEjs(template, context) {
|
|
1349
|
+
try {
|
|
1350
|
+
return ejs.render(template, context);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
throw new TemplateError(`EJS render error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function renderEjsFile(filePath, context) {
|
|
1356
|
+
if (!fs6.existsSync(filePath)) {
|
|
1357
|
+
throw new TemplateError(`Template file not found: ${filePath}`);
|
|
1358
|
+
}
|
|
1359
|
+
const template = fs6.readFileSync(filePath, "utf-8");
|
|
1360
|
+
return renderEjs(template, context);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/templates/handlebars.ts
|
|
1364
|
+
import * as fs7 from "fs";
|
|
1365
|
+
import * as path5 from "path";
|
|
1366
|
+
import Handlebars from "handlebars";
|
|
1367
|
+
function registerHandlebarsHelpers() {
|
|
1368
|
+
Handlebars.registerHelper("capitalize", (str) => {
|
|
1369
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1370
|
+
});
|
|
1371
|
+
Handlebars.registerHelper("eq", (a, b) => {
|
|
1372
|
+
return a === b;
|
|
1373
|
+
});
|
|
1374
|
+
Handlebars.registerHelper("ne", (a, b) => {
|
|
1375
|
+
return a !== b;
|
|
1376
|
+
});
|
|
1377
|
+
Handlebars.registerHelper("join", (arr, separator) => {
|
|
1378
|
+
return Array.isArray(arr) ? arr.join(separator) : "";
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
function renderHandlebars(template, context) {
|
|
1382
|
+
registerHandlebarsHelpers();
|
|
1383
|
+
try {
|
|
1384
|
+
const compiled = Handlebars.compile(template);
|
|
1385
|
+
return compiled(context);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
throw new TemplateError(`Handlebars render error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function renderHandlebarsFile(filePath, context) {
|
|
1391
|
+
if (!fs7.existsSync(filePath)) {
|
|
1392
|
+
throw new TemplateError(`Template file not found: ${filePath}`);
|
|
1393
|
+
}
|
|
1394
|
+
const template = fs7.readFileSync(filePath, "utf-8");
|
|
1395
|
+
return renderHandlebars(template, context);
|
|
1396
|
+
}
|
|
1397
|
+
function renderHandlebarsComposable(templateDir, context) {
|
|
1398
|
+
registerHandlebarsHelpers();
|
|
1399
|
+
const versionPath = path5.join(templateDir, "version.hbs");
|
|
1400
|
+
const entryPath = path5.join(templateDir, "entry.hbs");
|
|
1401
|
+
const documentPath = path5.join(templateDir, "document.hbs");
|
|
1402
|
+
if (!fs7.existsSync(documentPath)) {
|
|
1403
|
+
throw new TemplateError(`Document template not found: ${documentPath}`);
|
|
1404
|
+
}
|
|
1405
|
+
if (fs7.existsSync(versionPath)) {
|
|
1406
|
+
Handlebars.registerPartial("version", fs7.readFileSync(versionPath, "utf-8"));
|
|
1407
|
+
}
|
|
1408
|
+
if (fs7.existsSync(entryPath)) {
|
|
1409
|
+
Handlebars.registerPartial("entry", fs7.readFileSync(entryPath, "utf-8"));
|
|
1410
|
+
}
|
|
1411
|
+
try {
|
|
1412
|
+
const compiled = Handlebars.compile(fs7.readFileSync(documentPath, "utf-8"));
|
|
1413
|
+
return compiled(context);
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
throw new TemplateError(`Handlebars render error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/templates/liquid.ts
|
|
1420
|
+
import * as fs8 from "fs";
|
|
1421
|
+
import * as path6 from "path";
|
|
1422
|
+
import { Liquid } from "liquidjs";
|
|
1423
|
+
function createLiquidEngine(root) {
|
|
1424
|
+
return new Liquid({
|
|
1425
|
+
root: root ? [root] : [],
|
|
1426
|
+
extname: ".liquid",
|
|
1427
|
+
cache: false
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
function renderLiquid(template, context) {
|
|
1431
|
+
const engine = createLiquidEngine();
|
|
1432
|
+
try {
|
|
1433
|
+
return engine.renderSync(engine.parse(template), context);
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
throw new TemplateError(`Liquid render error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function renderLiquidFile(filePath, context) {
|
|
1439
|
+
if (!fs8.existsSync(filePath)) {
|
|
1440
|
+
throw new TemplateError(`Template file not found: ${filePath}`);
|
|
1441
|
+
}
|
|
1442
|
+
const template = fs8.readFileSync(filePath, "utf-8");
|
|
1443
|
+
return renderLiquid(template, context);
|
|
1444
|
+
}
|
|
1445
|
+
function renderLiquidComposable(templateDir, context) {
|
|
1446
|
+
const documentPath = path6.join(templateDir, "document.liquid");
|
|
1447
|
+
if (!fs8.existsSync(documentPath)) {
|
|
1448
|
+
throw new TemplateError(`Document template not found: ${documentPath}`);
|
|
1449
|
+
}
|
|
1450
|
+
const engine = createLiquidEngine(templateDir);
|
|
1451
|
+
try {
|
|
1452
|
+
return engine.renderFileSync("document", context);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
throw new TemplateError(`Liquid render error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/templates/loader.ts
|
|
1459
|
+
import * as fs9 from "fs";
|
|
1460
|
+
import * as path7 from "path";
|
|
1461
|
+
function getEngineFromFile(filePath) {
|
|
1462
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
1463
|
+
switch (ext) {
|
|
1464
|
+
case ".liquid":
|
|
1465
|
+
return "liquid";
|
|
1466
|
+
case ".hbs":
|
|
1467
|
+
case ".handlebars":
|
|
1468
|
+
return "handlebars";
|
|
1469
|
+
case ".ejs":
|
|
1470
|
+
return "ejs";
|
|
1471
|
+
default:
|
|
1472
|
+
throw new TemplateError(`Unknown template extension: ${ext}`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function getRenderFn(engine) {
|
|
1476
|
+
switch (engine) {
|
|
1477
|
+
case "liquid":
|
|
1478
|
+
return renderLiquid;
|
|
1479
|
+
case "handlebars":
|
|
1480
|
+
return renderHandlebars;
|
|
1481
|
+
case "ejs":
|
|
1482
|
+
return renderEjs;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
function getRenderFileFn(engine) {
|
|
1486
|
+
switch (engine) {
|
|
1487
|
+
case "liquid":
|
|
1488
|
+
return renderLiquidFile;
|
|
1489
|
+
case "handlebars":
|
|
1490
|
+
return renderHandlebarsFile;
|
|
1491
|
+
case "ejs":
|
|
1492
|
+
return renderEjsFile;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function detectTemplateMode(templatePath) {
|
|
1496
|
+
if (!fs9.existsSync(templatePath)) {
|
|
1497
|
+
throw new TemplateError(`Template path not found: ${templatePath}`);
|
|
1498
|
+
}
|
|
1499
|
+
const stat = fs9.statSync(templatePath);
|
|
1500
|
+
if (stat.isFile()) {
|
|
1501
|
+
return "single";
|
|
1502
|
+
}
|
|
1503
|
+
if (stat.isDirectory()) {
|
|
1504
|
+
return "composable";
|
|
1505
|
+
}
|
|
1506
|
+
throw new TemplateError(`Invalid template path: ${templatePath}`);
|
|
1507
|
+
}
|
|
1508
|
+
function renderSingleFile(templatePath, context, engine) {
|
|
1509
|
+
const resolvedEngine = engine ?? getEngineFromFile(templatePath);
|
|
1510
|
+
const renderFile = getRenderFileFn(resolvedEngine);
|
|
1511
|
+
return {
|
|
1512
|
+
content: renderFile(templatePath, context),
|
|
1513
|
+
engine: resolvedEngine
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function renderComposable(templateDir, context, engine) {
|
|
1517
|
+
const files = fs9.readdirSync(templateDir);
|
|
1518
|
+
const engineMap = {
|
|
1519
|
+
liquid: { document: "document.liquid", version: "version.liquid", entry: "entry.liquid" },
|
|
1520
|
+
handlebars: { document: "document.hbs", version: "version.hbs", entry: "entry.hbs" },
|
|
1521
|
+
ejs: { document: "document.ejs", version: "version.ejs", entry: "entry.ejs" }
|
|
1522
|
+
};
|
|
1523
|
+
let resolvedEngine;
|
|
1524
|
+
if (engine) {
|
|
1525
|
+
resolvedEngine = engine;
|
|
1526
|
+
} else {
|
|
1527
|
+
const detected = detectEngineFromFiles(templateDir, files);
|
|
1528
|
+
if (!detected) {
|
|
1529
|
+
throw new TemplateError(`Could not detect template engine. Found files: ${files.join(", ")}`);
|
|
1530
|
+
}
|
|
1531
|
+
resolvedEngine = detected;
|
|
1532
|
+
}
|
|
1533
|
+
if (resolvedEngine === "liquid") {
|
|
1534
|
+
return { content: renderLiquidComposable(templateDir, context), engine: resolvedEngine };
|
|
1535
|
+
}
|
|
1536
|
+
if (resolvedEngine === "handlebars") {
|
|
1537
|
+
return { content: renderHandlebarsComposable(templateDir, context), engine: resolvedEngine };
|
|
1538
|
+
}
|
|
1539
|
+
const expectedFiles = engineMap[resolvedEngine];
|
|
1540
|
+
const documentPath = path7.join(templateDir, expectedFiles.document);
|
|
1541
|
+
if (!fs9.existsSync(documentPath)) {
|
|
1542
|
+
throw new TemplateError(`Document template not found: ${expectedFiles.document}`);
|
|
1543
|
+
}
|
|
1544
|
+
const versionPath = path7.join(templateDir, expectedFiles.version);
|
|
1545
|
+
const entryPath = path7.join(templateDir, expectedFiles.entry);
|
|
1546
|
+
const render = getRenderFn(resolvedEngine);
|
|
1547
|
+
const entryTemplate = fs9.existsSync(entryPath) ? fs9.readFileSync(entryPath, "utf-8") : null;
|
|
1548
|
+
const versionTemplate = fs9.existsSync(versionPath) ? fs9.readFileSync(versionPath, "utf-8") : null;
|
|
1549
|
+
if (entryTemplate && versionTemplate) {
|
|
1550
|
+
const versionsWithEntries = context.versions.map((versionCtx) => {
|
|
1551
|
+
const entries = versionCtx.entries.map((entry) => {
|
|
1552
|
+
const entryCtx = { ...entry, packageName: versionCtx.packageName, version: versionCtx.version };
|
|
1553
|
+
return render(entryTemplate, entryCtx);
|
|
1554
|
+
});
|
|
1555
|
+
return render(versionTemplate, { ...versionCtx, renderedEntries: entries });
|
|
1556
|
+
});
|
|
1557
|
+
const docContext = { ...context, renderedVersions: versionsWithEntries };
|
|
1558
|
+
return {
|
|
1559
|
+
content: render(fs9.readFileSync(documentPath, "utf-8"), docContext),
|
|
1560
|
+
engine: resolvedEngine
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return renderSingleFile(documentPath, context, resolvedEngine);
|
|
1564
|
+
}
|
|
1565
|
+
function detectEngineFromFiles(_dir, files) {
|
|
1566
|
+
if (files.some((f) => f.endsWith(".liquid"))) return "liquid";
|
|
1567
|
+
if (files.some((f) => f.endsWith(".hbs") || f.endsWith(".handlebars"))) return "handlebars";
|
|
1568
|
+
if (files.some((f) => f.endsWith(".ejs"))) return "ejs";
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
function validateDocumentContext(context, templatePath) {
|
|
1572
|
+
if (!context.project?.name) {
|
|
1573
|
+
throw new TemplateError(`${templatePath}: DocumentContext missing required field "project.name"`);
|
|
1574
|
+
}
|
|
1575
|
+
if (!Array.isArray(context.versions)) {
|
|
1576
|
+
throw new TemplateError(`${templatePath}: DocumentContext missing required field "versions" (must be an array)`);
|
|
1577
|
+
}
|
|
1578
|
+
const requiredVersionFields = ["packageName", "version", "date", "entries"];
|
|
1579
|
+
for (const [i, v] of context.versions.entries()) {
|
|
1580
|
+
for (const field of requiredVersionFields) {
|
|
1581
|
+
if (v[field] === void 0 || v[field] === null) {
|
|
1582
|
+
throw new TemplateError(`${templatePath}: versions[${i}] missing required field "${field}"`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (!Array.isArray(v.entries)) {
|
|
1586
|
+
throw new TemplateError(`${templatePath}: versions[${i}].entries must be an array`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function renderTemplate(templatePath, context, engine) {
|
|
1591
|
+
validateDocumentContext(context, templatePath);
|
|
1592
|
+
const mode = detectTemplateMode(templatePath);
|
|
1593
|
+
if (mode === "single") {
|
|
1594
|
+
return renderSingleFile(templatePath, context, engine);
|
|
1595
|
+
}
|
|
1596
|
+
return renderComposable(templatePath, context, engine);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/core/pipeline.ts
|
|
1600
|
+
function generateCompareUrl(repoUrl, from, to, packageName) {
|
|
1601
|
+
const isPackageSpecific = from.includes("@") && packageName && from.includes(packageName);
|
|
1602
|
+
let fromVersion;
|
|
1603
|
+
let toVersion;
|
|
1604
|
+
if (isPackageSpecific) {
|
|
1605
|
+
fromVersion = from;
|
|
1606
|
+
toVersion = `${packageName}@${to.startsWith("v") ? "" : "v"}${to}`;
|
|
1607
|
+
} else {
|
|
1608
|
+
fromVersion = from.replace(/^v/, "");
|
|
1609
|
+
toVersion = to.replace(/^v/, "");
|
|
1610
|
+
}
|
|
1611
|
+
if (/gitlab\.com/i.test(repoUrl)) {
|
|
1612
|
+
return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
|
|
1613
|
+
}
|
|
1614
|
+
if (/bitbucket\.org/i.test(repoUrl)) {
|
|
1615
|
+
return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
|
|
1616
|
+
}
|
|
1617
|
+
return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
|
|
1618
|
+
}
|
|
1619
|
+
function buildOrderedCategories(rawCategories, configCategories) {
|
|
1620
|
+
const order = configCategories?.map((c) => c.name) ?? [];
|
|
1621
|
+
const mapped = rawCategories.map((c) => ({ name: c.category, entries: c.entries }));
|
|
1622
|
+
if (order.length === 0) return mapped;
|
|
1623
|
+
return mapped.sort((a, b) => {
|
|
1624
|
+
const ai = order.indexOf(a.name);
|
|
1625
|
+
const bi = order.indexOf(b.name);
|
|
1626
|
+
return (ai === -1 ? order.length : ai) - (bi === -1 ? order.length : bi);
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
function createTemplateContext(pkg) {
|
|
1630
|
+
const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version, pkg.packageName) : void 0;
|
|
1631
|
+
return {
|
|
1632
|
+
packageName: pkg.packageName,
|
|
1633
|
+
version: pkg.version,
|
|
1634
|
+
previousVersion: pkg.previousVersion,
|
|
1635
|
+
date: pkg.date,
|
|
1636
|
+
repoUrl: pkg.repoUrl,
|
|
1637
|
+
entries: pkg.entries,
|
|
1638
|
+
compareUrl
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function createDocumentContext(contexts, repoUrl) {
|
|
1642
|
+
const compareUrls = {};
|
|
1643
|
+
for (const ctx of contexts) {
|
|
1644
|
+
if (ctx.compareUrl) {
|
|
1645
|
+
compareUrls[ctx.version] = ctx.compareUrl;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
project: {
|
|
1650
|
+
name: contexts[0]?.packageName ?? "project",
|
|
1651
|
+
repoUrl
|
|
1652
|
+
},
|
|
1653
|
+
versions: contexts,
|
|
1654
|
+
compareUrls: Object.keys(compareUrls).length > 0 ? compareUrls : void 0
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
async function processWithLLM(context, config) {
|
|
1658
|
+
if (!config.llm) {
|
|
1659
|
+
return context;
|
|
1660
|
+
}
|
|
1661
|
+
const tasks = config.llm.tasks ?? {};
|
|
1662
|
+
const llmContext = {
|
|
1663
|
+
packageName: context.packageName,
|
|
1664
|
+
version: context.version,
|
|
1665
|
+
previousVersion: context.previousVersion ?? void 0,
|
|
1666
|
+
date: context.date,
|
|
1667
|
+
categories: config.llm.categories,
|
|
1668
|
+
style: config.llm.style,
|
|
1669
|
+
scopes: config.llm.scopes,
|
|
1670
|
+
prompts: config.llm.prompts
|
|
1671
|
+
};
|
|
1672
|
+
const enhanced = {
|
|
1673
|
+
entries: context.entries
|
|
1674
|
+
};
|
|
1675
|
+
try {
|
|
1676
|
+
info(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1677
|
+
if (config.llm.baseURL) {
|
|
1678
|
+
info(`LLM base URL: ${config.llm.baseURL}`);
|
|
1679
|
+
}
|
|
1680
|
+
const rawProvider = createProvider(config.llm);
|
|
1681
|
+
const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
|
|
1682
|
+
const configOptions = config.llm.options;
|
|
1683
|
+
const provider = {
|
|
1684
|
+
name: rawProvider.name,
|
|
1685
|
+
// Merge user-configured options (timeout, maxTokens, temperature) as base defaults,
|
|
1686
|
+
// allowing any per-call overrides to take precedence.
|
|
1687
|
+
complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, { ...configOptions, ...opts }), retryOpts)
|
|
1688
|
+
};
|
|
1689
|
+
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1690
|
+
info(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1691
|
+
if (tasks.enhance && tasks.categorize) {
|
|
1692
|
+
info("Enhancing and categorizing entries with LLM...");
|
|
1693
|
+
const result = await enhanceAndCategorize(provider, context.entries, llmContext);
|
|
1694
|
+
enhanced.entries = result.enhancedEntries;
|
|
1695
|
+
enhanced.categories = buildOrderedCategories(result.categories, llmContext.categories);
|
|
1696
|
+
info(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
|
|
1697
|
+
} else {
|
|
1698
|
+
if (tasks.enhance) {
|
|
1699
|
+
info("Enhancing entries with LLM...");
|
|
1700
|
+
enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
|
|
1701
|
+
info(`Enhanced ${enhanced.entries.length} entries`);
|
|
1702
|
+
}
|
|
1703
|
+
if (tasks.categorize) {
|
|
1704
|
+
info("Categorizing entries with LLM...");
|
|
1705
|
+
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1706
|
+
enhanced.categories = buildOrderedCategories(categorized, llmContext.categories);
|
|
1707
|
+
info(`Created ${categorized.length} categories`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (tasks.summarize) {
|
|
1711
|
+
info("Summarizing entries with LLM...");
|
|
1712
|
+
enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
|
|
1713
|
+
if (enhanced.summary) {
|
|
1714
|
+
info("Summary generated successfully");
|
|
1715
|
+
debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1716
|
+
} else {
|
|
1717
|
+
warn("Summary generation returned empty result");
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (tasks.releaseNotes) {
|
|
1721
|
+
info("Generating release notes with LLM...");
|
|
1722
|
+
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1723
|
+
if (enhanced.releaseNotes) {
|
|
1724
|
+
info("Release notes generated successfully");
|
|
1725
|
+
} else {
|
|
1726
|
+
warn("Release notes generation returned empty result");
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return {
|
|
1730
|
+
...context,
|
|
1731
|
+
enhanced
|
|
1732
|
+
};
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
warn(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1735
|
+
warn("Falling back to raw entries");
|
|
1736
|
+
return context;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function getBuiltinTemplatePath(style) {
|
|
1740
|
+
let packageRoot;
|
|
1741
|
+
try {
|
|
1742
|
+
const currentUrl = import.meta.url;
|
|
1743
|
+
packageRoot = path8.dirname(new URL(currentUrl).pathname);
|
|
1744
|
+
packageRoot = path8.join(packageRoot, "..", "..");
|
|
1745
|
+
} catch {
|
|
1746
|
+
packageRoot = __dirname;
|
|
1747
|
+
}
|
|
1748
|
+
return path8.join(packageRoot, "templates", style);
|
|
1749
|
+
}
|
|
1750
|
+
async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
1751
|
+
let templatePath;
|
|
1752
|
+
if (config.templates?.path) {
|
|
1753
|
+
templatePath = path8.resolve(config.templates.path);
|
|
1754
|
+
} else {
|
|
1755
|
+
templatePath = getBuiltinTemplatePath("keep-a-changelog");
|
|
1756
|
+
}
|
|
1757
|
+
const documentContext = createDocumentContext(
|
|
1758
|
+
contexts,
|
|
1759
|
+
config.templates?.path ? void 0 : contexts[0]?.repoUrl ?? void 0
|
|
1760
|
+
);
|
|
1761
|
+
const result = renderTemplate(templatePath, documentContext, config.templates?.engine);
|
|
1762
|
+
if (dryRun) {
|
|
1763
|
+
info(`[DRY RUN] Changelog preview (would write to ${outputPath}):`);
|
|
1764
|
+
info(result.content);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (outputPath === "-") {
|
|
1768
|
+
process.stdout.write(result.content);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
const dir = path8.dirname(outputPath);
|
|
1772
|
+
if (!fs10.existsSync(dir)) {
|
|
1773
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
1774
|
+
}
|
|
1775
|
+
fs10.writeFileSync(outputPath, result.content, "utf-8");
|
|
1776
|
+
const label = /changelog/i.test(outputPath) ? "Changelog" : "Release notes";
|
|
1777
|
+
success(`${label} written to ${outputPath} (using ${result.engine} template)`);
|
|
1778
|
+
}
|
|
1779
|
+
async function runPipeline(input, config, dryRun) {
|
|
1780
|
+
debug(`Processing ${input.packages.length} package(s)`);
|
|
1781
|
+
let contexts = input.packages.map(createTemplateContext);
|
|
1782
|
+
if (config.llm && !process.env.CHANGELOG_NO_LLM) {
|
|
1783
|
+
info("Processing with LLM enhancement");
|
|
1784
|
+
contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
|
|
1785
|
+
}
|
|
1786
|
+
const files = [];
|
|
1787
|
+
const fmtOpts = {
|
|
1788
|
+
includePackageName: contexts.length > 1 || contexts.some((c) => c.packageName.includes("/"))
|
|
1789
|
+
};
|
|
1790
|
+
for (const output of config.output) {
|
|
1791
|
+
const file = output.file ?? (output.format === "json" ? "changelog.json" : "CHANGELOG.md");
|
|
1792
|
+
const isChangelog = /changelog/i.test(file);
|
|
1793
|
+
const outputKind = isChangelog ? "changelog" : "release notes";
|
|
1794
|
+
info(`Generating ${outputKind} \u2192 ${file}`);
|
|
1795
|
+
switch (output.format) {
|
|
1796
|
+
case "markdown": {
|
|
1797
|
+
const file2 = output.file ?? "CHANGELOG.md";
|
|
1798
|
+
try {
|
|
1799
|
+
const effectiveTemplateConfig = output.templates ?? config.templates;
|
|
1800
|
+
if (effectiveTemplateConfig?.path || output.options?.template) {
|
|
1801
|
+
const configWithTemplate = { ...config, templates: effectiveTemplateConfig };
|
|
1802
|
+
await generateWithTemplate(contexts, configWithTemplate, file2, dryRun);
|
|
1803
|
+
} else {
|
|
1804
|
+
writeMarkdown(file2, contexts, config, dryRun, fmtOpts);
|
|
1805
|
+
}
|
|
1806
|
+
if (!dryRun) files.push(file2);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
warn(`Failed to write ${file2}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1809
|
+
}
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
case "json": {
|
|
1813
|
+
const file2 = output.file ?? "changelog.json";
|
|
1814
|
+
try {
|
|
1815
|
+
writeJson(file2, contexts, dryRun);
|
|
1816
|
+
if (!dryRun) files.push(file2);
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
warn(`Failed to write ${file2}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1819
|
+
}
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
case "github-release": {
|
|
1823
|
+
if (dryRun) {
|
|
1824
|
+
info("[DRY RUN] Would create GitHub release");
|
|
1825
|
+
break;
|
|
1826
|
+
}
|
|
1827
|
+
const firstContext = contexts[0];
|
|
1828
|
+
if (!firstContext) {
|
|
1829
|
+
warn("No context available for GitHub release");
|
|
1830
|
+
break;
|
|
1831
|
+
}
|
|
1832
|
+
const repoUrl = firstContext.repoUrl;
|
|
1833
|
+
if (!repoUrl) {
|
|
1834
|
+
warn("No repo URL available, cannot create GitHub release");
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
const parsed = parseRepoUrl(repoUrl);
|
|
1838
|
+
if (!parsed) {
|
|
1839
|
+
warn(`Could not parse repo URL: ${repoUrl}`);
|
|
1840
|
+
break;
|
|
1841
|
+
}
|
|
1842
|
+
await createGitHubRelease(firstContext, {
|
|
1843
|
+
owner: parsed.owner,
|
|
1844
|
+
repo: parsed.repo,
|
|
1845
|
+
draft: output.options?.draft,
|
|
1846
|
+
prerelease: output.options?.prerelease
|
|
1847
|
+
});
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (config.monorepo?.mode) {
|
|
1853
|
+
const { detectMonorepo, writeMonorepoChangelogs } = await import("./aggregator-XJ2EILO3.js");
|
|
1854
|
+
const cwd = process.cwd();
|
|
1855
|
+
const detected = detectMonorepo(cwd);
|
|
1856
|
+
if (detected.isMonorepo) {
|
|
1857
|
+
const monoFiles = writeMonorepoChangelogs(
|
|
1858
|
+
contexts,
|
|
1859
|
+
{
|
|
1860
|
+
rootPath: config.monorepo.rootPath ?? cwd,
|
|
1861
|
+
packagesPath: config.monorepo.packagesPath ?? detected.packagesPath,
|
|
1862
|
+
mode: config.monorepo.mode
|
|
1863
|
+
},
|
|
1864
|
+
config,
|
|
1865
|
+
dryRun
|
|
1866
|
+
);
|
|
1867
|
+
files.push(...monoFiles);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const packageNotes = {};
|
|
1871
|
+
for (const ctx of contexts) {
|
|
1872
|
+
packageNotes[ctx.packageName] = formatVersion(ctx);
|
|
1873
|
+
}
|
|
1874
|
+
return { packageNotes, files };
|
|
1875
|
+
}
|
|
1876
|
+
async function processInput(inputJson, config, dryRun) {
|
|
1877
|
+
const input = parseVersionOutput(inputJson);
|
|
1878
|
+
return runPipeline(input, config, dryRun);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
export {
|
|
1882
|
+
loadAuth,
|
|
1883
|
+
saveAuth,
|
|
1884
|
+
loadConfig2 as loadConfig,
|
|
1885
|
+
getDefaultConfig,
|
|
1886
|
+
NotesError,
|
|
1887
|
+
InputParseError,
|
|
1888
|
+
TemplateError,
|
|
1889
|
+
LLMError,
|
|
1890
|
+
GitHubError,
|
|
1891
|
+
ConfigError2 as ConfigError,
|
|
1892
|
+
getExitCode,
|
|
1893
|
+
parseVersionOutput,
|
|
1894
|
+
parseVersionOutputFile,
|
|
1895
|
+
parseVersionOutputStdin,
|
|
1896
|
+
renderJson,
|
|
1897
|
+
writeJson,
|
|
1898
|
+
createTemplateContext,
|
|
1899
|
+
runPipeline,
|
|
1900
|
+
processInput
|
|
1901
|
+
};
|