keepchanges 0.0.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/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/cli.d.mts +14 -0
- package/dist/cli.mjs +433 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 edram
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# keepchanges
|
|
2
|
+
|
|
3
|
+
Generate and maintain `CHANGELOG.md` from Conventional Commits.
|
|
4
|
+
|
|
5
|
+
[中文](#中文) · [English](#english)
|
|
6
|
+
|
|
7
|
+
## 中文
|
|
8
|
+
|
|
9
|
+
### 功能
|
|
10
|
+
|
|
11
|
+
- 根据最近的 Git tag 到 `HEAD` 之间的 Conventional Commits 生成变更记录
|
|
12
|
+
- 首次发布时读取完整 Git 历史
|
|
13
|
+
- 将新版本插入 `CHANGELOG.md` 的已有内容之前
|
|
14
|
+
- 为 npm 项目同步更新 `package.json` 中的 `version`
|
|
15
|
+
- 为提交、对比页面、作者和共同作者生成 GitHub 信息
|
|
16
|
+
- 可选择自动提交、创建 tag、推送并发布 GitHub Release
|
|
17
|
+
- 没有 GitHub token 时提供预填内容的手动发布链接
|
|
18
|
+
|
|
19
|
+
当前会收录 `feat`、`fix`、带 `!` 的破坏性变更,以及包含
|
|
20
|
+
`BREAKING CHANGE` 或 `BREAKING-CHANGE` trailer 的提交。其他提交类型会被忽略。
|
|
21
|
+
|
|
22
|
+
### 快速开始
|
|
23
|
+
|
|
24
|
+
需要 Node.js 20.19.0 或更高版本。
|
|
25
|
+
|
|
26
|
+
在 Git 仓库根目录运行,并传入要发布的版本号:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx keepchanges 1.1.0
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
也可以使用带 `v` 前缀的版本号:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx keepchanges v1.1.0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
默认行为会写入 `CHANGELOG.md`。如果当前项目是 npm 包,还会将
|
|
39
|
+
`package.json#version` 更新为 `1.1.0`,但不会自动创建 Git commit。
|
|
40
|
+
|
|
41
|
+
### 命令格式
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
npx keepchanges <version> [options]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### 参数
|
|
48
|
+
|
|
49
|
+
| 参数 | 默认值 | 说明 |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `<version>` | 必填 | 要生成的版本号。可以传入 `1.1.0` 或 `v1.1.0`;生成的标题和 tag 统一使用 `v1.1.0`。包含 `-` 的版本会作为预发布版本,例如 `1.1.0-beta.1`。 |
|
|
52
|
+
| `--output <path>` | `CHANGELOG.md` | 指定 changelog 文件路径。相对路径以当前工作目录为基准。 |
|
|
53
|
+
| `--dry` | `false` | 只预览,不写入 changelog 或版本文件。单独使用时输出完整 changelog;与 `--release` 一起使用时输出 Release notes 和手动发布链接。 |
|
|
54
|
+
| `--commit` | `false` | 写入文件后创建 Git commit。只提交 changelog 和检测到的版本文件,默认提交信息为 `chore(release): v<version>`。 |
|
|
55
|
+
| `--release` | `false` | 执行完整发布流程:写入文件、创建或复用 release commit、创建 annotated tag、推送 `HEAD` 和 tag,然后创建或更新仓库 Release。该参数隐含 `--commit`。 |
|
|
56
|
+
| `--author <author>` | Git 默认作者 | 设置自动创建的 release commit 作者,格式必须为 `"Name <email>"`。只影响作者,不改变仓库配置的 committer;需要与 `--commit` 或 `--release` 一起使用。 |
|
|
57
|
+
| `--token <token>` | 环境变量 | 仓库访问令牌,用于解析 GitHub 用户名以及创建或更新 GitHub Release。优先级为 `--token`、`GITHUB_TOKEN`、`GH_TOKEN`。 |
|
|
58
|
+
|
|
59
|
+
### 常见用法
|
|
60
|
+
|
|
61
|
+
指定输出文件:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx keepchanges 1.1.0 --output docs/CHANGELOG.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
预览完整 changelog,不修改文件:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx keepchanges 1.1.0 --dry
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
写入 changelog、更新版本并创建 commit:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx keepchanges 1.1.0 --commit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
指定 release commit 的作者:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx keepchanges 1.1.0 --commit \
|
|
83
|
+
--author "Release Author <release@example.com>"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
创建 GitHub Release:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
预览 Release,不写文件、不提交、不创建 tag、不推送且不调用发布 API:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx keepchanges 1.1.0 --release --dry
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 生成和发布行为
|
|
99
|
+
|
|
100
|
+
仓库地址优先读取 `package.json#repository`,不存在时读取 Git 的 `origin`。
|
|
101
|
+
当前支持 GitHub。识别到仓库后,每条记录会包含 commit 链接,末尾会包含版本对比链接。
|
|
102
|
+
|
|
103
|
+
默认使用 Git 提交中的作者名,并将 `Co-Authored-By` 参与者一起写入记录,bot
|
|
104
|
+
账号会被忽略。提供 token 后,会通过 GitHub 尝试将邮箱解析为用户名。
|
|
105
|
+
|
|
106
|
+
`--commit` 只提交 changelog 和检测到的版本文件。其他已暂存或未暂存的改动会
|
|
107
|
+
保持原状。
|
|
108
|
+
|
|
109
|
+
`--release` 在 tag 不存在时:
|
|
110
|
+
|
|
111
|
+
1. 写入 changelog 并更新版本文件。
|
|
112
|
+
2. 创建或复用 release commit。
|
|
113
|
+
3. 创建 annotated tag。
|
|
114
|
+
4. 将 `HEAD` 和 tag 推送到 `origin`。
|
|
115
|
+
5. 创建 GitHub Release;没有 token 时输出手动发布链接。
|
|
116
|
+
|
|
117
|
+
`--release` 在 tag 已存在时不会移动 tag。它会根据前一个版本到现有 tag
|
|
118
|
+
重新生成 Release notes,并创建或更新 Release。只有远程存在 tag 时会先将它
|
|
119
|
+
拉取到本地;只有本地存在 tag 时会推送该 tag。本地与远程 tag 指向不同 commit
|
|
120
|
+
时会停止,不会强制覆盖。
|
|
121
|
+
|
|
122
|
+
稳定版本会与前一个稳定 tag 对比,预发布版本会与最近的前一个 tag 对比。与
|
|
123
|
+
`--release` 搭配的 `--dry` 优先级最高,不会执行任何写入或远程修改。
|
|
124
|
+
|
|
125
|
+
> [!NOTE]
|
|
126
|
+
> 没有 token 的普通 `--release` 仍会执行本地写入、commit、tag 和 push,
|
|
127
|
+
> 然后提供手动创建 GitHub Release 的链接。如需完全无副作用的预览,请使用
|
|
128
|
+
> `--release --dry`。
|
|
129
|
+
|
|
130
|
+
## English
|
|
131
|
+
|
|
132
|
+
### Features
|
|
133
|
+
|
|
134
|
+
- Generates release notes from Conventional Commits between the latest Git tag and `HEAD`
|
|
135
|
+
- Reads the complete Git history for a first release
|
|
136
|
+
- Inserts a new release before existing content in `CHANGELOG.md`
|
|
137
|
+
- Updates `package.json#version` for npm projects
|
|
138
|
+
- Adds GitHub commit links, comparison links, authors, and co-authors
|
|
139
|
+
- Can commit, tag, push, and publish a GitHub Release
|
|
140
|
+
- Provides a prefilled manual release URL when no GitHub token is available
|
|
141
|
+
|
|
142
|
+
The current release includes `feat`, `fix`, breaking changes marked with `!`,
|
|
143
|
+
and commits with a `BREAKING CHANGE` or `BREAKING-CHANGE` trailer. Other commit
|
|
144
|
+
types are ignored.
|
|
145
|
+
|
|
146
|
+
### Quick start
|
|
147
|
+
|
|
148
|
+
Node.js 20.19.0 or later is required.
|
|
149
|
+
|
|
150
|
+
Run the CLI at the root of a Git repository and provide the version to release:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx keepchanges 1.1.0
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The version may also include a leading `v`:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npx keepchanges v1.1.0
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
By default, the command writes `CHANGELOG.md`. For an npm package, it also
|
|
163
|
+
updates `package.json#version` to `1.1.0`, but it does not create a Git commit.
|
|
164
|
+
|
|
165
|
+
### Command
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
npx keepchanges <version> [options]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Arguments and options
|
|
172
|
+
|
|
173
|
+
| Argument or option | Default | Description |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `<version>` | Required | Version to generate. Accepts `1.1.0` or `v1.1.0`; headings and tags use `v1.1.0`. A version containing `-`, such as `1.1.0-beta.1`, is treated as a prerelease. |
|
|
176
|
+
| `--output <path>` | `CHANGELOG.md` | Sets the changelog file path. Relative paths are resolved from the current working directory. |
|
|
177
|
+
| `--dry` | `false` | Previews without writing the changelog or version file. By itself it prints the complete changelog; with `--release` it prints the Release notes and manual release URL. |
|
|
178
|
+
| `--commit` | `false` | Creates a Git commit after writing. It commits only the changelog and detected version file, using `chore(release): v<version>` by default. |
|
|
179
|
+
| `--release` | `false` | Runs the complete release flow: writes files, creates or reuses a release commit, creates an annotated tag, pushes `HEAD` and the tag, then creates or updates the repository Release. This implies `--commit`. |
|
|
180
|
+
| `--author <author>` | Git default | Sets the author of the generated release commit in `"Name <email>"` format. It does not change the configured committer and only applies with `--commit` or `--release`. |
|
|
181
|
+
| `--token <token>` | Environment | Repository token used to resolve GitHub usernames and create or update a GitHub Release. Precedence is `--token`, `GITHUB_TOKEN`, then `GH_TOKEN`. |
|
|
182
|
+
|
|
183
|
+
### Examples
|
|
184
|
+
|
|
185
|
+
Write to a different file:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx keepchanges 1.1.0 --output docs/CHANGELOG.md
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Preview the complete changelog without changing files:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx keepchanges 1.1.0 --dry
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Write the changelog, update the version, and create a commit:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npx keepchanges 1.1.0 --commit
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Set the release commit author:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx keepchanges 1.1.0 --commit \
|
|
207
|
+
--author "Release Author <release@example.com>"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Create a GitHub Release:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Preview a Release without writing, committing, tagging, pushing, or calling the
|
|
217
|
+
release API:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npx keepchanges 1.1.0 --release --dry
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Generation and release behavior
|
|
224
|
+
|
|
225
|
+
The repository URL is read from `package.json#repository`, then from the Git
|
|
226
|
+
`origin`. GitHub is currently supported. When a repository is detected, each
|
|
227
|
+
entry includes a commit link and the release ends with a version comparison
|
|
228
|
+
link.
|
|
229
|
+
|
|
230
|
+
Entries use Git author names by default and include `Co-Authored-By`
|
|
231
|
+
participants. Bot accounts are omitted. With a token, the CLI asks GitHub to
|
|
232
|
+
resolve email addresses to usernames.
|
|
233
|
+
|
|
234
|
+
`--commit` commits only the changelog and detected version file. Other staged
|
|
235
|
+
and unstaged changes remain untouched.
|
|
236
|
+
|
|
237
|
+
When the tag does not exist, `--release`:
|
|
238
|
+
|
|
239
|
+
1. Writes the changelog and updates the version file.
|
|
240
|
+
2. Creates or reuses a release commit.
|
|
241
|
+
3. Creates an annotated tag.
|
|
242
|
+
4. Pushes `HEAD` and the tag to `origin`.
|
|
243
|
+
5. Creates a GitHub Release, or prints a manual release URL without a token.
|
|
244
|
+
|
|
245
|
+
When the tag already exists, `--release` never moves it. It regenerates Release
|
|
246
|
+
notes from the previous version to the existing tag and creates or updates the
|
|
247
|
+
Release. A remote-only tag is fetched, while a local-only tag is pushed. The
|
|
248
|
+
command stops without force-updating when local and remote tags point to
|
|
249
|
+
different commits.
|
|
250
|
+
|
|
251
|
+
Stable releases compare with the previous stable tag. Prereleases compare with
|
|
252
|
+
the nearest previous tag. With `--release`, `--dry` takes precedence and
|
|
253
|
+
prevents all file writes and remote mutations.
|
|
254
|
+
|
|
255
|
+
> [!NOTE]
|
|
256
|
+
> A regular `--release` without a token still writes files, commits, tags, and
|
|
257
|
+
> pushes before providing the manual GitHub Release URL. Use `--release --dry`
|
|
258
|
+
> for a completely non-mutating preview.
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
pnpm install
|
|
264
|
+
pnpm test
|
|
265
|
+
pnpm run typecheck
|
|
266
|
+
pnpm run build
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ansis from "ansis";
|
|
2
|
+
//#region src/run.d.ts
|
|
3
|
+
interface CliEnvironment {
|
|
4
|
+
cwd: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
stdout?: (value: string) => void;
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
colors?: typeof ansis;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/cli.d.ts
|
|
12
|
+
declare function runCli(args: string[], environment: CliEnvironment): Promise<void>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { type CliEnvironment, runCli };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { cac } from "cac";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import ansis from "ansis";
|
|
8
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { normalizeFull } from "verkit";
|
|
10
|
+
import { x } from "tinyexec";
|
|
11
|
+
//#region package.json
|
|
12
|
+
var version = "0.0.0";
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/changelog.ts
|
|
15
|
+
function createChangelog(version, commits, repository, comparisonFrom) {
|
|
16
|
+
const body = [
|
|
17
|
+
...renderSection(commits.filter((commit) => commit.isBreaking), "🚨 Breaking Changes", repository),
|
|
18
|
+
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "feat"), "🚀 Features", repository),
|
|
19
|
+
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "fix"), "🐞 Bug Fixes", repository),
|
|
20
|
+
...repository && comparisonFrom ? ["", `##### [View changes on ${repository.provider.name}](${repository.provider.compareUrl(repository, comparisonFrom, `v${version}`)})`] : []
|
|
21
|
+
].join("\n").trim();
|
|
22
|
+
return {
|
|
23
|
+
body,
|
|
24
|
+
release: [
|
|
25
|
+
`## v${version}`,
|
|
26
|
+
"",
|
|
27
|
+
body,
|
|
28
|
+
""
|
|
29
|
+
].join("\n")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function readChangelog(path) {
|
|
33
|
+
return readFile(path, "utf8").catch((error) => {
|
|
34
|
+
if (error.code === "ENOENT") return "# Changelog\n";
|
|
35
|
+
throw error;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function writeChangelog(path, changelog) {
|
|
39
|
+
return writeFile(path, changelog);
|
|
40
|
+
}
|
|
41
|
+
function hasRelease(changelog, version) {
|
|
42
|
+
const releaseVersion = normalizeFull(version);
|
|
43
|
+
return releaseHeadings(changelog).some((heading) => heading.version === releaseVersion);
|
|
44
|
+
}
|
|
45
|
+
function insertRelease(changelog, release) {
|
|
46
|
+
const releaseVersion = releaseHeadings(release)[0]?.version;
|
|
47
|
+
const headings = releaseHeadings(changelog);
|
|
48
|
+
const existingReleaseIndex = headings.findIndex((heading) => heading.version === releaseVersion);
|
|
49
|
+
if (existingReleaseIndex !== -1) {
|
|
50
|
+
const start = headings[existingReleaseIndex].index;
|
|
51
|
+
const end = headings[existingReleaseIndex + 1]?.index ?? changelog.length;
|
|
52
|
+
changelog = changelog.slice(0, start) + changelog.slice(end);
|
|
53
|
+
}
|
|
54
|
+
const firstRelease = releaseHeadings(changelog)[0];
|
|
55
|
+
if (!firstRelease) return `${changelog.trimEnd()}\n\n${release.trim()}\n`;
|
|
56
|
+
const preamble = changelog.slice(0, firstRelease.index).trimEnd();
|
|
57
|
+
const history = changelog.slice(firstRelease.index).trim();
|
|
58
|
+
return `${preamble}\n\n${release.trim()}\n\n${history}\n`;
|
|
59
|
+
}
|
|
60
|
+
function releaseHeadings(changelog) {
|
|
61
|
+
return [...changelog.matchAll(/^##\s+(\S+).*$/gm)].flatMap((heading) => {
|
|
62
|
+
const version = normalizeFull(heading[1]);
|
|
63
|
+
return version ? [{
|
|
64
|
+
index: heading.index,
|
|
65
|
+
version
|
|
66
|
+
}] : [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function renderSection(commits, title, repository) {
|
|
70
|
+
const lines = commits.map((commit) => {
|
|
71
|
+
const scope = commit.scope ? `**${escapeHtml(commit.scope)}**: ` : "";
|
|
72
|
+
const reference = repository ? `[<samp>(${commit.hash.slice(0, 5)})</samp>](${repository.provider.commitUrl(repository, commit.hash)})` : "";
|
|
73
|
+
const authorNames = commit.authors.map((author) => author.login ? `@${author.login}` : `**${escapeHtml(author.name)}**`);
|
|
74
|
+
const authors = authorNames.length > 1 ? `${authorNames.slice(0, -1).join(", ")} and ${authorNames.at(-1)}` : authorNames[0] || "";
|
|
75
|
+
const details = [authors ? `by ${authors}` : "", reference].filter(Boolean).join(" ");
|
|
76
|
+
const suffix = details ? ` - ${details}` : "";
|
|
77
|
+
return `${scope}${escapeHtml(capitalize$1(commit.description))}${suffix}`;
|
|
78
|
+
}).reverse();
|
|
79
|
+
if (!lines.length) return [];
|
|
80
|
+
return [
|
|
81
|
+
"",
|
|
82
|
+
`### ${title}`,
|
|
83
|
+
"",
|
|
84
|
+
...lines.map((line) => `- ${line}`)
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function capitalize$1(value) {
|
|
88
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
89
|
+
}
|
|
90
|
+
function escapeHtml(value) {
|
|
91
|
+
const htmlEntities = {
|
|
92
|
+
"&": "&",
|
|
93
|
+
"<": "<",
|
|
94
|
+
">": ">",
|
|
95
|
+
"\"": """,
|
|
96
|
+
"'": "'"
|
|
97
|
+
};
|
|
98
|
+
return value.replace(/[&<>"']/g, (character) => htmlEntities[character]);
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/git.ts
|
|
102
|
+
async function git(cwd, ...args) {
|
|
103
|
+
const result = await x("git", args, { nodeOptions: { cwd } });
|
|
104
|
+
if (result.exitCode !== 0) throw new Error(result.stderr.trim() || `Git exited with status ${result.exitCode}`);
|
|
105
|
+
return result.stdout;
|
|
106
|
+
}
|
|
107
|
+
async function getLatestTag(cwd) {
|
|
108
|
+
return (await x("git", [
|
|
109
|
+
"describe",
|
|
110
|
+
"--tags",
|
|
111
|
+
"--abbrev=0"
|
|
112
|
+
], { nodeOptions: { cwd } })).stdout.trim();
|
|
113
|
+
}
|
|
114
|
+
async function getTagCommit(cwd, tag) {
|
|
115
|
+
return git(cwd, "rev-list", "-n", "1", tag).then((output) => output.trim() || void 0).catch(() => void 0);
|
|
116
|
+
}
|
|
117
|
+
async function getPreviousTag(cwd, tag, releaseRef) {
|
|
118
|
+
if (!tag.includes("-")) {
|
|
119
|
+
const previousStable = (await git(cwd, "tag", "--merged", releaseRef, "--sort=-version:refname")).trim().split("\n").find((candidate) => candidate !== tag && /^v?\d+\.\d+\.\d+$/.test(candidate));
|
|
120
|
+
if (previousStable) return previousStable;
|
|
121
|
+
}
|
|
122
|
+
return git(cwd, "describe", "--tags", "--abbrev=0", `${releaseRef}^`).then((output) => output.trim()).catch(() => "");
|
|
123
|
+
}
|
|
124
|
+
async function getRemoteTagCommit(cwd, tag) {
|
|
125
|
+
const refs = (await git(cwd, "ls-remote", "--tags", "origin", `refs/tags/${tag}`, `refs/tags/${tag}^{}`)).trim().split("\n").filter(Boolean);
|
|
126
|
+
return (refs.find((line) => line.endsWith("^{}")) || refs[0])?.split(/\s+/)[0];
|
|
127
|
+
}
|
|
128
|
+
async function readCommits(cwd, from, to) {
|
|
129
|
+
return parseGitLog(await git(cwd, "log", from ? `${from}..${to}` : to, "--format=%h%x00%an%x00%ae%x00%s%x00%b%x00")).map((commit) => parseCommit(commit.hash, commit.subject, commit.body, commit.author)).filter((commit) => commit !== null);
|
|
130
|
+
}
|
|
131
|
+
function parseGitLog(log) {
|
|
132
|
+
const fields = log.split("\0");
|
|
133
|
+
const commits = [];
|
|
134
|
+
for (let index = 0; index + 4 < fields.length; index += 5) {
|
|
135
|
+
const subject = fields[index + 3].trim();
|
|
136
|
+
if (subject) commits.push({
|
|
137
|
+
hash: fields[index].trim(),
|
|
138
|
+
subject,
|
|
139
|
+
body: fields[index + 4],
|
|
140
|
+
author: {
|
|
141
|
+
name: fields[index + 1].trim(),
|
|
142
|
+
email: fields[index + 2].trim()
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return commits;
|
|
147
|
+
}
|
|
148
|
+
function parseCommit(hash, subject, body, author) {
|
|
149
|
+
const match = /^(?<type>[a-z]+)(?:\((?<scope>[^()\r\n]+)\))?(?<breaking>!)?: (?<description>.+)$/i.exec(subject);
|
|
150
|
+
if (!match?.groups) return null;
|
|
151
|
+
const authors = [author];
|
|
152
|
+
for (const coAuthor of body.matchAll(/^Co-Authored-By:\s*(.+?)\s*<([^>]+)>$/gim)) {
|
|
153
|
+
const email = coAuthor[2].trim();
|
|
154
|
+
if (!authors.some((author) => author.email === email)) authors.push({
|
|
155
|
+
name: coAuthor[1].trim(),
|
|
156
|
+
email
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
hash,
|
|
161
|
+
authors: authors.filter((author) => !/\[bot\]|dependabot|\(bot\)/i.test(author.name)),
|
|
162
|
+
type: match.groups.type.toLowerCase(),
|
|
163
|
+
scope: match.groups.scope || "",
|
|
164
|
+
description: match.groups.description,
|
|
165
|
+
isBreaking: Boolean(match.groups.breaking) || /^BREAKING(?: |-)CHANGE:/im.test(body)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/providers/github.ts
|
|
170
|
+
const githubProvider = {
|
|
171
|
+
name: "GitHub",
|
|
172
|
+
parse(source) {
|
|
173
|
+
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
|
|
174
|
+
if (!match) return;
|
|
175
|
+
return {
|
|
176
|
+
provider: githubProvider,
|
|
177
|
+
path: match[1],
|
|
178
|
+
webUrl: `https://github.com/${match[1]}`
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
token(explicit, env) {
|
|
182
|
+
return explicit || env.GITHUB_TOKEN || env.GH_TOKEN;
|
|
183
|
+
},
|
|
184
|
+
commitUrl(repository, hash) {
|
|
185
|
+
return `${repository.webUrl}/commit/${hash}`;
|
|
186
|
+
},
|
|
187
|
+
compareUrl(repository, from, to) {
|
|
188
|
+
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
189
|
+
},
|
|
190
|
+
manualReleaseUrl(repository, release) {
|
|
191
|
+
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
192
|
+
url.search = new URLSearchParams({
|
|
193
|
+
title: release.name,
|
|
194
|
+
body: release.body,
|
|
195
|
+
tag: release.tag,
|
|
196
|
+
prerelease: String(release.prerelease)
|
|
197
|
+
}).toString();
|
|
198
|
+
return url.toString();
|
|
199
|
+
},
|
|
200
|
+
async resolveAuthors(commits, repository, token, fetch) {
|
|
201
|
+
const headers = {
|
|
202
|
+
accept: "application/vnd.github.v3+json",
|
|
203
|
+
authorization: `token ${token}`
|
|
204
|
+
};
|
|
205
|
+
const authorsByEmail = new Map(commits.flatMap((commit) => commit.authors).map((author) => [author.email, author]));
|
|
206
|
+
await Promise.all([...authorsByEmail.values()].map(async (author) => {
|
|
207
|
+
try {
|
|
208
|
+
author.login = (await (await fetch(`https://api.github.com/search/users?q=${encodeURIComponent(`${author.email} type:user in:email`)}`, { headers })).json()).items?.[0]?.login;
|
|
209
|
+
const commit = commits.find((commit) => commit.authors[0].email === author.email);
|
|
210
|
+
if (!author.login && commit) author.login = (await (await fetch(`https://api.github.com/repos/${repository.path}/commits/${commit.hash}`, { headers })).json()).author?.login;
|
|
211
|
+
} catch {}
|
|
212
|
+
}));
|
|
213
|
+
for (const commit of commits) for (const author of commit.authors) author.login = authorsByEmail.get(author.email)?.login;
|
|
214
|
+
},
|
|
215
|
+
async publishRelease(repository, release, token, fetch) {
|
|
216
|
+
const headers = {
|
|
217
|
+
accept: "application/vnd.github+json",
|
|
218
|
+
authorization: `Bearer ${token}`,
|
|
219
|
+
"content-type": "application/json"
|
|
220
|
+
};
|
|
221
|
+
const releasesUrl = `https://api.github.com/repos/${repository.path}/releases`;
|
|
222
|
+
const existing = await fetch(`${releasesUrl}/tags/${encodeURIComponent(release.tag)}`, { headers });
|
|
223
|
+
let url = releasesUrl;
|
|
224
|
+
let method = "POST";
|
|
225
|
+
let action = "created";
|
|
226
|
+
if (existing.ok) {
|
|
227
|
+
url = `${releasesUrl}/${(await existing.json()).id}`;
|
|
228
|
+
method = "PATCH";
|
|
229
|
+
action = "updated";
|
|
230
|
+
} else if (existing.status !== 404) throw new Error(`GitHub release lookup failed (${existing.status})`);
|
|
231
|
+
const response = await fetch(url, {
|
|
232
|
+
method,
|
|
233
|
+
headers,
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
tag_name: release.tag,
|
|
236
|
+
name: release.name,
|
|
237
|
+
body: release.body,
|
|
238
|
+
prerelease: release.prerelease
|
|
239
|
+
})
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
|
|
242
|
+
return {
|
|
243
|
+
url: (await response.json()).html_url,
|
|
244
|
+
action
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/repository.ts
|
|
250
|
+
const providers$1 = [githubProvider];
|
|
251
|
+
async function resolveRepository(cwd) {
|
|
252
|
+
let source = await readFile(resolve(cwd, "package.json"), "utf8").then((contents) => {
|
|
253
|
+
const repository = JSON.parse(contents).repository;
|
|
254
|
+
return typeof repository === "string" ? repository : repository?.url || "";
|
|
255
|
+
}).catch(() => "");
|
|
256
|
+
if (!source) source = (await x("git", [
|
|
257
|
+
"remote",
|
|
258
|
+
"get-url",
|
|
259
|
+
"origin"
|
|
260
|
+
], { nodeOptions: { cwd } })).stdout.trim();
|
|
261
|
+
for (const provider of providers$1) {
|
|
262
|
+
const repository = provider.parse(source);
|
|
263
|
+
if (repository) return repository;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/version.ts
|
|
268
|
+
const providers = [{ async update(cwd, version) {
|
|
269
|
+
const path = resolve(cwd, "package.json");
|
|
270
|
+
const contents = await readFile(path, "utf8").catch((error) => {
|
|
271
|
+
if (error.code === "ENOENT") return;
|
|
272
|
+
throw error;
|
|
273
|
+
});
|
|
274
|
+
if (contents === void 0) return;
|
|
275
|
+
const packageJson = JSON.parse(contents);
|
|
276
|
+
if (packageJson.version === version) return path;
|
|
277
|
+
packageJson.version = version;
|
|
278
|
+
const indent = /\n([ \t]+)"/.exec(contents)?.[1] ?? " ";
|
|
279
|
+
const newline = contents.endsWith("\n") ? "\n" : "";
|
|
280
|
+
await writeFile(path, `${JSON.stringify(packageJson, null, indent)}${newline}`);
|
|
281
|
+
return path;
|
|
282
|
+
} }];
|
|
283
|
+
async function updateVersion(cwd, version) {
|
|
284
|
+
for (const provider of providers) {
|
|
285
|
+
const path = await provider.update(cwd, version);
|
|
286
|
+
if (path) return path;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/run.ts
|
|
291
|
+
const defaultAuthor = "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>";
|
|
292
|
+
async function runChangelog(options, environment) {
|
|
293
|
+
const { version: version$1 } = options;
|
|
294
|
+
const tag = `v${version$1}`;
|
|
295
|
+
const gitIdentity = options.commit || options.release ? resolveGitIdentity(options.author) : [];
|
|
296
|
+
const env = environment.env ?? process.env;
|
|
297
|
+
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
298
|
+
const colors = environment.colors ?? ansis;
|
|
299
|
+
const latestTag = await getLatestTag(environment.cwd);
|
|
300
|
+
const repository = await resolveRepository(environment.cwd);
|
|
301
|
+
const token = repository?.provider.token(options.token, env);
|
|
302
|
+
if (options.release) {
|
|
303
|
+
if (!repository) throw new Error("A supported repository is required to release");
|
|
304
|
+
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
305
|
+
}
|
|
306
|
+
let taggedCommit = options.release ? await getTagCommit(environment.cwd, tag) : void 0;
|
|
307
|
+
let releaseRef = taggedCommit ? tag : void 0;
|
|
308
|
+
const remoteTaggedCommit = options.release ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
|
|
309
|
+
if (taggedCommit && remoteTaggedCommit && taggedCommit !== remoteTaggedCommit) throw new Error(`Tag ${tag} differs between local and origin`);
|
|
310
|
+
if (remoteTaggedCommit && !taggedCommit) if (options.dry) {
|
|
311
|
+
taggedCommit = remoteTaggedCommit;
|
|
312
|
+
releaseRef = remoteTaggedCommit;
|
|
313
|
+
} else {
|
|
314
|
+
await git(environment.cwd, "fetch", "origin", `refs/tags/${tag}:refs/tags/${tag}`);
|
|
315
|
+
taggedCommit = await getTagCommit(environment.cwd, tag);
|
|
316
|
+
releaseRef = tag;
|
|
317
|
+
}
|
|
318
|
+
const from = taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : latestTag;
|
|
319
|
+
const to = releaseRef || "HEAD";
|
|
320
|
+
const comparisonFrom = from || (repository ? (await git(environment.cwd, "rev-list", "--max-parents=0", to)).trim() : "");
|
|
321
|
+
const commits = await readCommits(environment.cwd, from, to);
|
|
322
|
+
if (token && repository && !(options.release && options.dry)) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
|
|
323
|
+
const { body: releaseBody, release } = createChangelog(version$1, commits, repository, comparisonFrom);
|
|
324
|
+
const repositoryRelease = {
|
|
325
|
+
tag,
|
|
326
|
+
name: tag,
|
|
327
|
+
body: releaseBody,
|
|
328
|
+
prerelease: version$1.includes("-")
|
|
329
|
+
};
|
|
330
|
+
const printReleasePreview = () => {
|
|
331
|
+
stdout([
|
|
332
|
+
colors.dim(`keep${colors.bold("changes")} v${version}`),
|
|
333
|
+
`${colors.cyan(from || comparisonFrom)}${colors.dim(" -> ")}${colors.blue(tag)}${colors.dim(` (${commits.length} commits)`)}`,
|
|
334
|
+
colors.dim("--------------"),
|
|
335
|
+
"",
|
|
336
|
+
releaseBody.replaceAll(" ", ""),
|
|
337
|
+
"",
|
|
338
|
+
colors.dim("--------------"),
|
|
339
|
+
""
|
|
340
|
+
].join("\n"));
|
|
341
|
+
};
|
|
342
|
+
const printManualReleaseUrl = () => {
|
|
343
|
+
const url = repository.provider.manualReleaseUrl?.(repository, repositoryRelease);
|
|
344
|
+
if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
|
|
345
|
+
return url;
|
|
346
|
+
};
|
|
347
|
+
const publishRepositoryRelease = async () => {
|
|
348
|
+
printReleasePreview();
|
|
349
|
+
if (!token || !repository.provider.publishRelease) {
|
|
350
|
+
if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
|
|
351
|
+
stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via GITHUB_TOKEN env. Release skipped.`)}\n\n`);
|
|
352
|
+
printManualReleaseUrl();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const result = await repository.provider.publishRelease(repository, repositoryRelease, token, environment.fetch ?? globalThis.fetch);
|
|
356
|
+
stdout(`${colors.green(`${capitalize(result.action)} ${repository.provider.name} release: ${result.url}`)}\n`);
|
|
357
|
+
};
|
|
358
|
+
const outputPath = resolve(environment.cwd, options.output || "CHANGELOG.md");
|
|
359
|
+
const currentChangelog = await readChangelog(outputPath);
|
|
360
|
+
const releaseExists = hasRelease(currentChangelog, version$1);
|
|
361
|
+
const changelog = insertRelease(currentChangelog, release);
|
|
362
|
+
if (options.dry) {
|
|
363
|
+
if (options.release) {
|
|
364
|
+
printReleasePreview();
|
|
365
|
+
stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
|
|
366
|
+
printManualReleaseUrl();
|
|
367
|
+
} else stdout(changelog);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (options.release && taggedCommit) {
|
|
371
|
+
if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
|
|
372
|
+
await publishRepositoryRelease();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await writeChangelog(outputPath, changelog);
|
|
376
|
+
const versionPath = await updateVersion(environment.cwd, version$1);
|
|
377
|
+
if (options.commit || options.release) {
|
|
378
|
+
const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
|
|
379
|
+
if ((await git(environment.cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) {
|
|
380
|
+
await git(environment.cwd, "add", "--", ...releasePaths);
|
|
381
|
+
const versionChanges = versionPath ? await git(environment.cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
382
|
+
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${version$1} release notes` : `chore(release): v${version$1}`;
|
|
383
|
+
await git(environment.cwd, ...gitIdentity, "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (options.release) {
|
|
387
|
+
await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
|
|
388
|
+
await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
|
|
389
|
+
await publishRepositoryRelease();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function capitalize(value) {
|
|
393
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
394
|
+
}
|
|
395
|
+
function resolveGitIdentity(author = defaultAuthor) {
|
|
396
|
+
const match = /^(.+?)\s*<([^<>]+)>$/.exec(author);
|
|
397
|
+
if (!match) throw new Error("Author must use the \"Name <email>\" format");
|
|
398
|
+
return [
|
|
399
|
+
"-c",
|
|
400
|
+
`user.name=${match[1].trim()}`,
|
|
401
|
+
"-c",
|
|
402
|
+
`user.email=${match[2].trim()}`
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/cli.ts
|
|
407
|
+
async function runCli(args, environment) {
|
|
408
|
+
const cli = cac("changelog").version(version).option("--output <path>", "Changelog file path").option("--dry", "Print the changelog without writing it").option("--commit", "Commit the changelog").option("--release", "Publish a repository release").option("--author <author>", "Commit author in \"Name <email>\" format").option("--token <token>", "Repository token for resolving authors").help();
|
|
409
|
+
cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
|
|
410
|
+
if (!versionArgument) throw new Error("A release version is required");
|
|
411
|
+
await runChangelog({
|
|
412
|
+
version: versionArgument.replace(/^v/, ""),
|
|
413
|
+
output: options.output,
|
|
414
|
+
dry: options.dry,
|
|
415
|
+
commit: options.commit,
|
|
416
|
+
release: options.release,
|
|
417
|
+
author: options.author,
|
|
418
|
+
token: options.token
|
|
419
|
+
}, environment);
|
|
420
|
+
});
|
|
421
|
+
cli.parse([
|
|
422
|
+
"node",
|
|
423
|
+
"changelog",
|
|
424
|
+
...args
|
|
425
|
+
], { run: false });
|
|
426
|
+
await cli.runMatchedCommand();
|
|
427
|
+
}
|
|
428
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) runCli(process.argv.slice(2), { cwd: process.cwd() }).catch((error) => {
|
|
429
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
430
|
+
process.exitCode = 1;
|
|
431
|
+
});
|
|
432
|
+
//#endregion
|
|
433
|
+
export { runCli };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "keepchanges",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "Generate and maintain CHANGELOG.md from Conventional Commits.",
|
|
6
|
+
"author": "edram",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/edram/gitchangelog#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/edram/gitchangelog.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/edram/gitchangelog/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"changelog": "./dist/cli.mjs"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/cli.mjs",
|
|
21
|
+
"./cli": "./dist/cli.mjs",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.19.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"changelog",
|
|
32
|
+
"cli",
|
|
33
|
+
"conventional-commits",
|
|
34
|
+
"git"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.1.1",
|
|
38
|
+
"bumpp": "^11.1.0",
|
|
39
|
+
"tsdown": "^0.22.5",
|
|
40
|
+
"typescript": "^6.0.3",
|
|
41
|
+
"vitest": "^4.1.10"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"ansis": "^4.3.1",
|
|
45
|
+
"cac": "^7.0.0",
|
|
46
|
+
"tinyexec": "^1.2.4",
|
|
47
|
+
"verkit": "^0.3.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"dev": "tsdown --watch",
|
|
52
|
+
"test": "pnpm run build && vitest run",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"release": "bumpp"
|
|
55
|
+
}
|
|
56
|
+
}
|