@releasekit/notes 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -1,15 +1,21 @@
1
1
  # @releasekit/notes
2
2
 
3
- Release notes and changelog generation from conventional commits with LLM-powered enhancement and flexible templating
3
+ [![@releasekit/notes](https://img.shields.io/badge/@releasekit-notes-9feaf9?labelColor=1a1a1a&style=plastic)](https://www.npmjs.com/package/@releasekit/notes)
4
+ [![Version](https://img.shields.io/npm/v/@releasekit/notes?color=28a745&labelColor=1a1a1a)](https://www.npmjs.com/package/@releasekit/notes)
5
+ [![Downloads](https://img.shields.io/npm/dw/@releasekit/notes?color=6f42c1&labelColor=1a1a1a)](https://www.npmjs.com/package/@releasekit/notes)
6
+
7
+ **Changelog and release notes generation from conventional commits**
8
+
9
+ Generates CHANGELOG.md and release notes from `@releasekit/version` output, with optional LLM-powered enhancement and flexible templating.
4
10
 
5
11
  ## Features
6
12
 
7
- - **Multiple input sources** — `@releasekit/version` JSON, git log, or manual JSON
8
- - **Flexible templating** — Liquid, Handlebars, or EJS with single-file or composable templates
9
- - **LLM enhancement** (optional) summarize, categorize, enhance descriptions, generate release notes
10
- - **Monorepo support** — root aggregation, per-package changelogs, or both
11
- - **Multiple outputs** — Markdown, JSON, or GitHub Releases API
12
- - **Dry-run mode** — preview without writing files
13
+ - 📝 **Conventional changelog** — Keep a Changelog, Angular, or custom format
14
+ - 🤖 **LLM enhancement** (optional) enhance descriptions, summarize, categorize, or generate prose release notes
15
+ - 🎨 **Flexible templating** — Liquid, Handlebars, or EJS; single-file or composable layout
16
+ - 📦 **Monorepo support** — root aggregation, per-package changelogs, or both
17
+ - 🔀 **Two outputs** — `CHANGELOG.md` and `RELEASE_NOTES.md` are configured independently
18
+ - 🔍 **Dry-run mode** — preview without writing files
13
19
 
14
20
  ## Installation
15
21
 
@@ -19,7 +25,7 @@ npm install -g @releasekit/notes
19
25
  pnpm add -g @releasekit/notes
20
26
  ```
21
27
 
22
- > **Note:** This package is ESM only and requires Node.js 20+.
28
+ > **Note:** ESM only. Requires Node.js 20+.
23
29
 
24
30
  ## Quick Start
25
31
 
@@ -30,107 +36,122 @@ releasekit-version --json | releasekit-notes
30
36
  # From a file
31
37
  releasekit-notes --input version-data.json
32
38
 
33
- # With LLM enhancement
34
- releasekit-notes --input version-data.json --llm-provider openai --llm-model gpt-4o-mini
35
-
36
39
  # Preview without writing
37
40
  releasekit-notes --dry-run
41
+
42
+ # With LLM enhancement
43
+ releasekit-notes --input version-data.json \
44
+ --llm-provider openai \
45
+ --llm-model gpt-4o-mini \
46
+ --llm-tasks enhance,summarize
38
47
  ```
39
48
 
40
49
  ## CLI Reference
41
50
 
51
+ ### `releasekit-notes generate` (default)
52
+
42
53
  | Flag | Description | Default |
43
54
  |------|-------------|---------|
44
55
  | `-i, --input <file>` | Input file path | stdin |
45
- | `-o, --output <spec>` | Output spec (`format:file`) | config |
56
+ | `--changelog-mode <mode>` | Changelog location: `root`, `packages`, `both` | `root` |
57
+ | `--changelog-file <name>` | Changelog file name override | `CHANGELOG.md` |
58
+ | `--no-changelog` | Disable changelog generation | — |
59
+ | `--release-notes-mode <mode>` | Enable release notes file output: `root`, `packages`, `both` | — |
60
+ | `--release-notes-file <name>` | Release notes file name override | `RELEASE_NOTES.md` |
61
+ | `--no-release-notes` | Disable release notes generation | — |
46
62
  | `-t, --template <path>` | Template file or directory | built-in |
47
63
  | `-e, --engine <engine>` | Template engine: `handlebars`, `liquid`, `ejs` | `liquid` |
48
64
  | `--monorepo <mode>` | Monorepo mode: `root`, `packages`, `both` | — |
49
65
  | `--llm-provider <name>` | LLM provider | — |
50
66
  | `--llm-model <model>` | LLM model | — |
51
- | `--llm-tasks <tasks>` | Comma-separated LLM tasks | — |
52
- | `--no-llm` | Disable LLM processing | `false` |
67
+ | `--llm-base-url <url>` | Base URL for openai-compatible providers | — |
68
+ | `--llm-tasks <tasks>` | Comma-separated tasks: `enhance`, `summarize`, `categorize`, `release-notes` | — |
69
+ | `--no-llm` | Disable LLM processing | — |
70
+ | `--target <package>` | Filter to a specific package name | — |
53
71
  | `--config <path>` | Config file path | `releasekit.config.json` |
72
+ | `--regenerate` | Regenerate entire file instead of prepending | `false` |
54
73
  | `--dry-run` | Preview without writing | `false` |
55
- | `--regenerate` | Regenerate entire changelog | `false` |
56
- | `-v, --verbose` | Verbose logging | `false` |
57
- | `-q, --quiet` | Suppress non-error output | `false` |
58
-
59
- ## Subcommands
60
-
61
- ### `releasekit-notes init`
62
-
63
- Create a default configuration file.
64
-
65
- ```bash
66
- releasekit-notes init [--force]
67
- ```
74
+ | `-v, --verbose` | Verbose logging (repeat for more: `-vv`) | — |
75
+ | `-q, --quiet` | Suppress non-error output | |
68
76
 
69
77
  ### `releasekit-notes auth <provider>`
70
78
 
71
- Configure API key for an LLM provider.
79
+ Store an API key for an LLM provider.
72
80
 
73
81
  ```bash
74
- releasekit-notes auth openai --key sk-...
75
- releasekit-notes auth anthropic
82
+ releasekit-notes auth openai
83
+ releasekit-notes auth anthropic --key sk-ant-...
76
84
  ```
77
85
 
86
+ Keys are saved to `~/.config/releasekit/auth.json`.
87
+
78
88
  ### `releasekit-notes providers`
79
89
 
80
90
  List available LLM providers.
81
91
 
82
92
  ## Configuration
83
93
 
84
- Configure via `releasekit.config.json`:
94
+ All options live under the `notes` key in `releasekit.config.json`:
85
95
 
86
96
  ```json
87
97
  {
98
+ "$schema": "https://goosewobbler.github.io/releasekit/schema.json",
88
99
  "notes": {
89
- "output": [
90
- { "format": "markdown", "file": "CHANGELOG.md" }
91
- ],
92
- "updateStrategy": "prepend",
93
- "templates": {
94
- "path": "./templates/",
95
- "engine": "liquid"
100
+ "changelog": {
101
+ "mode": "root",
102
+ "file": "CHANGELOG.md",
103
+ "templates": {
104
+ "path": "./templates/changelog/",
105
+ "engine": "liquid"
106
+ }
96
107
  },
97
- "llm": {
98
- "provider": "openai",
99
- "model": "gpt-4o-mini",
100
- "tasks": {
101
- "summarize": true,
102
- "enhance": true
108
+ "releaseNotes": {
109
+ "mode": "root",
110
+ "llm": {
111
+ "provider": "openai",
112
+ "model": "gpt-4o-mini",
113
+ "tasks": {
114
+ "enhance": true,
115
+ "summarize": true
116
+ }
103
117
  }
104
- }
118
+ },
119
+ "updateStrategy": "prepend"
105
120
  }
106
121
  }
107
122
  ```
108
123
 
124
+ `changelog` and `releaseNotes` are configured independently. Set either to `false` to disable it entirely.
125
+
109
126
  ## LLM Providers
110
127
 
111
- | Provider | Config Key | Notes |
112
- |----------|------------|-------|
113
- | OpenAI | `openai` | Requires `OPENAI_API_KEY` |
114
- | Anthropic | `anthropic` | Requires `ANTHROPIC_API_KEY` |
115
- | Ollama | `ollama` | Local, no API key needed |
116
- | OpenAI-Compatible | `openai-compatible` | Any OpenAI-compatible endpoint |
128
+ LLM configuration lives under `notes.releaseNotes.llm`:
129
+
130
+ | Provider | Key | Auth |
131
+ |----------|-----|------|
132
+ | OpenAI | `openai` | `OPENAI_API_KEY` or `releasekit-notes auth openai` |
133
+ | Anthropic | `anthropic` | `ANTHROPIC_API_KEY` or `releasekit-notes auth anthropic` |
134
+ | Ollama | `ollama` | None (local) |
135
+ | OpenAI-compatible | `openai-compatible` | Varies — set `baseURL` and `apiKey` |
117
136
 
118
137
  ### LLM Tasks
119
138
 
120
- | Task | Description |
139
+ | Task | What it does |
121
140
  |------|-------------|
122
- | `enhance` | Improve entry descriptions |
123
- | `summarize` | Create version summary |
124
- | `categorize` | Group entries by category |
125
- | `releaseNotes` | Generate release notes |
141
+ | `enhance` | Rewrites each changelog entry description to be clearer |
142
+ | `summarize` | Generates a one-paragraph summary of the release |
143
+ | `categorize` | Groups entries into user-friendly categories (Features, Fixes, …) |
144
+ | `releaseNotes` | Generates full prose release notes (use as GitHub release body) |
126
145
 
127
146
  ## Templates
128
147
 
129
- ### Built-in
148
+ ### Built-in Templates
130
149
 
131
- - `keep-a-changelog` Keep a Changelog format (default)
132
- - `angular` — Angular-style changelog
133
- - `github-release` GitHub release notes
150
+ | Name | Engine | Description |
151
+ |------|--------|-------------|
152
+ | `keep-a-changelog` | Liquid | [Keep a Changelog](https://keepachangelog.com) format (default) |
153
+ | `angular` | Handlebars | Angular-style changelog |
154
+ | `github-release` | EJS | GitHub release notes format |
134
155
 
135
156
  ### Custom Templates
136
157
 
@@ -138,32 +159,37 @@ Configure via `releasekit.config.json`:
138
159
  # Single file
139
160
  releasekit-notes --template ./my-changelog.liquid
140
161
 
141
- # Composable directory
162
+ # Composable directory (document + version + entry)
142
163
  releasekit-notes --template ./templates/
143
164
  ```
144
165
 
145
- Composable directory structure:
146
-
147
- ```
148
- templates/
149
- ├── document.liquid
150
- ├── version.liquid
151
- └── entry.liquid
152
- ```
166
+ See **[Templates guide](./docs/templates.md)** for the full template context reference and authoring guide.
153
167
 
154
168
  ## Monorepo Support
155
169
 
156
170
  ```bash
157
171
  # Root changelog only (aggregates all packages)
158
- releasekit-notes --monorepo root
172
+ releasekit-notes --changelog-mode root
159
173
 
160
174
  # Per-package changelogs
161
- releasekit-notes --monorepo packages
175
+ releasekit-notes --changelog-mode packages
162
176
 
163
- # Both
164
- releasekit-notes --monorepo both
177
+ # Both root and per-package
178
+ releasekit-notes --changelog-mode both
165
179
  ```
166
180
 
181
+ See **[Monorepo guide](./docs/monorepo.md)** for details on file placement and aggregation behaviour.
182
+
183
+ ## Documentation
184
+
185
+ **Getting Started**
186
+ - [Configuration reference](./docs/configuration.md) — all `notes.*` options
187
+ - [LLM providers](./docs/llm-providers.md) — provider setup, auth, tasks, prompt customisation
188
+
189
+ **Guides**
190
+ - [Templates](./docs/templates.md) — custom template authoring and context reference
191
+ - [Monorepo](./docs/monorepo.md) — per-package and root output modes
192
+
167
193
  ## License
168
194
 
169
195
  MIT
@@ -0,0 +1,13 @@
1
+ import {
2
+ aggregateToRoot,
3
+ detectMonorepo,
4
+ splitByPackage,
5
+ writeMonorepoChangelogs
6
+ } from "./chunk-F7MUVHZ2.js";
7
+ import "./chunk-7TJSPQPW.js";
8
+ export {
9
+ aggregateToRoot,
10
+ detectMonorepo,
11
+ splitByPackage,
12
+ writeMonorepoChangelogs
13
+ };
@@ -0,0 +1,243 @@
1
+ // ../core/dist/index.js
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import chalk from "chalk";
6
+ function readPackageVersion(importMetaUrl) {
7
+ try {
8
+ const dir = path.dirname(fileURLToPath(importMetaUrl));
9
+ const packageJsonPath = path.resolve(dir, "../package.json");
10
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
11
+ return packageJson.version ?? "0.0.0";
12
+ } catch {
13
+ return "0.0.0";
14
+ }
15
+ }
16
+ var LOG_LEVELS = {
17
+ error: 0,
18
+ warn: 1,
19
+ info: 2,
20
+ debug: 3,
21
+ trace: 4
22
+ };
23
+ var PREFIXES = {
24
+ error: "[ERROR]",
25
+ warn: "[WARN]",
26
+ info: "[INFO]",
27
+ debug: "[DEBUG]",
28
+ trace: "[TRACE]"
29
+ };
30
+ var COLORS = {
31
+ error: chalk.red,
32
+ warn: chalk.yellow,
33
+ info: chalk.blue,
34
+ debug: chalk.gray,
35
+ trace: chalk.dim
36
+ };
37
+ var currentLevel = "info";
38
+ var quietMode = false;
39
+ function setLogLevel(level) {
40
+ currentLevel = level;
41
+ }
42
+ function setQuietMode(quiet) {
43
+ quietMode = quiet;
44
+ }
45
+ function shouldLog(level) {
46
+ if (quietMode && level !== "error") return false;
47
+ return LOG_LEVELS[level] <= LOG_LEVELS[currentLevel];
48
+ }
49
+ function log(message, level = "info") {
50
+ if (!shouldLog(level)) return;
51
+ const formatted = COLORS[level](`${PREFIXES[level]} ${message}`);
52
+ console.error(formatted);
53
+ }
54
+ function error(message) {
55
+ log(message, "error");
56
+ }
57
+ function warn(message) {
58
+ log(message, "warn");
59
+ }
60
+ function info(message) {
61
+ log(message, "info");
62
+ }
63
+ function success(message) {
64
+ if (!shouldLog("info")) return;
65
+ console.error(chalk.green(`[SUCCESS] ${message}`));
66
+ }
67
+ function debug(message) {
68
+ log(message, "debug");
69
+ }
70
+ var ReleaseKitError = class _ReleaseKitError extends Error {
71
+ constructor(message) {
72
+ super(message);
73
+ this.name = this.constructor.name;
74
+ }
75
+ logError() {
76
+ log(this.message, "error");
77
+ if (this.suggestions.length > 0) {
78
+ log("\nSuggested solutions:", "info");
79
+ for (const [i, suggestion] of this.suggestions.entries()) {
80
+ log(`${i + 1}. ${suggestion}`, "info");
81
+ }
82
+ }
83
+ }
84
+ static isReleaseKitError(error2) {
85
+ return error2 instanceof _ReleaseKitError;
86
+ }
87
+ };
88
+ var EXIT_CODES = {
89
+ SUCCESS: 0,
90
+ GENERAL_ERROR: 1,
91
+ CONFIG_ERROR: 2,
92
+ INPUT_ERROR: 3,
93
+ TEMPLATE_ERROR: 4,
94
+ LLM_ERROR: 5,
95
+ GITHUB_ERROR: 6,
96
+ GIT_ERROR: 7,
97
+ VERSION_ERROR: 8,
98
+ PUBLISH_ERROR: 9
99
+ };
100
+
101
+ // src/output/markdown.ts
102
+ import * as fs2 from "fs";
103
+ import * as path2 from "path";
104
+ var TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
105
+ var TYPE_LABELS = {
106
+ added: "Added",
107
+ changed: "Changed",
108
+ deprecated: "Deprecated",
109
+ removed: "Removed",
110
+ fixed: "Fixed",
111
+ security: "Security"
112
+ };
113
+ function groupEntriesByType(entries) {
114
+ const grouped = /* @__PURE__ */ new Map();
115
+ for (const type of TYPE_ORDER) {
116
+ grouped.set(type, []);
117
+ }
118
+ for (const entry of entries) {
119
+ const existing = grouped.get(entry.type) ?? [];
120
+ existing.push(entry);
121
+ grouped.set(entry.type, existing);
122
+ }
123
+ return grouped;
124
+ }
125
+ function formatEntry(entry) {
126
+ let line;
127
+ if (entry.breaking && entry.scope) {
128
+ line = `- **BREAKING** **${entry.scope}**: ${entry.description}`;
129
+ } else if (entry.breaking) {
130
+ line = `- **BREAKING** ${entry.description}`;
131
+ } else if (entry.scope) {
132
+ line = `- **${entry.scope}**: ${entry.description}`;
133
+ } else {
134
+ line = `- ${entry.description}`;
135
+ }
136
+ if (entry.issueIds && entry.issueIds.length > 0) {
137
+ line += ` (${entry.issueIds.join(", ")})`;
138
+ }
139
+ return line;
140
+ }
141
+ function formatVersion(context, options) {
142
+ const lines = [];
143
+ const versionLabel = options?.includePackageName && context.packageName ? `${context.packageName}@${context.version}` : context.version;
144
+ const versionHeader = context.previousVersion ? `## [${versionLabel}]` : `## ${versionLabel}`;
145
+ lines.push(`${versionHeader} - ${context.date}`);
146
+ lines.push("");
147
+ if (context.compareUrl) {
148
+ lines.push(`[Full Changelog](${context.compareUrl})`);
149
+ lines.push("");
150
+ }
151
+ if (context.enhanced?.summary) {
152
+ lines.push(context.enhanced.summary);
153
+ lines.push("");
154
+ }
155
+ const grouped = groupEntriesByType(context.entries);
156
+ for (const [type, entries] of grouped) {
157
+ if (entries.length === 0) continue;
158
+ lines.push(`### ${TYPE_LABELS[type]}`);
159
+ for (const entry of entries) {
160
+ lines.push(formatEntry(entry));
161
+ }
162
+ lines.push("");
163
+ }
164
+ return lines.join("\n");
165
+ }
166
+ function formatHeader() {
167
+ return `# Changelog
168
+
169
+ All notable changes to this project will be documented in this file.
170
+
171
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
172
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
173
+
174
+ `;
175
+ }
176
+ function renderMarkdown(contexts, options) {
177
+ const sections = [formatHeader()];
178
+ for (const context of contexts) {
179
+ sections.push(formatVersion(context, options));
180
+ }
181
+ return sections.join("\n");
182
+ }
183
+ function prependVersion(existingPath, context, options) {
184
+ let existing = "";
185
+ if (fs2.existsSync(existingPath)) {
186
+ existing = fs2.readFileSync(existingPath, "utf-8");
187
+ const headerEnd = existing.indexOf("\n## ");
188
+ if (headerEnd >= 0) {
189
+ const header = existing.slice(0, headerEnd);
190
+ const body = existing.slice(headerEnd + 1);
191
+ const newVersion = formatVersion(context, options);
192
+ return `${header}
193
+
194
+ ${newVersion}
195
+ ${body}`;
196
+ }
197
+ }
198
+ return renderMarkdown([context]);
199
+ }
200
+ function writeMarkdown(outputPath, contexts, config, dryRun, options) {
201
+ const content = renderMarkdown(contexts, options);
202
+ const label = /changelog/i.test(outputPath) ? "Changelog" : "Release notes";
203
+ if (dryRun) {
204
+ info(`[DRY RUN] ${label} preview (would write to ${outputPath}):`);
205
+ info(content);
206
+ return;
207
+ }
208
+ const dir = path2.dirname(outputPath);
209
+ if (!fs2.existsSync(dir)) {
210
+ fs2.mkdirSync(dir, { recursive: true });
211
+ }
212
+ if (outputPath === "-") {
213
+ process.stdout.write(content);
214
+ return;
215
+ }
216
+ if (config.updateStrategy !== "regenerate" && fs2.existsSync(outputPath) && contexts.length === 1) {
217
+ const firstContext = contexts[0];
218
+ if (firstContext) {
219
+ const updated = prependVersion(outputPath, firstContext, options);
220
+ fs2.writeFileSync(outputPath, updated, "utf-8");
221
+ }
222
+ } else {
223
+ fs2.writeFileSync(outputPath, content, "utf-8");
224
+ }
225
+ success(`${label} written to ${outputPath}`);
226
+ }
227
+
228
+ export {
229
+ readPackageVersion,
230
+ setLogLevel,
231
+ setQuietMode,
232
+ error,
233
+ warn,
234
+ info,
235
+ success,
236
+ debug,
237
+ ReleaseKitError,
238
+ EXIT_CODES,
239
+ formatVersion,
240
+ renderMarkdown,
241
+ prependVersion,
242
+ writeMarkdown
243
+ };
@@ -0,0 +1,165 @@
1
+ import {
2
+ debug,
3
+ formatVersion,
4
+ info,
5
+ prependVersion,
6
+ renderMarkdown,
7
+ success
8
+ } from "./chunk-7TJSPQPW.js";
9
+
10
+ // src/monorepo/aggregator.ts
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+
14
+ // src/monorepo/splitter.ts
15
+ function splitByPackage(contexts) {
16
+ const byPackage = /* @__PURE__ */ new Map();
17
+ for (const ctx of contexts) {
18
+ byPackage.set(ctx.packageName, ctx);
19
+ }
20
+ return byPackage;
21
+ }
22
+
23
+ // src/monorepo/aggregator.ts
24
+ function writeFile(outputPath, content, dryRun) {
25
+ if (dryRun) {
26
+ info(`Would write to ${outputPath}`);
27
+ debug(content);
28
+ return false;
29
+ }
30
+ const dir = path.dirname(outputPath);
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+ fs.writeFileSync(outputPath, content, "utf-8");
35
+ success(`Changelog written to ${outputPath}`);
36
+ return true;
37
+ }
38
+ function aggregateToRoot(contexts) {
39
+ const aggregated = {
40
+ packageName: "monorepo",
41
+ version: contexts[0]?.version ?? "0.0.0",
42
+ previousVersion: contexts[0]?.previousVersion ?? null,
43
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "",
44
+ repoUrl: contexts[0]?.repoUrl ?? null,
45
+ entries: []
46
+ };
47
+ for (const ctx of contexts) {
48
+ for (const entry of ctx.entries) {
49
+ aggregated.entries.push({
50
+ ...entry,
51
+ scope: entry.scope ? `${ctx.packageName}/${entry.scope}` : ctx.packageName
52
+ });
53
+ }
54
+ }
55
+ return aggregated;
56
+ }
57
+ function writeMonorepoChangelogs(contexts, options, config, dryRun) {
58
+ const files = [];
59
+ if (options.mode === "root" || options.mode === "both") {
60
+ const rootPath = path.join(options.rootPath, options.fileName ?? "CHANGELOG.md");
61
+ const fmtOpts = { includePackageName: true };
62
+ info(`Writing root changelog to ${rootPath}`);
63
+ let rootContent;
64
+ if (config.updateStrategy !== "regenerate" && fs.existsSync(rootPath)) {
65
+ const newSections = contexts.map((ctx) => formatVersion(ctx, fmtOpts)).join("\n");
66
+ const existing = fs.readFileSync(rootPath, "utf-8");
67
+ const headerEnd = existing.indexOf("\n## ");
68
+ if (headerEnd >= 0) {
69
+ rootContent = `${existing.slice(0, headerEnd)}
70
+
71
+ ${newSections}
72
+ ${existing.slice(headerEnd + 1)}`;
73
+ } else {
74
+ rootContent = renderMarkdown(contexts, fmtOpts);
75
+ }
76
+ } else {
77
+ rootContent = renderMarkdown(contexts, fmtOpts);
78
+ }
79
+ if (writeFile(rootPath, rootContent, dryRun)) {
80
+ files.push(rootPath);
81
+ }
82
+ }
83
+ if (options.mode === "packages" || options.mode === "both") {
84
+ const byPackage = splitByPackage(contexts);
85
+ const packageDirMap = buildPackageDirMap(options.rootPath, options.packagesPath);
86
+ for (const [packageName, ctx] of byPackage) {
87
+ const simpleName = packageName.split("/").pop();
88
+ const packageDir = packageDirMap.get(packageName) ?? (simpleName ? packageDirMap.get(simpleName) : void 0) ?? null;
89
+ if (packageDir) {
90
+ const changelogPath = path.join(packageDir, options.fileName ?? "CHANGELOG.md");
91
+ info(`Writing changelog for ${packageName} to ${changelogPath}`);
92
+ const pkgContent = config.updateStrategy !== "regenerate" && fs.existsSync(changelogPath) ? prependVersion(changelogPath, ctx) : renderMarkdown([ctx]);
93
+ if (writeFile(changelogPath, pkgContent, dryRun)) {
94
+ files.push(changelogPath);
95
+ }
96
+ } else {
97
+ info(`Could not find directory for package ${packageName}, skipping`);
98
+ }
99
+ }
100
+ }
101
+ return files;
102
+ }
103
+ function buildPackageDirMap(rootPath, packagesPath) {
104
+ const map = /* @__PURE__ */ new Map();
105
+ const packagesDir = path.join(rootPath, packagesPath);
106
+ if (!fs.existsSync(packagesDir)) {
107
+ return map;
108
+ }
109
+ for (const entry of fs.readdirSync(packagesDir, { withFileTypes: true })) {
110
+ if (!entry.isDirectory()) continue;
111
+ const dirPath = path.join(packagesDir, entry.name);
112
+ map.set(entry.name, dirPath);
113
+ const packageJsonPath = path.join(dirPath, "package.json");
114
+ if (fs.existsSync(packageJsonPath)) {
115
+ try {
116
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
117
+ if (pkg.name) {
118
+ map.set(pkg.name, dirPath);
119
+ }
120
+ } catch {
121
+ }
122
+ }
123
+ }
124
+ return map;
125
+ }
126
+ function detectMonorepo(cwd) {
127
+ const pnpmWorkspacesPath = path.join(cwd, "pnpm-workspace.yaml");
128
+ const packageJsonPath = path.join(cwd, "package.json");
129
+ if (fs.existsSync(pnpmWorkspacesPath)) {
130
+ const content = fs.readFileSync(pnpmWorkspacesPath, "utf-8");
131
+ const packagesMatch = content.match(/packages:\s*\n\s*-\s*['"]([^'"]+)['"]/);
132
+ if (packagesMatch?.[1]) {
133
+ const packagesGlob = packagesMatch[1];
134
+ const packagesPath = packagesGlob.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
135
+ return { isMonorepo: true, packagesPath: packagesPath || "packages" };
136
+ }
137
+ return { isMonorepo: true, packagesPath: "packages" };
138
+ }
139
+ if (fs.existsSync(packageJsonPath)) {
140
+ try {
141
+ const content = fs.readFileSync(packageJsonPath, "utf-8");
142
+ const pkg = JSON.parse(content);
143
+ if (pkg.workspaces) {
144
+ const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
145
+ if (workspaces?.length) {
146
+ const firstWorkspace = workspaces[0];
147
+ if (firstWorkspace) {
148
+ const packagesPath = firstWorkspace.replace(/\/?\*$/, "").replace(/\/\*\*$/, "");
149
+ return { isMonorepo: true, packagesPath: packagesPath || "packages" };
150
+ }
151
+ }
152
+ }
153
+ } catch {
154
+ return { isMonorepo: false, packagesPath: "" };
155
+ }
156
+ }
157
+ return { isMonorepo: false, packagesPath: "" };
158
+ }
159
+
160
+ export {
161
+ splitByPackage,
162
+ aggregateToRoot,
163
+ writeMonorepoChangelogs,
164
+ detectMonorepo
165
+ };