@releasekit/notes 0.2.0-next.9 → 0.2.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/cli.cjs DELETED
@@ -1,1750 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
- // If the importer is in node compatibility mode or this is not an ESM
19
- // file that has been converted to a CommonJS file using a Babel-
20
- // compatible transform (i.e. "__esModule" has not been set), then set
21
- // "default" to the CommonJS "module.exports" for node compatibility.
22
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
- mod
24
- ));
25
-
26
- // src/cli.ts
27
- var fs10 = __toESM(require("fs"), 1);
28
- var readline = __toESM(require("readline"), 1);
29
- var import_core10 = require("@releasekit/core");
30
- var import_commander = require("commander");
31
-
32
- // src/core/config.ts
33
- var import_config = require("@releasekit/config");
34
- function loadConfig(projectDir = process.cwd(), configFile) {
35
- const options = { cwd: projectDir, configPath: configFile };
36
- return (0, import_config.loadNotesConfig)(options) ?? getDefaultConfig();
37
- }
38
- function getDefaultConfig() {
39
- return {
40
- output: [{ format: "markdown", file: "CHANGELOG.md" }],
41
- updateStrategy: "prepend"
42
- };
43
- }
44
-
45
- // src/core/pipeline.ts
46
- var fs8 = __toESM(require("fs"), 1);
47
- var path6 = __toESM(require("path"), 1);
48
- var import_core8 = require("@releasekit/core");
49
-
50
- // src/input/package-versioner.ts
51
- var fs = __toESM(require("fs"), 1);
52
-
53
- // src/errors/index.ts
54
- var import_core = require("@releasekit/core");
55
- var import_core2 = require("@releasekit/core");
56
- var NotesError = class extends import_core.ReleaseKitError {
57
- };
58
- var InputParseError = class extends NotesError {
59
- code = "INPUT_PARSE_ERROR";
60
- suggestions = [
61
- "Ensure input is valid JSON",
62
- "Check that input matches expected schema",
63
- "Use --input-source to specify format"
64
- ];
65
- };
66
- var TemplateError = class extends NotesError {
67
- code = "TEMPLATE_ERROR";
68
- suggestions = [
69
- "Check template syntax",
70
- "Ensure all required files exist (document, version, entry)",
71
- "Verify template engine matches file extension"
72
- ];
73
- };
74
- var LLMError = class extends NotesError {
75
- code = "LLM_ERROR";
76
- suggestions = [
77
- "Check API key is configured",
78
- "Verify model name is correct",
79
- "Check network connectivity",
80
- "Try with --no-llm to skip LLM processing"
81
- ];
82
- };
83
- var GitHubError = class extends NotesError {
84
- code = "GITHUB_ERROR";
85
- suggestions = [
86
- "Ensure GITHUB_TOKEN is set",
87
- "Check token has repo scope",
88
- "Verify repository exists and is accessible"
89
- ];
90
- };
91
- function getExitCode(error2) {
92
- switch (error2.code) {
93
- case "CONFIG_ERROR":
94
- return import_core.EXIT_CODES.CONFIG_ERROR;
95
- case "INPUT_PARSE_ERROR":
96
- return import_core.EXIT_CODES.INPUT_ERROR;
97
- case "TEMPLATE_ERROR":
98
- return import_core.EXIT_CODES.TEMPLATE_ERROR;
99
- case "LLM_ERROR":
100
- return import_core.EXIT_CODES.LLM_ERROR;
101
- case "GITHUB_ERROR":
102
- return import_core.EXIT_CODES.GITHUB_ERROR;
103
- default:
104
- return import_core.EXIT_CODES.GENERAL_ERROR;
105
- }
106
- }
107
-
108
- // src/input/package-versioner.ts
109
- function normalizeEntryType(type) {
110
- const typeMap = {
111
- added: "added",
112
- feat: "added",
113
- feature: "added",
114
- changed: "changed",
115
- update: "changed",
116
- refactor: "changed",
117
- deprecated: "deprecated",
118
- removed: "removed",
119
- fixed: "fixed",
120
- fix: "fixed",
121
- security: "security",
122
- sec: "security"
123
- };
124
- return typeMap[type.toLowerCase()] ?? "changed";
125
- }
126
- function parsePackageVersioner(json) {
127
- let data;
128
- try {
129
- data = JSON.parse(json);
130
- } catch (error2) {
131
- throw new InputParseError(`Invalid JSON input: ${error2 instanceof Error ? error2.message : String(error2)}`);
132
- }
133
- if (!data.changelogs || !Array.isArray(data.changelogs)) {
134
- throw new InputParseError('Input must contain a "changelogs" array');
135
- }
136
- const packages = data.changelogs.map((changelog) => ({
137
- packageName: changelog.packageName,
138
- version: changelog.version,
139
- previousVersion: changelog.previousVersion,
140
- revisionRange: changelog.revisionRange,
141
- repoUrl: changelog.repoUrl,
142
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
143
- entries: changelog.entries.map((entry) => ({
144
- type: normalizeEntryType(entry.type),
145
- description: entry.description,
146
- issueIds: entry.issueIds,
147
- scope: entry.scope,
148
- originalType: entry.originalType,
149
- breaking: entry.breaking ?? entry.originalType?.includes("!") ?? false
150
- }))
151
- }));
152
- const repoUrl = packages[0]?.repoUrl ?? null;
153
- return {
154
- source: "package-versioner",
155
- packages,
156
- metadata: {
157
- repoUrl: repoUrl ?? void 0
158
- }
159
- };
160
- }
161
-
162
- // src/llm/defaults.ts
163
- var LLM_DEFAULTS = {
164
- timeout: 6e4,
165
- maxTokens: 2e3,
166
- temperature: 0.7,
167
- concurrency: 5,
168
- retry: {
169
- maxAttempts: 3,
170
- initialDelay: 1e3,
171
- maxDelay: 3e4,
172
- backoffFactor: 2
173
- },
174
- models: {
175
- openai: "gpt-4o-mini",
176
- "openai-compatible": "gpt-4o-mini",
177
- anthropic: "claude-3-5-haiku-latest",
178
- ollama: "llama3.2"
179
- }
180
- };
181
-
182
- // src/llm/anthropic.ts
183
- var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
184
-
185
- // src/llm/base.ts
186
- var BaseLLMProvider = class {
187
- getTimeout(options) {
188
- return options?.timeout ?? LLM_DEFAULTS.timeout;
189
- }
190
- getMaxTokens(options) {
191
- return options?.maxTokens ?? LLM_DEFAULTS.maxTokens;
192
- }
193
- getTemperature(options) {
194
- return options?.temperature ?? LLM_DEFAULTS.temperature;
195
- }
196
- };
197
-
198
- // src/llm/anthropic.ts
199
- var AnthropicProvider = class extends BaseLLMProvider {
200
- name = "anthropic";
201
- client;
202
- model;
203
- constructor(config = {}) {
204
- super();
205
- const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;
206
- if (!apiKey) {
207
- throw new LLMError("Anthropic API key not configured. Set ANTHROPIC_API_KEY or use --llm-api-key");
208
- }
209
- this.client = new import_sdk.default({ apiKey });
210
- this.model = config.model ?? LLM_DEFAULTS.models.anthropic;
211
- }
212
- async complete(prompt, options) {
213
- try {
214
- const response = await this.client.messages.create({
215
- model: this.model,
216
- max_tokens: this.getMaxTokens(options),
217
- messages: [{ role: "user", content: prompt }]
218
- });
219
- const firstBlock = response.content[0];
220
- if (!firstBlock || firstBlock.type !== "text") {
221
- throw new LLMError("Unexpected response format from Anthropic");
222
- }
223
- return firstBlock.text;
224
- } catch (error2) {
225
- if (error2 instanceof LLMError) throw error2;
226
- throw new LLMError(`Anthropic API error: ${error2 instanceof Error ? error2.message : String(error2)}`);
227
- }
228
- }
229
- };
230
-
231
- // src/llm/ollama.ts
232
- var OllamaProvider = class extends BaseLLMProvider {
233
- name = "ollama";
234
- baseURL;
235
- model;
236
- apiKey;
237
- constructor(config = {}) {
238
- super();
239
- this.baseURL = config.baseURL ?? process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
240
- this.model = config.model ?? LLM_DEFAULTS.models.ollama;
241
- this.apiKey = config.apiKey ?? process.env.OLLAMA_API_KEY;
242
- }
243
- async complete(prompt, options) {
244
- const requestBody = {
245
- model: this.model,
246
- messages: [{ role: "user", content: prompt }],
247
- stream: false,
248
- options: {
249
- num_predict: this.getMaxTokens(options),
250
- temperature: this.getTemperature(options)
251
- }
252
- };
253
- try {
254
- const headers = {
255
- "Content-Type": "application/json"
256
- };
257
- if (this.apiKey) {
258
- headers["Authorization"] = `Bearer ${this.apiKey}`;
259
- }
260
- const baseUrl = this.baseURL.endsWith("/api") ? this.baseURL.slice(0, -4) : this.baseURL;
261
- const response = await fetch(`${baseUrl}/api/chat`, {
262
- method: "POST",
263
- headers,
264
- body: JSON.stringify(requestBody)
265
- });
266
- if (!response.ok) {
267
- const text = await response.text();
268
- throw new LLMError(`Ollama request failed: ${response.status} ${text}`);
269
- }
270
- const data = await response.json();
271
- if (!data.message?.content) {
272
- throw new LLMError("Empty response from Ollama");
273
- }
274
- return data.message.content;
275
- } catch (error2) {
276
- if (error2 instanceof LLMError) throw error2;
277
- throw new LLMError(`Ollama error: ${error2 instanceof Error ? error2.message : String(error2)}`);
278
- }
279
- }
280
- };
281
-
282
- // src/llm/openai.ts
283
- var import_openai = __toESM(require("openai"), 1);
284
- var OpenAIProvider = class extends BaseLLMProvider {
285
- name = "openai";
286
- client;
287
- model;
288
- constructor(config = {}) {
289
- super();
290
- const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
291
- if (!apiKey) {
292
- throw new LLMError("OpenAI API key not configured. Set OPENAI_API_KEY or use --llm-api-key");
293
- }
294
- this.client = new import_openai.default({
295
- apiKey,
296
- baseURL: config.baseURL
297
- });
298
- this.model = config.model ?? LLM_DEFAULTS.models.openai;
299
- }
300
- async complete(prompt, options) {
301
- try {
302
- const response = await this.client.chat.completions.create({
303
- model: this.model,
304
- messages: [{ role: "user", content: prompt }],
305
- max_tokens: this.getMaxTokens(options),
306
- temperature: this.getTemperature(options)
307
- });
308
- const content = response.choices[0]?.message?.content;
309
- if (!content) {
310
- throw new LLMError("Empty response from OpenAI");
311
- }
312
- return content;
313
- } catch (error2) {
314
- if (error2 instanceof LLMError) throw error2;
315
- throw new LLMError(`OpenAI API error: ${error2 instanceof Error ? error2.message : String(error2)}`);
316
- }
317
- }
318
- };
319
-
320
- // src/llm/openai-compatible.ts
321
- var import_openai2 = __toESM(require("openai"), 1);
322
- var OpenAICompatibleProvider = class extends BaseLLMProvider {
323
- name = "openai-compatible";
324
- client;
325
- model;
326
- constructor(config) {
327
- super();
328
- const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY ?? "dummy";
329
- this.client = new import_openai2.default({
330
- apiKey,
331
- baseURL: config.baseURL
332
- });
333
- this.model = config.model ?? LLM_DEFAULTS.models["openai-compatible"];
334
- }
335
- async complete(prompt, options) {
336
- try {
337
- const response = await this.client.chat.completions.create({
338
- model: this.model,
339
- messages: [{ role: "user", content: prompt }],
340
- max_tokens: this.getMaxTokens(options),
341
- temperature: this.getTemperature(options)
342
- });
343
- const content = response.choices[0]?.message?.content;
344
- if (!content) {
345
- throw new LLMError("Empty response from LLM");
346
- }
347
- return content;
348
- } catch (error2) {
349
- if (error2 instanceof LLMError) throw error2;
350
- throw new LLMError(`LLM API error: ${error2 instanceof Error ? error2.message : String(error2)}`);
351
- }
352
- }
353
- };
354
-
355
- // src/llm/tasks/categorize.ts
356
- var import_core3 = require("@releasekit/core");
357
- var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
358
-
359
- Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
360
-
361
- Output a JSON object where keys are category names and values are arrays of entry indices (0-based).
362
-
363
- Entries:
364
- {{entries}}
365
-
366
- Output only valid JSON, nothing else:`;
367
- function buildCustomCategorizePrompt(categories) {
368
- const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
369
- const developerCategory = categories.find((c) => c.name === "Developer");
370
- let scopeInstructions = "";
371
- if (developerCategory) {
372
- const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
373
- if (scopeMatch?.[1]) {
374
- const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
375
- if (scopes.length > 0) {
376
- scopeInstructions = `
377
-
378
- For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
379
- `;
380
- }
381
- }
382
- }
383
- return `You are categorizing changelog entries for a software release.
384
-
385
- Given the following entries, group them into the specified categories. Only use the categories listed below in this exact order:
386
-
387
- Categories:
388
- ${categoryList}${scopeInstructions}
389
- Output a JSON object with two fields:
390
- - "categories": an object where keys are category names and values are arrays of entry indices (0-based)
391
- - "scopes": an object where keys are entry indices (as strings) and values are scope labels
392
-
393
- Entries:
394
- {{entries}}
395
-
396
- Output only valid JSON, nothing else:`;
397
- }
398
- async function categorizeEntries(provider, entries, context) {
399
- if (entries.length === 0) {
400
- return [];
401
- }
402
- const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
403
- const hasCustomCategories = context.categories && context.categories.length > 0;
404
- const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
405
- const prompt = promptTemplate.replace("{{entries}}", entriesText);
406
- try {
407
- const response = await provider.complete(prompt);
408
- const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
409
- const parsed = JSON.parse(cleaned);
410
- const result = [];
411
- if (hasCustomCategories && parsed.categories) {
412
- const categoryMap = parsed.categories;
413
- const scopeMap = parsed.scopes || {};
414
- for (const [indexStr, scope] of Object.entries(scopeMap)) {
415
- const idx = Number.parseInt(indexStr, 10);
416
- if (entries[idx] && scope) {
417
- entries[idx] = { ...entries[idx], scope };
418
- }
419
- }
420
- for (const [category, rawIndices] of Object.entries(categoryMap)) {
421
- const indices = Array.isArray(rawIndices) ? rawIndices : [];
422
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
423
- if (categoryEntries.length > 0) {
424
- result.push({ category, entries: categoryEntries });
425
- }
426
- }
427
- } else {
428
- const categoryMap = parsed;
429
- for (const [category, rawIndices] of Object.entries(categoryMap)) {
430
- const indices = Array.isArray(rawIndices) ? rawIndices : [];
431
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
432
- if (categoryEntries.length > 0) {
433
- result.push({ category, entries: categoryEntries });
434
- }
435
- }
436
- }
437
- return result;
438
- } catch (error2) {
439
- (0, import_core3.warn)(
440
- `LLM categorization failed, falling back to General: ${error2 instanceof Error ? error2.message : String(error2)}`
441
- );
442
- return [{ category: "General", entries }];
443
- }
444
- }
445
-
446
- // src/llm/tasks/enhance.ts
447
- var ENHANCE_PROMPT = `You are improving changelog entries for a software project.
448
- Given a technical commit message, rewrite it as a clear, user-friendly changelog entry.
449
-
450
- Rules:
451
- - Be concise (1-2 sentences max)
452
- - Focus on user impact, not implementation details
453
- - Don't use technical jargon unless necessary
454
- - Preserve the scope if mentioned (e.g., "core:", "api:")
455
- {{style}}
456
-
457
- Original entry:
458
- Type: {{type}}
459
- {{#if scope}}Scope: {{scope}}{{/if}}
460
- Description: {{description}}
461
-
462
- Rewritten description (only output the new description, nothing else):`;
463
- async function enhanceEntry(provider, entry, _context) {
464
- const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
465
- const prompt = ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
466
- const response = await provider.complete(prompt);
467
- return response.trim();
468
- }
469
- async function enhanceEntries(provider, entries, context, concurrency = LLM_DEFAULTS.concurrency) {
470
- const results = [];
471
- for (let i = 0; i < entries.length; i += concurrency) {
472
- const batch = entries.slice(i, i + concurrency);
473
- const batchResults = await Promise.all(
474
- batch.map(async (entry) => {
475
- try {
476
- const newDescription = await enhanceEntry(provider, entry, context);
477
- return { ...entry, description: newDescription };
478
- } catch {
479
- return entry;
480
- }
481
- })
482
- );
483
- results.push(...batchResults);
484
- }
485
- return results;
486
- }
487
-
488
- // src/llm/tasks/enhance-and-categorize.ts
489
- var import_core4 = require("@releasekit/core");
490
-
491
- // src/utils/retry.ts
492
- function sleep(ms) {
493
- return new Promise((resolve2) => setTimeout(resolve2, ms));
494
- }
495
- async function withRetry(fn, options = {}) {
496
- const maxAttempts = options.maxAttempts ?? 3;
497
- const initialDelay = options.initialDelay ?? 1e3;
498
- const maxDelay = options.maxDelay ?? 3e4;
499
- const backoffFactor = options.backoffFactor ?? 2;
500
- let lastError;
501
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
502
- try {
503
- return await fn();
504
- } catch (error2) {
505
- lastError = error2;
506
- if (attempt < maxAttempts - 1) {
507
- const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
508
- const jitter = base * 0.2 * (Math.random() * 2 - 1);
509
- await sleep(Math.max(0, base + jitter));
510
- }
511
- }
512
- }
513
- throw lastError;
514
- }
515
-
516
- // src/llm/tasks/enhance-and-categorize.ts
517
- function buildPrompt(entries, categories, style) {
518
- const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
519
- const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
520
- const categorySection = categories ? `Categories (use ONLY these):
521
- ${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
522
- return `You are generating release notes for a software project. Given the following changelog entries, do two things:
523
-
524
- 1. **Rewrite** each entry as a clear, user-friendly description
525
- 2. **Categorize** each entry into the appropriate category
526
-
527
- Style guidelines:
528
- - ${styleText}
529
- - Be concise (1 short sentence per entry)
530
- - Focus on what changed, not implementation details
531
-
532
- ${categorySection}
533
-
534
- ${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
535
-
536
- Entries:
537
- ${entriesText}
538
-
539
- Output a JSON object with:
540
- - "entries": array of objects, one per input entry (same order), each with: { "description": "rewritten text", "category": "CategoryName", "scope": "optional subcategory label or null" }
541
-
542
- Output only valid JSON, nothing else:`;
543
- }
544
- async function enhanceAndCategorize(provider, entries, context) {
545
- if (entries.length === 0) {
546
- return { enhancedEntries: [], categories: [] };
547
- }
548
- const retryOpts = LLM_DEFAULTS.retry;
549
- try {
550
- return await withRetry(async () => {
551
- const prompt = buildPrompt(entries, context.categories, context.style);
552
- const response = await provider.complete(prompt);
553
- const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
554
- const parsed = JSON.parse(cleaned);
555
- if (!Array.isArray(parsed.entries)) {
556
- throw new Error('Response missing "entries" array');
557
- }
558
- const enhancedEntries = entries.map((original, i) => {
559
- const result = parsed.entries[i];
560
- if (!result) return original;
561
- return {
562
- ...original,
563
- description: result.description || original.description,
564
- scope: result.scope || original.scope
565
- };
566
- });
567
- const categoryMap = /* @__PURE__ */ new Map();
568
- for (let i = 0; i < parsed.entries.length; i++) {
569
- const result = parsed.entries[i];
570
- const category = result?.category || "General";
571
- const entry = enhancedEntries[i];
572
- if (!entry) continue;
573
- if (!categoryMap.has(category)) {
574
- categoryMap.set(category, []);
575
- }
576
- categoryMap.get(category).push(entry);
577
- }
578
- const categories = [];
579
- for (const [category, catEntries] of categoryMap) {
580
- categories.push({ category, entries: catEntries });
581
- }
582
- return { enhancedEntries, categories };
583
- }, retryOpts);
584
- } catch (error2) {
585
- (0, import_core4.warn)(
586
- `Combined enhance+categorize failed after ${retryOpts.maxAttempts} attempts: ${error2 instanceof Error ? error2.message : String(error2)}`
587
- );
588
- return {
589
- enhancedEntries: entries,
590
- categories: [{ category: "General", entries }]
591
- };
592
- }
593
- }
594
-
595
- // src/llm/tasks/release-notes.ts
596
- var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
597
-
598
- Create engaging, user-friendly release notes for the following changes.
599
-
600
- Rules:
601
- - Start with a brief introduction (1-2 sentences)
602
- - Group related changes into sections
603
- - Use friendly, approachable language
604
- - Highlight breaking changes prominently
605
- - End with a brief conclusion or call to action
606
- - Use markdown formatting
607
-
608
- Version: {{version}}
609
- {{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}
610
- Date: {{date}}
611
-
612
- Changes:
613
- {{entries}}
614
-
615
- Release notes (output only the markdown content):`;
616
- async function generateReleaseNotes(provider, entries, context) {
617
- if (entries.length === 0) {
618
- return `## Release ${context.version ?? "v1.0.0"}
619
-
620
- No notable changes in this release.`;
621
- }
622
- const entriesText = entries.map((e) => {
623
- let line = `- [${e.type}]`;
624
- if (e.scope) line += ` (${e.scope})`;
625
- line += `: ${e.description}`;
626
- if (e.breaking) line += " **BREAKING**";
627
- return line;
628
- }).join("\n");
629
- const prompt = RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
630
- "{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}",
631
- context.previousVersion ? `Previous version: ${context.previousVersion}` : ""
632
- ).replace("{{date}}", context.date ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "").replace("{{entries}}", entriesText);
633
- const response = await provider.complete(prompt);
634
- return response.trim();
635
- }
636
-
637
- // src/llm/tasks/summarize.ts
638
- var SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
639
-
640
- Given the following changelog entries, create a brief summary (2-3 sentences) that captures the main themes of this release.
641
-
642
- Entries:
643
- {{entries}}
644
-
645
- Summary (only output the summary, nothing else):`;
646
- async function summarizeEntries(provider, entries, _context) {
647
- if (entries.length === 0) {
648
- return "";
649
- }
650
- const entriesText = entries.map((e) => `- [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
651
- const prompt = SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
652
- const response = await provider.complete(prompt);
653
- return response.trim();
654
- }
655
-
656
- // src/llm/index.ts
657
- function createProvider(config) {
658
- const authKeys = (0, import_config.loadAuth)();
659
- const apiKey = config.apiKey ?? authKeys[config.provider];
660
- switch (config.provider) {
661
- case "openai":
662
- return new OpenAIProvider({
663
- apiKey,
664
- baseURL: config.baseURL,
665
- model: config.model
666
- });
667
- case "anthropic":
668
- return new AnthropicProvider({
669
- apiKey,
670
- model: config.model
671
- });
672
- case "ollama":
673
- return new OllamaProvider({
674
- apiKey,
675
- baseURL: config.baseURL,
676
- model: config.model
677
- });
678
- case "openai-compatible": {
679
- if (!config.baseURL) {
680
- throw new LLMError("openai-compatible provider requires baseURL");
681
- }
682
- return new OpenAICompatibleProvider({
683
- apiKey,
684
- baseURL: config.baseURL,
685
- model: config.model
686
- });
687
- }
688
- default:
689
- throw new LLMError(`Unknown LLM provider: ${config.provider}`);
690
- }
691
- }
692
-
693
- // src/output/github-release.ts
694
- var import_rest = require("@octokit/rest");
695
- var import_core6 = require("@releasekit/core");
696
-
697
- // src/output/markdown.ts
698
- var fs2 = __toESM(require("fs"), 1);
699
- var path = __toESM(require("path"), 1);
700
- var import_core5 = require("@releasekit/core");
701
- var TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
702
- var TYPE_LABELS = {
703
- added: "Added",
704
- changed: "Changed",
705
- deprecated: "Deprecated",
706
- removed: "Removed",
707
- fixed: "Fixed",
708
- security: "Security"
709
- };
710
- function groupEntriesByType(entries) {
711
- const grouped = /* @__PURE__ */ new Map();
712
- for (const type of TYPE_ORDER) {
713
- grouped.set(type, []);
714
- }
715
- for (const entry of entries) {
716
- const existing = grouped.get(entry.type) ?? [];
717
- existing.push(entry);
718
- grouped.set(entry.type, existing);
719
- }
720
- return grouped;
721
- }
722
- function formatEntry(entry) {
723
- let line;
724
- if (entry.breaking && entry.scope) {
725
- line = `- **BREAKING** **${entry.scope}**: ${entry.description}`;
726
- } else if (entry.breaking) {
727
- line = `- **BREAKING** ${entry.description}`;
728
- } else if (entry.scope) {
729
- line = `- **${entry.scope}**: ${entry.description}`;
730
- } else {
731
- line = `- ${entry.description}`;
732
- }
733
- if (entry.issueIds && entry.issueIds.length > 0) {
734
- line += ` (${entry.issueIds.join(", ")})`;
735
- }
736
- return line;
737
- }
738
- function formatVersion(context) {
739
- const lines = [];
740
- const versionHeader = context.previousVersion ? `## [${context.version}]` : `## ${context.version}`;
741
- lines.push(`${versionHeader} - ${context.date}`);
742
- lines.push("");
743
- if (context.compareUrl) {
744
- lines.push(`[Full Changelog](${context.compareUrl})`);
745
- lines.push("");
746
- }
747
- if (context.enhanced?.summary) {
748
- lines.push(context.enhanced.summary);
749
- lines.push("");
750
- }
751
- const grouped = groupEntriesByType(context.entries);
752
- for (const [type, entries] of grouped) {
753
- if (entries.length === 0) continue;
754
- lines.push(`### ${TYPE_LABELS[type]}`);
755
- for (const entry of entries) {
756
- lines.push(formatEntry(entry));
757
- }
758
- lines.push("");
759
- }
760
- return lines.join("\n");
761
- }
762
- function formatHeader() {
763
- return `# Changelog
764
-
765
- All notable changes to this project will be documented in this file.
766
-
767
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
768
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
769
-
770
- `;
771
- }
772
- function renderMarkdown(contexts) {
773
- const sections = [formatHeader()];
774
- for (const context of contexts) {
775
- sections.push(formatVersion(context));
776
- }
777
- return sections.join("\n");
778
- }
779
- function prependVersion(existingPath, context) {
780
- let existing = "";
781
- if (fs2.existsSync(existingPath)) {
782
- existing = fs2.readFileSync(existingPath, "utf-8");
783
- const headerEnd = existing.indexOf("\n## ");
784
- if (headerEnd >= 0) {
785
- const header = existing.slice(0, headerEnd);
786
- const body = existing.slice(headerEnd + 1);
787
- const newVersion = formatVersion(context);
788
- return `${header}
789
-
790
- ${newVersion}
791
- ${body}`;
792
- }
793
- }
794
- return renderMarkdown([context]);
795
- }
796
- function writeMarkdown(outputPath, contexts, config, dryRun) {
797
- const content = renderMarkdown(contexts);
798
- if (dryRun) {
799
- (0, import_core5.info)("--- Changelog Preview ---");
800
- console.log(content);
801
- (0, import_core5.info)("--- End Preview ---");
802
- return;
803
- }
804
- const dir = path.dirname(outputPath);
805
- if (!fs2.existsSync(dir)) {
806
- fs2.mkdirSync(dir, { recursive: true });
807
- }
808
- if (outputPath === "-") {
809
- process.stdout.write(content);
810
- return;
811
- }
812
- if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
813
- const firstContext = contexts[0];
814
- if (firstContext) {
815
- const updated = prependVersion(outputPath, firstContext);
816
- fs2.writeFileSync(outputPath, updated, "utf-8");
817
- }
818
- } else {
819
- fs2.writeFileSync(outputPath, content, "utf-8");
820
- }
821
- (0, import_core5.success)(`Changelog written to ${outputPath}`);
822
- }
823
-
824
- // src/output/github-release.ts
825
- var GitHubClient = class {
826
- octokit;
827
- owner;
828
- repo;
829
- constructor(options) {
830
- const token = options.token ?? process.env.GITHUB_TOKEN;
831
- if (!token) {
832
- throw new GitHubError("GITHUB_TOKEN not set. Set it as an environment variable.");
833
- }
834
- this.octokit = new import_rest.Octokit({ auth: token });
835
- this.owner = options.owner;
836
- this.repo = options.repo;
837
- }
838
- async createRelease(context, options = {}) {
839
- const tagName = `v${context.version}`;
840
- let body;
841
- if (context.enhanced?.releaseNotes) {
842
- body = context.enhanced.releaseNotes;
843
- } else {
844
- body = renderMarkdown([context]);
845
- }
846
- (0, import_core6.info)(`Creating GitHub release for ${tagName}`);
847
- try {
848
- const response = await this.octokit.repos.createRelease({
849
- owner: this.owner,
850
- repo: this.repo,
851
- tag_name: tagName,
852
- name: tagName,
853
- body,
854
- draft: options.draft ?? false,
855
- prerelease: options.prerelease ?? false,
856
- generate_release_notes: options.generateNotes ?? false
857
- });
858
- (0, import_core6.success)(`Release created: ${response.data.html_url}`);
859
- return {
860
- id: response.data.id,
861
- htmlUrl: response.data.html_url,
862
- tagName
863
- };
864
- } catch (error2) {
865
- throw new GitHubError(`Failed to create release: ${error2 instanceof Error ? error2.message : String(error2)}`);
866
- }
867
- }
868
- async updateRelease(releaseId, context, options = {}) {
869
- const tagName = `v${context.version}`;
870
- let body;
871
- if (context.enhanced?.releaseNotes) {
872
- body = context.enhanced.releaseNotes;
873
- } else {
874
- body = renderMarkdown([context]);
875
- }
876
- (0, import_core6.info)(`Updating GitHub release ${releaseId}`);
877
- try {
878
- const response = await this.octokit.repos.updateRelease({
879
- owner: this.owner,
880
- repo: this.repo,
881
- release_id: releaseId,
882
- tag_name: tagName,
883
- name: tagName,
884
- body,
885
- draft: options.draft ?? false,
886
- prerelease: options.prerelease ?? false
887
- });
888
- (0, import_core6.success)(`Release updated: ${response.data.html_url}`);
889
- return {
890
- id: response.data.id,
891
- htmlUrl: response.data.html_url,
892
- tagName
893
- };
894
- } catch (error2) {
895
- throw new GitHubError(`Failed to update release: ${error2 instanceof Error ? error2.message : String(error2)}`);
896
- }
897
- }
898
- async getReleaseByTag(tag) {
899
- try {
900
- const response = await this.octokit.repos.getReleaseByTag({
901
- owner: this.owner,
902
- repo: this.repo,
903
- tag
904
- });
905
- return {
906
- id: response.data.id,
907
- htmlUrl: response.data.html_url,
908
- tagName: response.data.tag_name
909
- };
910
- } catch {
911
- return null;
912
- }
913
- }
914
- };
915
- function parseRepoUrl(repoUrl) {
916
- const patterns = [
917
- /^https:\/\/github\.com\/([^/]+)\/([^/]+)/,
918
- /^git@github\.com:([^/]+)\/([^/]+)/,
919
- /^github\.com\/([^/]+)\/([^/]+)/
920
- ];
921
- for (const pattern of patterns) {
922
- const match = repoUrl.match(pattern);
923
- if (match?.[1] && match[2]) {
924
- return {
925
- owner: match[1],
926
- repo: match[2].replace(/\.git$/, "")
927
- };
928
- }
929
- }
930
- return null;
931
- }
932
- async function createGitHubRelease(context, options) {
933
- const client = new GitHubClient(options);
934
- return client.createRelease(context, options);
935
- }
936
-
937
- // src/output/json.ts
938
- var fs3 = __toESM(require("fs"), 1);
939
- var path2 = __toESM(require("path"), 1);
940
- var import_core7 = require("@releasekit/core");
941
- function renderJson(contexts) {
942
- return JSON.stringify(
943
- {
944
- versions: contexts.map((ctx) => ({
945
- packageName: ctx.packageName,
946
- version: ctx.version,
947
- previousVersion: ctx.previousVersion,
948
- date: ctx.date,
949
- entries: ctx.entries,
950
- compareUrl: ctx.compareUrl
951
- }))
952
- },
953
- null,
954
- 2
955
- );
956
- }
957
- function writeJson(outputPath, contexts, dryRun) {
958
- const content = renderJson(contexts);
959
- if (dryRun) {
960
- (0, import_core7.info)("--- JSON Output Preview ---");
961
- console.log(content);
962
- (0, import_core7.info)("--- End Preview ---");
963
- return;
964
- }
965
- const dir = path2.dirname(outputPath);
966
- if (!fs3.existsSync(dir)) {
967
- fs3.mkdirSync(dir, { recursive: true });
968
- }
969
- fs3.writeFileSync(outputPath, content, "utf-8");
970
- (0, import_core7.success)(`JSON output written to ${outputPath}`);
971
- }
972
-
973
- // src/templates/ejs.ts
974
- var fs4 = __toESM(require("fs"), 1);
975
- var import_ejs = __toESM(require("ejs"), 1);
976
- function renderEjs(template, context) {
977
- try {
978
- return import_ejs.default.render(template, context);
979
- } catch (error2) {
980
- throw new TemplateError(`EJS render error: ${error2 instanceof Error ? error2.message : String(error2)}`);
981
- }
982
- }
983
- function renderEjsFile(filePath, context) {
984
- if (!fs4.existsSync(filePath)) {
985
- throw new TemplateError(`Template file not found: ${filePath}`);
986
- }
987
- const template = fs4.readFileSync(filePath, "utf-8");
988
- return renderEjs(template, context);
989
- }
990
-
991
- // src/templates/handlebars.ts
992
- var fs5 = __toESM(require("fs"), 1);
993
- var path3 = __toESM(require("path"), 1);
994
- var import_handlebars = __toESM(require("handlebars"), 1);
995
- function registerHandlebarsHelpers() {
996
- import_handlebars.default.registerHelper("capitalize", (str) => {
997
- return str.charAt(0).toUpperCase() + str.slice(1);
998
- });
999
- import_handlebars.default.registerHelper("eq", (a, b) => {
1000
- return a === b;
1001
- });
1002
- import_handlebars.default.registerHelper("ne", (a, b) => {
1003
- return a !== b;
1004
- });
1005
- import_handlebars.default.registerHelper("join", (arr, separator) => {
1006
- return Array.isArray(arr) ? arr.join(separator) : "";
1007
- });
1008
- }
1009
- function renderHandlebars(template, context) {
1010
- registerHandlebarsHelpers();
1011
- try {
1012
- const compiled = import_handlebars.default.compile(template);
1013
- return compiled(context);
1014
- } catch (error2) {
1015
- throw new TemplateError(`Handlebars render error: ${error2 instanceof Error ? error2.message : String(error2)}`);
1016
- }
1017
- }
1018
- function renderHandlebarsFile(filePath, context) {
1019
- if (!fs5.existsSync(filePath)) {
1020
- throw new TemplateError(`Template file not found: ${filePath}`);
1021
- }
1022
- const template = fs5.readFileSync(filePath, "utf-8");
1023
- return renderHandlebars(template, context);
1024
- }
1025
- function renderHandlebarsComposable(templateDir, context) {
1026
- registerHandlebarsHelpers();
1027
- const versionPath = path3.join(templateDir, "version.hbs");
1028
- const entryPath = path3.join(templateDir, "entry.hbs");
1029
- const documentPath = path3.join(templateDir, "document.hbs");
1030
- if (!fs5.existsSync(documentPath)) {
1031
- throw new TemplateError(`Document template not found: ${documentPath}`);
1032
- }
1033
- if (fs5.existsSync(versionPath)) {
1034
- import_handlebars.default.registerPartial("version", fs5.readFileSync(versionPath, "utf-8"));
1035
- }
1036
- if (fs5.existsSync(entryPath)) {
1037
- import_handlebars.default.registerPartial("entry", fs5.readFileSync(entryPath, "utf-8"));
1038
- }
1039
- try {
1040
- const compiled = import_handlebars.default.compile(fs5.readFileSync(documentPath, "utf-8"));
1041
- return compiled(context);
1042
- } catch (error2) {
1043
- throw new TemplateError(`Handlebars render error: ${error2 instanceof Error ? error2.message : String(error2)}`);
1044
- }
1045
- }
1046
-
1047
- // src/templates/liquid.ts
1048
- var fs6 = __toESM(require("fs"), 1);
1049
- var path4 = __toESM(require("path"), 1);
1050
- var import_liquidjs = require("liquidjs");
1051
- function createLiquidEngine(root) {
1052
- return new import_liquidjs.Liquid({
1053
- root: root ? [root] : [],
1054
- extname: ".liquid",
1055
- cache: false
1056
- });
1057
- }
1058
- function renderLiquid(template, context) {
1059
- const engine = createLiquidEngine();
1060
- try {
1061
- return engine.renderSync(engine.parse(template), context);
1062
- } catch (error2) {
1063
- throw new TemplateError(`Liquid render error: ${error2 instanceof Error ? error2.message : String(error2)}`);
1064
- }
1065
- }
1066
- function renderLiquidFile(filePath, context) {
1067
- if (!fs6.existsSync(filePath)) {
1068
- throw new TemplateError(`Template file not found: ${filePath}`);
1069
- }
1070
- const template = fs6.readFileSync(filePath, "utf-8");
1071
- return renderLiquid(template, context);
1072
- }
1073
- function renderLiquidComposable(templateDir, context) {
1074
- const documentPath = path4.join(templateDir, "document.liquid");
1075
- if (!fs6.existsSync(documentPath)) {
1076
- throw new TemplateError(`Document template not found: ${documentPath}`);
1077
- }
1078
- const engine = createLiquidEngine(templateDir);
1079
- try {
1080
- return engine.renderFileSync("document", context);
1081
- } catch (error2) {
1082
- throw new TemplateError(`Liquid render error: ${error2 instanceof Error ? error2.message : String(error2)}`);
1083
- }
1084
- }
1085
-
1086
- // src/templates/loader.ts
1087
- var fs7 = __toESM(require("fs"), 1);
1088
- var path5 = __toESM(require("path"), 1);
1089
- function getEngineFromFile(filePath) {
1090
- const ext = path5.extname(filePath).toLowerCase();
1091
- switch (ext) {
1092
- case ".liquid":
1093
- return "liquid";
1094
- case ".hbs":
1095
- case ".handlebars":
1096
- return "handlebars";
1097
- case ".ejs":
1098
- return "ejs";
1099
- default:
1100
- throw new TemplateError(`Unknown template extension: ${ext}`);
1101
- }
1102
- }
1103
- function getRenderFn(engine) {
1104
- switch (engine) {
1105
- case "liquid":
1106
- return renderLiquid;
1107
- case "handlebars":
1108
- return renderHandlebars;
1109
- case "ejs":
1110
- return renderEjs;
1111
- }
1112
- }
1113
- function getRenderFileFn(engine) {
1114
- switch (engine) {
1115
- case "liquid":
1116
- return renderLiquidFile;
1117
- case "handlebars":
1118
- return renderHandlebarsFile;
1119
- case "ejs":
1120
- return renderEjsFile;
1121
- }
1122
- }
1123
- function detectTemplateMode(templatePath) {
1124
- if (!fs7.existsSync(templatePath)) {
1125
- throw new TemplateError(`Template path not found: ${templatePath}`);
1126
- }
1127
- const stat = fs7.statSync(templatePath);
1128
- if (stat.isFile()) {
1129
- return "single";
1130
- }
1131
- if (stat.isDirectory()) {
1132
- return "composable";
1133
- }
1134
- throw new TemplateError(`Invalid template path: ${templatePath}`);
1135
- }
1136
- function renderSingleFile(templatePath, context, engine) {
1137
- const resolvedEngine = engine ?? getEngineFromFile(templatePath);
1138
- const renderFile = getRenderFileFn(resolvedEngine);
1139
- return {
1140
- content: renderFile(templatePath, context),
1141
- engine: resolvedEngine
1142
- };
1143
- }
1144
- function renderComposable(templateDir, context, engine) {
1145
- const files = fs7.readdirSync(templateDir);
1146
- const engineMap = {
1147
- liquid: { document: "document.liquid", version: "version.liquid", entry: "entry.liquid" },
1148
- handlebars: { document: "document.hbs", version: "version.hbs", entry: "entry.hbs" },
1149
- ejs: { document: "document.ejs", version: "version.ejs", entry: "entry.ejs" }
1150
- };
1151
- let resolvedEngine;
1152
- if (engine) {
1153
- resolvedEngine = engine;
1154
- } else {
1155
- const detected = detectEngineFromFiles(templateDir, files);
1156
- if (!detected) {
1157
- throw new TemplateError(`Could not detect template engine. Found files: ${files.join(", ")}`);
1158
- }
1159
- resolvedEngine = detected;
1160
- }
1161
- if (resolvedEngine === "liquid") {
1162
- return { content: renderLiquidComposable(templateDir, context), engine: resolvedEngine };
1163
- }
1164
- if (resolvedEngine === "handlebars") {
1165
- return { content: renderHandlebarsComposable(templateDir, context), engine: resolvedEngine };
1166
- }
1167
- const expectedFiles = engineMap[resolvedEngine];
1168
- const documentPath = path5.join(templateDir, expectedFiles.document);
1169
- if (!fs7.existsSync(documentPath)) {
1170
- throw new TemplateError(`Document template not found: ${expectedFiles.document}`);
1171
- }
1172
- const versionPath = path5.join(templateDir, expectedFiles.version);
1173
- const entryPath = path5.join(templateDir, expectedFiles.entry);
1174
- const render = getRenderFn(resolvedEngine);
1175
- const entryTemplate = fs7.existsSync(entryPath) ? fs7.readFileSync(entryPath, "utf-8") : null;
1176
- const versionTemplate = fs7.existsSync(versionPath) ? fs7.readFileSync(versionPath, "utf-8") : null;
1177
- if (entryTemplate && versionTemplate) {
1178
- const versionsWithEntries = context.versions.map((versionCtx) => {
1179
- const entries = versionCtx.entries.map((entry) => {
1180
- const entryCtx = { ...entry, packageName: versionCtx.packageName, version: versionCtx.version };
1181
- return render(entryTemplate, entryCtx);
1182
- });
1183
- return render(versionTemplate, { ...versionCtx, renderedEntries: entries });
1184
- });
1185
- const docContext = { ...context, renderedVersions: versionsWithEntries };
1186
- return {
1187
- content: render(fs7.readFileSync(documentPath, "utf-8"), docContext),
1188
- engine: resolvedEngine
1189
- };
1190
- }
1191
- return renderSingleFile(documentPath, context, resolvedEngine);
1192
- }
1193
- function detectEngineFromFiles(_dir, files) {
1194
- if (files.some((f) => f.endsWith(".liquid"))) return "liquid";
1195
- if (files.some((f) => f.endsWith(".hbs") || f.endsWith(".handlebars"))) return "handlebars";
1196
- if (files.some((f) => f.endsWith(".ejs"))) return "ejs";
1197
- return null;
1198
- }
1199
- function validateDocumentContext(context, templatePath) {
1200
- if (!context.project?.name) {
1201
- throw new TemplateError(`${templatePath}: DocumentContext missing required field "project.name"`);
1202
- }
1203
- if (!Array.isArray(context.versions)) {
1204
- throw new TemplateError(`${templatePath}: DocumentContext missing required field "versions" (must be an array)`);
1205
- }
1206
- const requiredVersionFields = ["packageName", "version", "date", "entries"];
1207
- for (const [i, v] of context.versions.entries()) {
1208
- for (const field of requiredVersionFields) {
1209
- if (v[field] === void 0 || v[field] === null) {
1210
- throw new TemplateError(`${templatePath}: versions[${i}] missing required field "${field}"`);
1211
- }
1212
- }
1213
- if (!Array.isArray(v.entries)) {
1214
- throw new TemplateError(`${templatePath}: versions[${i}].entries must be an array`);
1215
- }
1216
- }
1217
- }
1218
- function renderTemplate(templatePath, context, engine) {
1219
- validateDocumentContext(context, templatePath);
1220
- const mode = detectTemplateMode(templatePath);
1221
- if (mode === "single") {
1222
- return renderSingleFile(templatePath, context, engine);
1223
- }
1224
- return renderComposable(templatePath, context, engine);
1225
- }
1226
-
1227
- // src/core/pipeline.ts
1228
- var import_meta = {};
1229
- function generateCompareUrl(repoUrl, from, to) {
1230
- if (/gitlab\.com/i.test(repoUrl)) {
1231
- return `${repoUrl}/-/compare/${from}...${to}`;
1232
- }
1233
- if (/bitbucket\.org/i.test(repoUrl)) {
1234
- return `${repoUrl}/branches/compare/${from}..${to}`;
1235
- }
1236
- return `${repoUrl}/compare/${from}...${to}`;
1237
- }
1238
- function createTemplateContext(pkg) {
1239
- const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
1240
- return {
1241
- packageName: pkg.packageName,
1242
- version: pkg.version,
1243
- previousVersion: pkg.previousVersion,
1244
- date: pkg.date,
1245
- repoUrl: pkg.repoUrl,
1246
- entries: pkg.entries,
1247
- compareUrl
1248
- };
1249
- }
1250
- function createDocumentContext(contexts, repoUrl) {
1251
- const compareUrls = {};
1252
- for (const ctx of contexts) {
1253
- if (ctx.compareUrl) {
1254
- compareUrls[ctx.version] = ctx.compareUrl;
1255
- }
1256
- }
1257
- return {
1258
- project: {
1259
- name: contexts[0]?.packageName ?? "project",
1260
- repoUrl
1261
- },
1262
- versions: contexts,
1263
- compareUrls: Object.keys(compareUrls).length > 0 ? compareUrls : void 0
1264
- };
1265
- }
1266
- async function processWithLLM(context, config) {
1267
- if (!config.llm) {
1268
- return context;
1269
- }
1270
- const tasks = config.llm.tasks ?? {};
1271
- const llmContext = {
1272
- packageName: context.packageName,
1273
- version: context.version,
1274
- previousVersion: context.previousVersion ?? void 0,
1275
- date: context.date,
1276
- categories: config.llm.categories,
1277
- style: config.llm.style
1278
- };
1279
- const enhanced = {
1280
- entries: context.entries
1281
- };
1282
- try {
1283
- (0, import_core8.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1284
- if (config.llm.baseURL) {
1285
- (0, import_core8.info)(`LLM base URL: ${config.llm.baseURL}`);
1286
- }
1287
- const rawProvider = createProvider(config.llm);
1288
- const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
1289
- const provider = {
1290
- name: rawProvider.name,
1291
- complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
1292
- };
1293
- const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
1294
- (0, import_core8.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
1295
- if (tasks.enhance && tasks.categorize) {
1296
- (0, import_core8.info)("Enhancing and categorizing entries with LLM...");
1297
- const result = await enhanceAndCategorize(provider, context.entries, llmContext);
1298
- enhanced.entries = result.enhancedEntries;
1299
- enhanced.categories = {};
1300
- for (const cat of result.categories) {
1301
- enhanced.categories[cat.category] = cat.entries;
1302
- }
1303
- (0, import_core8.info)(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
1304
- } else {
1305
- if (tasks.enhance) {
1306
- (0, import_core8.info)("Enhancing entries with LLM...");
1307
- enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
1308
- (0, import_core8.info)(`Enhanced ${enhanced.entries.length} entries`);
1309
- }
1310
- if (tasks.categorize) {
1311
- (0, import_core8.info)("Categorizing entries with LLM...");
1312
- const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
1313
- enhanced.categories = {};
1314
- for (const cat of categorized) {
1315
- enhanced.categories[cat.category] = cat.entries;
1316
- }
1317
- (0, import_core8.info)(`Created ${categorized.length} categories`);
1318
- }
1319
- }
1320
- if (tasks.summarize) {
1321
- (0, import_core8.info)("Summarizing entries with LLM...");
1322
- enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
1323
- if (enhanced.summary) {
1324
- (0, import_core8.info)("Summary generated successfully");
1325
- (0, import_core8.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1326
- } else {
1327
- (0, import_core8.warn)("Summary generation returned empty result");
1328
- }
1329
- }
1330
- if (tasks.releaseNotes) {
1331
- (0, import_core8.info)("Generating release notes with LLM...");
1332
- enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
1333
- if (enhanced.releaseNotes) {
1334
- (0, import_core8.info)("Release notes generated successfully");
1335
- } else {
1336
- (0, import_core8.warn)("Release notes generation returned empty result");
1337
- }
1338
- }
1339
- return {
1340
- ...context,
1341
- entries: enhanced.entries,
1342
- enhanced
1343
- };
1344
- } catch (error2) {
1345
- (0, import_core8.warn)(`LLM processing failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
1346
- (0, import_core8.warn)("Falling back to raw entries");
1347
- return context;
1348
- }
1349
- }
1350
- function getBuiltinTemplatePath(style) {
1351
- let packageRoot;
1352
- try {
1353
- const currentUrl = import_meta.url;
1354
- packageRoot = path6.dirname(new URL(currentUrl).pathname);
1355
- packageRoot = path6.join(packageRoot, "..", "..");
1356
- } catch {
1357
- packageRoot = __dirname;
1358
- }
1359
- return path6.join(packageRoot, "templates", style);
1360
- }
1361
- async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1362
- let templatePath;
1363
- if (config.templates?.path) {
1364
- templatePath = path6.resolve(config.templates.path);
1365
- } else {
1366
- templatePath = getBuiltinTemplatePath("keep-a-changelog");
1367
- }
1368
- const documentContext = createDocumentContext(
1369
- contexts,
1370
- config.templates?.path ? void 0 : contexts[0]?.repoUrl ?? void 0
1371
- );
1372
- const result = renderTemplate(templatePath, documentContext, config.templates?.engine);
1373
- if (dryRun) {
1374
- (0, import_core8.info)("--- Changelog Preview ---");
1375
- console.log(result.content);
1376
- (0, import_core8.info)("--- End Preview ---");
1377
- return;
1378
- }
1379
- if (outputPath === "-") {
1380
- process.stdout.write(result.content);
1381
- return;
1382
- }
1383
- const dir = path6.dirname(outputPath);
1384
- if (!fs8.existsSync(dir)) {
1385
- fs8.mkdirSync(dir, { recursive: true });
1386
- }
1387
- fs8.writeFileSync(outputPath, result.content, "utf-8");
1388
- (0, import_core8.success)(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1389
- }
1390
- async function runPipeline(input, config, dryRun) {
1391
- (0, import_core8.debug)(`Processing ${input.packages.length} package(s)`);
1392
- let contexts = input.packages.map(createTemplateContext);
1393
- if (config.llm && !process.env.CHANGELOG_NO_LLM) {
1394
- (0, import_core8.info)("Processing with LLM enhancement");
1395
- contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
1396
- }
1397
- for (const output of config.output) {
1398
- (0, import_core8.info)(`Generating ${output.format} output`);
1399
- switch (output.format) {
1400
- case "markdown": {
1401
- const file = output.file ?? "CHANGELOG.md";
1402
- if (config.templates?.path || output.options?.template) {
1403
- await generateWithTemplate(contexts, config, file, dryRun);
1404
- } else {
1405
- writeMarkdown(file, contexts, config, dryRun);
1406
- }
1407
- break;
1408
- }
1409
- case "json": {
1410
- const file = output.file ?? "changelog.json";
1411
- writeJson(file, contexts, dryRun);
1412
- break;
1413
- }
1414
- case "github-release": {
1415
- if (dryRun) {
1416
- (0, import_core8.info)("[DRY RUN] Would create GitHub release");
1417
- break;
1418
- }
1419
- const firstContext = contexts[0];
1420
- if (!firstContext) {
1421
- (0, import_core8.warn)("No context available for GitHub release");
1422
- break;
1423
- }
1424
- const repoUrl = firstContext.repoUrl;
1425
- if (!repoUrl) {
1426
- (0, import_core8.warn)("No repo URL available, cannot create GitHub release");
1427
- break;
1428
- }
1429
- const parsed = parseRepoUrl(repoUrl);
1430
- if (!parsed) {
1431
- (0, import_core8.warn)(`Could not parse repo URL: ${repoUrl}`);
1432
- break;
1433
- }
1434
- await createGitHubRelease(firstContext, {
1435
- owner: parsed.owner,
1436
- repo: parsed.repo,
1437
- draft: output.options?.draft,
1438
- prerelease: output.options?.prerelease
1439
- });
1440
- break;
1441
- }
1442
- }
1443
- }
1444
- }
1445
-
1446
- // src/monorepo/aggregator.ts
1447
- var fs9 = __toESM(require("fs"), 1);
1448
- var path7 = __toESM(require("path"), 1);
1449
- var import_core9 = require("@releasekit/core");
1450
-
1451
- // src/monorepo/splitter.ts
1452
- function splitByPackage(contexts) {
1453
- const byPackage = /* @__PURE__ */ new Map();
1454
- for (const ctx of contexts) {
1455
- byPackage.set(ctx.packageName, ctx);
1456
- }
1457
- return byPackage;
1458
- }
1459
-
1460
- // src/monorepo/aggregator.ts
1461
- function writeFile(outputPath, content, dryRun) {
1462
- if (dryRun) {
1463
- (0, import_core9.info)(`[DRY RUN] Would write to ${outputPath}`);
1464
- console.log(content);
1465
- return;
1466
- }
1467
- const dir = path7.dirname(outputPath);
1468
- if (!fs9.existsSync(dir)) {
1469
- fs9.mkdirSync(dir, { recursive: true });
1470
- }
1471
- fs9.writeFileSync(outputPath, content, "utf-8");
1472
- (0, import_core9.success)(`Changelog written to ${outputPath}`);
1473
- }
1474
- function aggregateToRoot(contexts) {
1475
- const aggregated = {
1476
- packageName: "monorepo",
1477
- version: contexts[0]?.version ?? "0.0.0",
1478
- previousVersion: contexts[0]?.previousVersion ?? null,
1479
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
1480
- repoUrl: contexts[0]?.repoUrl ?? null,
1481
- entries: []
1482
- };
1483
- for (const ctx of contexts) {
1484
- for (const entry of ctx.entries) {
1485
- aggregated.entries.push({
1486
- ...entry,
1487
- scope: entry.scope ? `${ctx.packageName}/${entry.scope}` : ctx.packageName
1488
- });
1489
- }
1490
- }
1491
- return aggregated;
1492
- }
1493
- function writeMonorepoChangelogs(contexts, options, config, dryRun) {
1494
- if (options.mode === "root" || options.mode === "both") {
1495
- const aggregated = aggregateToRoot(contexts);
1496
- const rootPath = path7.join(options.rootPath, "CHANGELOG.md");
1497
- (0, import_core9.info)(`Writing root changelog to ${rootPath}`);
1498
- const rootContent = config.updateStrategy === "prepend" && fs9.existsSync(rootPath) ? prependVersion(rootPath, aggregated) : renderMarkdown([aggregated]);
1499
- writeFile(rootPath, rootContent, dryRun);
1500
- }
1501
- if (options.mode === "packages" || options.mode === "both") {
1502
- const byPackage = splitByPackage(contexts);
1503
- const packageDirMap = buildPackageDirMap(options.rootPath, options.packagesPath);
1504
- for (const [packageName, ctx] of byPackage) {
1505
- const simpleName = packageName.split("/").pop();
1506
- const packageDir = packageDirMap.get(packageName) ?? (simpleName ? packageDirMap.get(simpleName) : void 0) ?? null;
1507
- if (packageDir) {
1508
- const changelogPath = path7.join(packageDir, "CHANGELOG.md");
1509
- (0, import_core9.info)(`Writing changelog for ${packageName} to ${changelogPath}`);
1510
- const pkgContent = config.updateStrategy === "prepend" && fs9.existsSync(changelogPath) ? prependVersion(changelogPath, ctx) : renderMarkdown([ctx]);
1511
- writeFile(changelogPath, pkgContent, dryRun);
1512
- } else {
1513
- (0, import_core9.info)(`Could not find directory for package ${packageName}, skipping`);
1514
- }
1515
- }
1516
- }
1517
- }
1518
- function buildPackageDirMap(rootPath, packagesPath) {
1519
- const map = /* @__PURE__ */ new Map();
1520
- const packagesDir = path7.join(rootPath, packagesPath);
1521
- if (!fs9.existsSync(packagesDir)) {
1522
- return map;
1523
- }
1524
- for (const entry of fs9.readdirSync(packagesDir, { withFileTypes: true })) {
1525
- if (!entry.isDirectory()) continue;
1526
- const dirPath = path7.join(packagesDir, entry.name);
1527
- map.set(entry.name, dirPath);
1528
- const packageJsonPath = path7.join(dirPath, "package.json");
1529
- if (fs9.existsSync(packageJsonPath)) {
1530
- try {
1531
- const pkg = JSON.parse(fs9.readFileSync(packageJsonPath, "utf-8"));
1532
- if (pkg.name) {
1533
- map.set(pkg.name, dirPath);
1534
- }
1535
- } catch {
1536
- }
1537
- }
1538
- }
1539
- return map;
1540
- }
1541
- function detectMonorepo(cwd) {
1542
- const pnpmWorkspacesPath = path7.join(cwd, "pnpm-workspace.yaml");
1543
- const packageJsonPath = path7.join(cwd, "package.json");
1544
- if (fs9.existsSync(pnpmWorkspacesPath)) {
1545
- const content = fs9.readFileSync(pnpmWorkspacesPath, "utf-8");
1546
- const packagesMatch = content.match(/packages:\s*\n\s*-\s*['"]([^'"]+)['"]/);
1547
- if (packagesMatch?.[1]) {
1548
- const packagesGlob = packagesMatch[1];
1549
- const packagesPath = packagesGlob.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1550
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1551
- }
1552
- return { isMonorepo: true, packagesPath: "packages" };
1553
- }
1554
- if (fs9.existsSync(packageJsonPath)) {
1555
- try {
1556
- const content = fs9.readFileSync(packageJsonPath, "utf-8");
1557
- const pkg = JSON.parse(content);
1558
- if (pkg.workspaces) {
1559
- const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
1560
- if (workspaces?.length) {
1561
- const firstWorkspace = workspaces[0];
1562
- if (firstWorkspace) {
1563
- const packagesPath = firstWorkspace.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
1564
- return { isMonorepo: true, packagesPath: packagesPath || "packages" };
1565
- }
1566
- }
1567
- }
1568
- } catch {
1569
- return { isMonorepo: false, packagesPath: "" };
1570
- }
1571
- }
1572
- return { isMonorepo: false, packagesPath: "" };
1573
- }
1574
-
1575
- // src/cli.ts
1576
- var program = new import_commander.Command();
1577
- program.name("releasekit-notes").description("Generate changelogs with LLM-powered enhancement and flexible templating").version("0.1.0");
1578
- program.command("generate", { isDefault: true }).description("Generate changelog from input data").option("-i, --input <file>", "Input file (default: stdin)").option("-o, --output <spec>", "Output spec (format:file)", collectOutputs, []).option("-t, --template <path>", "Template file or directory").option("-e, --engine <engine>", "Template engine (handlebars|liquid|ejs)").option("--monorepo <mode>", "Monorepo mode (root|packages|both)").option("--llm-provider <provider>", "LLM provider").option("--llm-model <model>", "LLM model").option("--llm-base-url <url>", "LLM base URL (for openai-compatible provider)").option("--llm-tasks <tasks>", "Comma-separated LLM tasks").option("--no-llm", "Disable LLM processing").option("--target <package>", "Filter to a specific package name").option("--config <path>", "Config file path").option("--dry-run", "Preview without writing").option("--regenerate", "Regenerate entire changelog").option("-v, --verbose", "Increase verbosity", increaseVerbosity, 0).option("-q, --quiet", "Suppress non-error output").action(async (options) => {
1579
- setVerbosity(options.verbose);
1580
- if (options.quiet) (0, import_core10.setQuietMode)(true);
1581
- try {
1582
- const config = loadConfig(process.cwd(), options.config);
1583
- if (options.output.length > 0) {
1584
- config.output = options.output;
1585
- }
1586
- if (config.output.length === 0) {
1587
- config.output = getDefaultConfig().output;
1588
- }
1589
- if (options.regenerate) {
1590
- config.updateStrategy = "regenerate";
1591
- }
1592
- if (options.template) {
1593
- config.templates = { ...config.templates, path: options.template };
1594
- }
1595
- if (options.engine) {
1596
- config.templates = { ...config.templates, engine: options.engine };
1597
- }
1598
- if (options.llm === false) {
1599
- (0, import_core10.info)("LLM processing disabled via --no-llm flag");
1600
- delete config.llm;
1601
- } else if (options.llmProvider || options.llmModel || options.llmBaseUrl || options.llmTasks) {
1602
- config.llm = config.llm ?? { provider: "openai-compatible", model: "" };
1603
- if (options.llmProvider) config.llm.provider = options.llmProvider;
1604
- if (options.llmModel) config.llm.model = options.llmModel;
1605
- if (options.llmBaseUrl) config.llm.baseURL = options.llmBaseUrl;
1606
- if (options.llmTasks) {
1607
- const taskNames = options.llmTasks.split(",").map((t) => t.trim());
1608
- config.llm.tasks = {
1609
- enhance: taskNames.includes("enhance"),
1610
- summarize: taskNames.includes("summarize"),
1611
- categorize: taskNames.includes("categorize"),
1612
- releaseNotes: taskNames.includes("release-notes") || taskNames.includes("releaseNotes")
1613
- };
1614
- }
1615
- (0, import_core10.info)(`LLM configured: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
1616
- if (config.llm.baseURL) {
1617
- (0, import_core10.info)(`LLM base URL: ${config.llm.baseURL}`);
1618
- }
1619
- const taskList = Object.entries(config.llm.tasks || {}).filter(([, enabled]) => enabled).map(([name]) => name).join(", ");
1620
- if (taskList) {
1621
- (0, import_core10.info)(`LLM tasks: ${taskList}`);
1622
- }
1623
- }
1624
- let inputJson;
1625
- if (options.input) {
1626
- inputJson = fs10.readFileSync(options.input, "utf-8");
1627
- } else {
1628
- inputJson = await readStdin();
1629
- }
1630
- const input = parsePackageVersioner(inputJson);
1631
- if (options.target) {
1632
- const before = input.packages.length;
1633
- input.packages = input.packages.filter((p) => p.packageName === options.target);
1634
- if (input.packages.length === 0) {
1635
- (0, import_core10.info)(`No changelog found for package "${options.target}" (had ${before} package(s))`);
1636
- return;
1637
- }
1638
- (0, import_core10.info)(`Filtered to package: ${options.target}`);
1639
- }
1640
- if (options.monorepo || config.monorepo) {
1641
- const monorepoMode = options.monorepo ?? config.monorepo?.mode ?? "both";
1642
- const detected = detectMonorepo(process.cwd());
1643
- if (!detected.isMonorepo) {
1644
- (0, import_core10.info)("No monorepo detected, using single package mode");
1645
- await runPipeline(input, config, options.dryRun ?? false);
1646
- } else {
1647
- (0, import_core10.info)(`Monorepo detected with packages at ${detected.packagesPath}`);
1648
- const contexts = input.packages.map(createTemplateContext);
1649
- writeMonorepoChangelogs(
1650
- contexts,
1651
- {
1652
- rootPath: config.monorepo?.rootPath ?? process.cwd(),
1653
- packagesPath: config.monorepo?.packagesPath ?? detected.packagesPath,
1654
- mode: monorepoMode
1655
- },
1656
- config,
1657
- options.dryRun ?? false
1658
- );
1659
- }
1660
- } else {
1661
- await runPipeline(input, config, options.dryRun ?? false);
1662
- }
1663
- if (options.dryRun) {
1664
- (0, import_core10.info)("Dry run complete - no files were written");
1665
- } else {
1666
- (0, import_core10.success)("Changelog generation complete");
1667
- }
1668
- } catch (err) {
1669
- handleError(err);
1670
- }
1671
- });
1672
- program.command("init").description("Create default configuration file").option("-f, --force", "Overwrite existing config").action((options) => {
1673
- const configPath = "releasekit.config.json";
1674
- if (fs10.existsSync(configPath) && !options.force) {
1675
- (0, import_core10.error)(`Config file already exists at ${configPath}. Use --force to overwrite.`);
1676
- process.exit(import_core2.EXIT_CODES.GENERAL_ERROR);
1677
- }
1678
- const defaultConfig = {
1679
- $schema: "https://releasekit.dev/schema.json",
1680
- notes: {
1681
- output: [{ format: "markdown", file: "CHANGELOG.md" }],
1682
- updateStrategy: "prepend"
1683
- }
1684
- };
1685
- fs10.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), "utf-8");
1686
- (0, import_core10.success)(`Created config file at ${configPath}`);
1687
- });
1688
- program.command("auth <provider>").description("Configure API key for an LLM provider").option("--key <key>", "API key (omit to be prompted)").action(async (provider, options) => {
1689
- let apiKey;
1690
- if (options.key) {
1691
- apiKey = options.key;
1692
- } else {
1693
- apiKey = await promptSecret(`Enter API key for ${provider}: `);
1694
- }
1695
- if (!apiKey.trim()) {
1696
- (0, import_core10.error)("API key cannot be empty");
1697
- process.exit(import_core2.EXIT_CODES.GENERAL_ERROR);
1698
- }
1699
- (0, import_config.saveAuth)(provider, apiKey.trim());
1700
- (0, import_core10.success)(`API key saved for ${provider}`);
1701
- });
1702
- program.command("providers").description("List available LLM providers").action(() => {
1703
- (0, import_core10.info)("Available LLM providers:");
1704
- console.log(" openai - OpenAI (GPT models)");
1705
- console.log(" anthropic - Anthropic (Claude models)");
1706
- console.log(" ollama - Ollama (local models)");
1707
- console.log(" openai-compatible - Any OpenAI-compatible endpoint");
1708
- });
1709
- function collectOutputs(value, previous) {
1710
- const parts = value.split(":");
1711
- const format = parts[0] ?? "markdown";
1712
- const file = parts[1];
1713
- const spec = { format };
1714
- if (file) {
1715
- spec.file = file;
1716
- }
1717
- return [...previous, spec];
1718
- }
1719
- function increaseVerbosity(_, previous) {
1720
- return previous + 1;
1721
- }
1722
- function setVerbosity(level) {
1723
- const levels = ["info", "debug", "trace"];
1724
- (0, import_core10.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "info");
1725
- }
1726
- async function readStdin() {
1727
- const chunks = [];
1728
- for await (const chunk of process.stdin) {
1729
- chunks.push(chunk);
1730
- }
1731
- return chunks.join("");
1732
- }
1733
- function promptSecret(prompt) {
1734
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1735
- return new Promise((resolve2) => {
1736
- rl.question(prompt, (answer) => {
1737
- rl.close();
1738
- resolve2(answer);
1739
- });
1740
- });
1741
- }
1742
- function handleError(err) {
1743
- if (err instanceof NotesError) {
1744
- err.logError();
1745
- process.exit(getExitCode(err));
1746
- }
1747
- (0, import_core10.error)(err instanceof Error ? err.message : String(err));
1748
- process.exit(import_core2.EXIT_CODES.GENERAL_ERROR);
1749
- }
1750
- program.parse();