@releasekit/notes 0.3.1 → 0.4.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.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  NotesError,
4
- getDefaultConfig,
5
4
  getExitCode,
6
5
  loadConfig,
7
6
  parseVersionOutput,
8
7
  runPipeline,
9
8
  saveAuth
10
- } from "./chunk-ENAWZXFG.js";
9
+ } from "./chunk-QX23CBNW.js";
11
10
  import {
12
11
  EXIT_CODES,
13
12
  error,
@@ -15,8 +14,9 @@ import {
15
14
  readPackageVersion,
16
15
  setLogLevel,
17
16
  setQuietMode,
18
- success
19
- } from "./chunk-E4454SIS.js";
17
+ success,
18
+ warn
19
+ } from "./chunk-7TJSPQPW.js";
20
20
 
21
21
  // src/cli.ts
22
22
  import * as fs from "fs";
@@ -27,50 +27,98 @@ function createNotesCommand() {
27
27
  const cmd = new Command("notes").description(
28
28
  "Generate changelogs with LLM-powered enhancement and flexible templating"
29
29
  );
30
- cmd.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) => {
30
+ cmd.command("generate", { isDefault: true }).description("Generate changelog from input data").option("-i, --input <file>", "Input file (default: stdin)").option("--no-changelog", "Disable changelog generation").option("--changelog-mode <mode>", "Changelog location mode (root|packages|both)").option("--changelog-file <name>", "Changelog file name override").option("--release-notes-mode <mode>", "Enable release notes and set location (root|packages|both)").option("--release-notes-file <name>", "Release notes file name override").option("--no-release-notes", "Disable release notes generation").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("--regenerate", "Regenerate entire changelog instead of prepending new entries").option("--dry-run", "Preview without writing").option("-v, --verbose", "Increase verbosity", increaseVerbosity, 0).option("-q, --quiet", "Suppress non-error output").action(async (options) => {
31
31
  setVerbosity(options.verbose);
32
32
  if (options.quiet) setQuietMode(true);
33
33
  try {
34
- const config = loadConfig(process.cwd(), options.config);
35
- if (options.output.length > 0) {
36
- config.output = options.output;
34
+ const loadedConfig = loadConfig({ cwd: process.cwd(), configPath: options.config });
35
+ const config = loadedConfig?.notes ?? {};
36
+ if (loadedConfig?.monorepo && !config.monorepo) {
37
+ config.monorepo = loadedConfig.monorepo;
37
38
  }
38
- if (config.output.length === 0) {
39
- config.output = getDefaultConfig().output;
39
+ if (options.changelog === false) {
40
+ config.changelog = false;
41
+ } else if (options.changelogMode || options.changelogFile) {
42
+ const existing = config.changelog !== false ? config.changelog ?? {} : {};
43
+ const mode = options.changelogMode ?? existing.mode ?? "root";
44
+ config.changelog = {
45
+ ...existing,
46
+ mode,
47
+ ...options.changelogFile ? { file: options.changelogFile } : {}
48
+ };
40
49
  }
41
- if (options.regenerate) {
42
- config.updateStrategy = "regenerate";
50
+ if (options.releaseNotes === false) {
51
+ config.releaseNotes = false;
52
+ } else if (options.releaseNotesMode || options.releaseNotesFile) {
53
+ const existing = config.releaseNotes !== false ? config.releaseNotes ?? {} : {};
54
+ const mode = options.releaseNotesMode ?? existing.mode ?? "root";
55
+ config.releaseNotes = {
56
+ ...existing,
57
+ mode,
58
+ ...options.releaseNotesFile ? { file: options.releaseNotesFile } : {}
59
+ };
60
+ }
61
+ if (config.changelog === false && (options.template || options.engine)) {
62
+ const ignored = [options.template && "--template", options.engine && "--engine"].filter(Boolean).join(" and ");
63
+ warn(`${ignored} ignored: changelog is disabled via --no-changelog`);
43
64
  }
44
- if (options.template) {
45
- config.templates = { ...config.templates, path: options.template };
65
+ if (options.template && config.changelog !== false) {
66
+ const existing = config.changelog ?? {};
67
+ config.changelog = {
68
+ ...existing,
69
+ templates: { ...existing.templates, path: options.template }
70
+ };
46
71
  }
47
- if (options.engine) {
48
- config.templates = { ...config.templates, engine: options.engine };
72
+ if (options.engine && config.changelog !== false) {
73
+ const existing = config.changelog ?? {};
74
+ config.changelog = {
75
+ ...existing,
76
+ templates: { ...existing.templates, engine: options.engine }
77
+ };
78
+ }
79
+ if (options.regenerate) {
80
+ config.updateStrategy = "regenerate";
49
81
  }
50
82
  if (options.llm === false) {
51
- info("LLM processing disabled via --no-llm flag");
52
- delete config.llm;
83
+ if (config.releaseNotes && typeof config.releaseNotes !== "boolean" && config.releaseNotes.llm) {
84
+ info("LLM processing disabled via --no-llm flag");
85
+ config.releaseNotes = { ...config.releaseNotes, llm: void 0 };
86
+ }
53
87
  } else if (options.llmProvider || options.llmModel || options.llmBaseUrl || options.llmTasks) {
54
- config.llm = config.llm ?? { provider: "openai-compatible", model: "" };
55
- if (options.llmProvider) config.llm.provider = options.llmProvider;
56
- if (options.llmModel) config.llm.model = options.llmModel;
57
- if (options.llmBaseUrl) config.llm.baseURL = options.llmBaseUrl;
58
- if (options.llmTasks) {
59
- const taskNames = options.llmTasks.split(",").map((t) => t.trim());
60
- config.llm.tasks = {
61
- enhance: taskNames.includes("enhance"),
62
- summarize: taskNames.includes("summarize"),
63
- categorize: taskNames.includes("categorize"),
64
- releaseNotes: taskNames.includes("release-notes") || taskNames.includes("releaseNotes")
88
+ if (config.releaseNotes === false) {
89
+ warn("--llm-* flags ignored: release notes are disabled via --no-release-notes");
90
+ } else {
91
+ const existingRn = typeof config.releaseNotes === "object" ? config.releaseNotes : {};
92
+ const existingLlm = existingRn.llm;
93
+ const llm = {
94
+ provider: existingLlm?.provider ?? "openai-compatible",
95
+ model: existingLlm?.model ?? "",
96
+ ...existingLlm ?? {}
65
97
  };
66
- }
67
- info(`LLM configured: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
68
- if (config.llm.baseURL) {
69
- info(`LLM base URL: ${config.llm.baseURL}`);
70
- }
71
- const taskList = Object.entries(config.llm.tasks || {}).filter(([, enabled]) => enabled).map(([name]) => name).join(", ");
72
- if (taskList) {
73
- info(`LLM tasks: ${taskList}`);
98
+ if (options.llmProvider) llm.provider = options.llmProvider;
99
+ if (options.llmModel) llm.model = options.llmModel;
100
+ if (options.llmBaseUrl) llm.baseURL = options.llmBaseUrl;
101
+ if (options.llmTasks) {
102
+ const taskNames = options.llmTasks.split(",").map((t) => t.trim());
103
+ llm.tasks = {
104
+ enhance: taskNames.includes("enhance"),
105
+ summarize: taskNames.includes("summarize"),
106
+ categorize: taskNames.includes("categorize"),
107
+ releaseNotes: taskNames.includes("release-notes") || taskNames.includes("releaseNotes")
108
+ };
109
+ }
110
+ config.releaseNotes = {
111
+ ...existingRn,
112
+ llm
113
+ };
114
+ info(`LLM configured: ${llm.provider}${llm.model ? ` (${llm.model})` : ""}`);
115
+ if (llm.baseURL) {
116
+ info(`LLM base URL: ${llm.baseURL}`);
117
+ }
118
+ const taskList = Object.entries(llm.tasks || {}).filter(([, enabled]) => enabled).map(([name]) => name).join(", ");
119
+ if (taskList) {
120
+ info(`LLM tasks: ${taskList}`);
121
+ }
74
122
  }
75
123
  }
76
124
  let inputJson;
@@ -90,7 +138,14 @@ function createNotesCommand() {
90
138
  info(`Filtered to package: ${options.target}`);
91
139
  }
92
140
  if (options.monorepo) {
93
- config.monorepo = { ...config.monorepo, mode: options.monorepo };
141
+ const monoMode = options.monorepo;
142
+ config.monorepo = { ...config.monorepo, mode: monoMode };
143
+ if (config.changelog !== false && !options.changelogMode) {
144
+ config.changelog = { ...config.changelog ?? {}, mode: monoMode };
145
+ }
146
+ if (config.releaseNotes !== false && config.releaseNotes !== void 0 && !options.releaseNotesMode) {
147
+ config.releaseNotes = { ...config.releaseNotes, mode: monoMode };
148
+ }
94
149
  }
95
150
  await runPipeline(input, config, options.dryRun ?? false);
96
151
  if (options.dryRun) {
@@ -102,22 +157,6 @@ function createNotesCommand() {
102
157
  handleError(err);
103
158
  }
104
159
  });
105
- cmd.command("init").description("Create default configuration file").option("-f, --force", "Overwrite existing config").action((options) => {
106
- const configPath = "releasekit.config.json";
107
- if (fs.existsSync(configPath) && !options.force) {
108
- error(`Config file already exists at ${configPath}. Use --force to overwrite.`);
109
- process.exit(EXIT_CODES.GENERAL_ERROR);
110
- }
111
- const defaultConfig = {
112
- $schema: "https://releasekit.dev/schema.json",
113
- notes: {
114
- output: [{ format: "markdown", file: "CHANGELOG.md" }],
115
- updateStrategy: "prepend"
116
- }
117
- };
118
- fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), "utf-8");
119
- success(`Created config file at ${configPath}`);
120
- });
121
160
  cmd.command("auth <provider>").description("Configure API key for an LLM provider").option("--key <key>", "API key (omit to be prompted)").action(async (provider, options) => {
122
161
  let apiKey;
123
162
  if (options.key) {
@@ -141,16 +180,6 @@ function createNotesCommand() {
141
180
  });
142
181
  return cmd;
143
182
  }
144
- function collectOutputs(value, previous) {
145
- const parts = value.split(":");
146
- const format = parts[0] ?? "markdown";
147
- const file = parts[1];
148
- const spec = { format };
149
- if (file) {
150
- spec.file = file;
151
- }
152
- return [...previous, spec];
153
- }
154
183
  function increaseVerbosity(_, previous) {
155
184
  return previous + 1;
156
185
  }
package/dist/index.js CHANGED
@@ -9,26 +9,63 @@ import {
9
9
  getDefaultConfig,
10
10
  getExitCode,
11
11
  loadAuth,
12
- loadConfig,
12
+ loadConfig2 as loadConfig,
13
13
  parseVersionOutput,
14
14
  parseVersionOutputFile,
15
15
  parseVersionOutputStdin,
16
16
  processInput,
17
- renderJson,
18
17
  runPipeline,
19
- saveAuth,
20
- writeJson
21
- } from "./chunk-ENAWZXFG.js";
18
+ saveAuth
19
+ } from "./chunk-QX23CBNW.js";
22
20
  import {
23
21
  aggregateToRoot,
24
22
  detectMonorepo,
25
23
  writeMonorepoChangelogs
26
- } from "./chunk-DCQ32FVH.js";
24
+ } from "./chunk-F7MUVHZ2.js";
27
25
  import {
26
+ debug,
28
27
  formatVersion,
28
+ info,
29
29
  renderMarkdown,
30
+ success,
30
31
  writeMarkdown
31
- } from "./chunk-E4454SIS.js";
32
+ } from "./chunk-7TJSPQPW.js";
33
+
34
+ // src/output/json.ts
35
+ import * as fs from "fs";
36
+ import * as path from "path";
37
+ function renderJson(contexts) {
38
+ return JSON.stringify(
39
+ {
40
+ versions: contexts.map((ctx) => ({
41
+ packageName: ctx.packageName,
42
+ version: ctx.version,
43
+ previousVersion: ctx.previousVersion,
44
+ date: ctx.date,
45
+ entries: ctx.entries,
46
+ compareUrl: ctx.compareUrl
47
+ }))
48
+ },
49
+ null,
50
+ 2
51
+ );
52
+ }
53
+ function writeJson(outputPath, contexts, dryRun) {
54
+ const content = renderJson(contexts);
55
+ if (dryRun) {
56
+ info(`Would write JSON output to ${outputPath}`);
57
+ debug("--- JSON Output Preview ---");
58
+ debug(content);
59
+ debug("--- End Preview ---");
60
+ return;
61
+ }
62
+ const dir = path.dirname(outputPath);
63
+ if (!fs.existsSync(dir)) {
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ }
66
+ fs.writeFileSync(outputPath, content, "utf-8");
67
+ success(`JSON output written to ${outputPath}`);
68
+ }
32
69
  export {
33
70
  ConfigError,
34
71
  GitHubError,
@@ -0,0 +1,276 @@
1
+ # Notes Configuration Reference
2
+
3
+ All options live under the `notes` key in `releasekit.config.json`. Add `$schema` for editor autocompletion:
4
+
5
+ ```json
6
+ {
7
+ "$schema": "https://goosewobbler.github.io/releasekit/schema.json",
8
+ "notes": {}
9
+ }
10
+ ```
11
+
12
+ ---
13
+
14
+ ## `notes.changelog`
15
+
16
+ Controls whether and where changelog files are written.
17
+
18
+ **Type:** `false | object`
19
+ **Default:** generates `CHANGELOG.md` at the repo root
20
+
21
+ Set to `false` to disable changelog generation entirely.
22
+
23
+ ```json
24
+ {
25
+ "notes": {
26
+ "changelog": false
27
+ }
28
+ }
29
+ ```
30
+
31
+ When an object, the following properties are available:
32
+
33
+ ### `notes.changelog.mode`
34
+
35
+ | Value | Behaviour |
36
+ |-------|-----------|
37
+ | `"root"` | Write a single changelog at the repo root (default) |
38
+ | `"packages"` | Write one changelog per package inside each package directory |
39
+ | `"both"` | Write both root and per-package changelogs |
40
+
41
+ ```json
42
+ {
43
+ "notes": {
44
+ "changelog": { "mode": "packages" }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### `notes.changelog.file`
50
+
51
+ Override the changelog file name.
52
+
53
+ **Type:** `string`
54
+ **Default:** `"CHANGELOG.md"`
55
+
56
+ ```json
57
+ {
58
+ "notes": {
59
+ "changelog": { "mode": "root", "file": "CHANGES.md" }
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### `notes.changelog.templates`
65
+
66
+ Custom template for changelog rendering.
67
+
68
+ | Property | Type | Description |
69
+ |----------|------|-------------|
70
+ | `path` | `string` | Path to a template file or directory |
71
+ | `engine` | `"handlebars" \| "liquid" \| "ejs"` | Template engine |
72
+
73
+ ```json
74
+ {
75
+ "notes": {
76
+ "changelog": {
77
+ "mode": "root",
78
+ "templates": {
79
+ "path": "./templates/changelog/",
80
+ "engine": "liquid"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ See the [templates guide](./templates.md) for authoring details.
88
+
89
+ ---
90
+
91
+ ## `notes.releaseNotes`
92
+
93
+ Controls release notes generation. Release notes are a separate output from the changelog — typically prose-formatted and suitable for GitHub release bodies.
94
+
95
+ **Type:** `false | object`
96
+ **Default:** `undefined` (release notes not generated)
97
+
98
+ Set to `false` to explicitly disable when it has been enabled via config inheritance.
99
+
100
+ When an object with a `mode` or `file` property, release notes are written to a file. When only `llm` is present (no `mode`/`file`), the LLM runs but no file is written — the generated content is passed to the publish step for use as a GitHub release body.
101
+
102
+ ### `notes.releaseNotes.mode`
103
+
104
+ | Value | Behaviour |
105
+ |-------|-----------|
106
+ | `"root"` | Write `RELEASE_NOTES.md` at the repo root |
107
+ | `"packages"` | Write one release notes file per package |
108
+ | `"both"` | Write both root and per-package files |
109
+
110
+ ### `notes.releaseNotes.file`
111
+
112
+ Override the release notes file name.
113
+
114
+ **Type:** `string`
115
+ **Default:** `"RELEASE_NOTES.md"`
116
+
117
+ ### `notes.releaseNotes.templates`
118
+
119
+ Same structure as `notes.changelog.templates`. See the [templates guide](./templates.md).
120
+
121
+ ### `notes.releaseNotes.llm`
122
+
123
+ LLM configuration for release notes enhancement. Requires `provider` and `model`.
124
+
125
+ ```json
126
+ {
127
+ "notes": {
128
+ "releaseNotes": {
129
+ "llm": {
130
+ "provider": "openai",
131
+ "model": "gpt-4o-mini",
132
+ "tasks": { "enhance": true, "summarize": true }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ See the full LLM option reference below.
140
+
141
+ ---
142
+
143
+ ## `notes.updateStrategy`
144
+
145
+ How existing changelog files are updated when new entries are generated.
146
+
147
+ **Type:** `"prepend" | "regenerate"`
148
+ **Default:** `"prepend"`
149
+
150
+ | Value | Behaviour |
151
+ |-------|-----------|
152
+ | `"prepend"` | New entries are inserted at the top of the existing file |
153
+ | `"regenerate"` | The entire file is rewritten from scratch using all available history |
154
+
155
+ ```json
156
+ {
157
+ "notes": {
158
+ "updateStrategy": "regenerate"
159
+ }
160
+ }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## LLM options (`notes.releaseNotes.llm`)
166
+
167
+ ### Required
168
+
169
+ | Option | Type | Description |
170
+ |--------|------|-------------|
171
+ | `provider` | `string` | LLM provider key. See [LLM providers](./llm-providers.md). |
172
+ | `model` | `string` | Model identifier (e.g. `"gpt-4o-mini"`, `"claude-sonnet-4-5"`) |
173
+
174
+ ### Connection
175
+
176
+ | Option | Type | Description |
177
+ |--------|------|-------------|
178
+ | `baseURL` | `string` | Custom API base URL (for `openai-compatible` providers) |
179
+ | `apiKey` | `string` | API key inline. Prefer env var or `releasekit-notes auth` over this. |
180
+
181
+ ### Behaviour
182
+
183
+ | Option | Type | Default | Description |
184
+ |--------|------|---------|-------------|
185
+ | `concurrency` | `integer` | `3` | Maximum parallel LLM requests when enhancing entries |
186
+
187
+ ### Model options (`options`)
188
+
189
+ | Option | Type | Description |
190
+ |--------|------|-------------|
191
+ | `timeout` | `integer` (ms) | Request timeout |
192
+ | `maxTokens` | `integer` | Maximum tokens to generate |
193
+ | `temperature` | `number` | Sampling temperature (0–2) |
194
+
195
+ ```json
196
+ {
197
+ "llm": {
198
+ "provider": "openai",
199
+ "model": "gpt-4o",
200
+ "options": { "temperature": 0.3, "maxTokens": 1000 }
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Tasks (`tasks`)
206
+
207
+ | Task | Type | Description |
208
+ |------|------|-------------|
209
+ | `enhance` | `boolean` | Rewrite each entry description to be clearer and more user-facing |
210
+ | `summarize` | `boolean` | Generate a one-paragraph summary of the release |
211
+ | `categorize` | `boolean` | Group entries into user-friendly categories (Features, Fixes, …) |
212
+ | `releaseNotes` | `boolean` | Generate full prose release notes suitable for a GitHub release body |
213
+
214
+ ### Categories (`categories`)
215
+
216
+ Provide hints to the `categorize` task for how to group entries.
217
+
218
+ ```json
219
+ {
220
+ "llm": {
221
+ "tasks": { "categorize": true },
222
+ "categories": [
223
+ { "name": "Features", "description": "New user-facing functionality" },
224
+ { "name": "Bug Fixes", "description": "Corrections to existing behaviour" },
225
+ { "name": "Performance", "description": "Speed or resource improvements" }
226
+ ]
227
+ }
228
+ }
229
+ ```
230
+
231
+ Each category object:
232
+
233
+ | Field | Required | Description |
234
+ |-------|----------|-------------|
235
+ | `name` | yes | Display name used in template rendering |
236
+ | `description` | yes | Hint sent to the LLM to guide grouping |
237
+ | `scopes` | no | Commit scopes that always map to this category |
238
+
239
+ ### Style (`style`)
240
+
241
+ A brief style instruction appended to every LLM prompt.
242
+
243
+ ```json
244
+ {
245
+ "llm": {
246
+ "style": "Use concise, non-technical language suitable for end users."
247
+ }
248
+ }
249
+ ```
250
+
251
+ ### Retry (`retry`)
252
+
253
+ | Option | Type | Default | Description |
254
+ |--------|------|---------|-------------|
255
+ | `maxAttempts` | `integer` | `3` | Maximum retries on failure |
256
+ | `initialDelay` | `integer` (ms) | `500` | Delay before first retry |
257
+ | `maxDelay` | `integer` (ms) | `10000` | Maximum delay between retries |
258
+ | `backoffFactor` | `number` | `2` | Exponential backoff multiplier |
259
+
260
+ ### Prompt overrides (`prompts`)
261
+
262
+ Override the built-in prompt instructions or templates for any task. The key is the task name (`enhance`, `categorize`, `summarize`, `releaseNotes`).
263
+
264
+ ```json
265
+ {
266
+ "llm": {
267
+ "prompts": {
268
+ "instructions": {
269
+ "enhance": "Rewrite the description from a developer's perspective, keeping it technical."
270
+ }
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ See the [LLM providers guide](./llm-providers.md) for examples.