pi-release 1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZachDreamZ
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,179 @@
1
+ # pi-release
2
+
3
+ > Pi-native release automation: bump version, generate changelog, create GitHub release, publish to npm.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pi install npm:pi-release
9
+ ```
10
+
11
+ ## What It Does
12
+
13
+ pi-release automates the full release lifecycle for pi.dev packages. It completes the pipeline:
14
+
15
+ ```
16
+ pi-commit-lint → pi-changelog → pi-release
17
+ ```
18
+
19
+ Three focused tools and one orchestrated `/release` command handle:
20
+ - **Version bumping** — semver-aware `package.json` updates with git commits and tags
21
+ - **GitHub releases** — auto-generated release notes via GitHub CLI
22
+ - **npm publishing** — with OTP/2FA support and custom registries
23
+
24
+ ## Tools
25
+
26
+ ### `bump_version`
27
+
28
+ Bump the version in `package.json`, create a git commit and tag.
29
+
30
+ ```
31
+ bump_version(bump: "patch" | "minor" | "major", pre_release?: "beta", dry_run?: false)
32
+ ```
33
+
34
+ **Parameters:**
35
+ | Parameter | Type | Default | Description |
36
+ |-----------|------|---------|-------------|
37
+ | `bump` | `major \| minor \| patch` | `patch` | Version bump type |
38
+ | `pre_release` | `string` | — | Pre-release tag (e.g. `beta`, `alpha`, `rc`) |
39
+ | `dry_run` | `boolean` | `false` | Preview without making changes |
40
+ | `message` | `string` | `chore(release): {version}` | Custom commit message |
41
+
42
+ ### `create_github_release`
43
+
44
+ Create a GitHub release from a git tag.
45
+
46
+ ```
47
+ create_github_release(tag: "v1.0.0", title?: "Release v1.0.0", notes?: "...", prerelease?: false, dry_run?: false)
48
+ ```
49
+
50
+ **Parameters:**
51
+ | Parameter | Type | Default | Description |
52
+ |-----------|------|---------|-------------|
53
+ | `tag` | `string` | *required* | Git tag to release from |
54
+ | `title` | `string` | `Release {tag}` | Release title |
55
+ | `notes` | `string` | *auto-generated* | Custom release notes |
56
+ | `prerelease` | `boolean` | `false` | Mark as pre-release |
57
+ | `dry_run` | `boolean` | `false` | Preview without creating |
58
+
59
+ Requires [GitHub CLI](https://cli.github.com/) (`gh`) installed and authenticated.
60
+
61
+ ### `publish_npm`
62
+
63
+ Publish the package to npm.
64
+
65
+ ```
66
+ publish_npm(otp?: "123456", registry?: "https://...", tag?: "beta", dry_run?: false)
67
+ ```
68
+
69
+ **Parameters:**
70
+ | Parameter | Type | Default | Description |
71
+ |-----------|------|---------|-------------|
72
+ | `otp` | `string` | — | One-time password for npm 2FA |
73
+ | `registry` | `string` | `https://registry.npmjs.org/` | npm registry URL |
74
+ | `tag` | `string` | `latest` | npm dist-tag |
75
+ | `dry_run` | `boolean` | `false` | Preview without publishing |
76
+
77
+ ## Command
78
+
79
+ ### `/release`
80
+
81
+ Orchestrates the full release flow:
82
+
83
+ ```
84
+ /release [major|minor|patch] [--dry-run]
85
+ ```
86
+
87
+ **Steps:**
88
+ 1. 📦 Bump version in `package.json`, commit, tag
89
+ 2. 📝 Generate changelog from conventional commits
90
+ 3. 🚀 Create GitHub release with auto-generated notes
91
+ 4. 📤 Publish to npm
92
+
93
+ ```bash
94
+ # Preview a patch release
95
+ /release patch --dry-run
96
+
97
+ # Execute a minor release
98
+ /release minor
99
+
100
+ # Major release
101
+ /release major
102
+ ```
103
+
104
+ ## Configuration
105
+
106
+ Create `pi-release.json` in your project root:
107
+
108
+ ```json
109
+ {
110
+ "registry": "https://registry.npmjs.org/",
111
+ "githubRepo": "owner/repo",
112
+ "defaultBump": "patch",
113
+ "preReleaseTag": "",
114
+ "generateChangelog": true,
115
+ "commitBump": true,
116
+ "commitMessage": "chore(release): {version}",
117
+ "createTag": true,
118
+ "tagPrefix": "v",
119
+ "createGithubRelease": true,
120
+ "publishToNpm": true
121
+ }
122
+ ```
123
+
124
+ **Config options:**
125
+ | Option | Type | Default | Description |
126
+ |--------|------|---------|-------------|
127
+ | `registry` | `string` | `https://registry.npmjs.org/` | npm registry URL |
128
+ | `githubRepo` | `string \| null` | `null` (auto-detect) | GitHub `owner/repo` |
129
+ | `defaultBump` | `string` | `patch` | Default bump type for `/release` |
130
+ | `preReleaseTag` | `string` | `""` | Pre-release tag applied to all releases |
131
+ | `generateChangelog` | `boolean` | `true` | Auto-generate changelog |
132
+ | `commitBump` | `boolean` | `true` | Commit the version bump |
133
+ | `commitMessage` | `string` | `chore(release): {version}` | Commit message template |
134
+ | `createTag` | `boolean` | `true` | Create git tag |
135
+ | `tagPrefix` | `string` | `v` | Tag prefix (e.g. `v1.0.0`) |
136
+ | `createGithubRelease` | `boolean` | `true` | Create GitHub release |
137
+ | `publishToNpm` | `boolean` | `true` | Publish to npm |
138
+
139
+ ## Dry Run
140
+
141
+ All tools support `--dry-run` mode to preview what would happen without making any changes:
142
+
143
+ ```bash
144
+ # Via tool
145
+ bump_version(bump="minor", dry_run=true)
146
+
147
+ # Via command
148
+ /release minor --dry-run
149
+ ```
150
+
151
+ ## Example Workflow
152
+
153
+ ```bash
154
+ # 1. Ensure commits follow conventional format
155
+ pi> /lint-commits
156
+
157
+ # 2. Preview the release
158
+ pi> /release minor --dry-run
159
+
160
+ # 3. Execute the release
161
+ pi> /release minor
162
+
163
+ # Output:
164
+ # 📦 Version Bump: 1.0.0 → 1.1.0
165
+ # 📝 Changelog generated
166
+ # 🚀 GitHub Release: https://github.com/owner/repo/releases/tag/v1.1.0
167
+ # 📤 Published to npm: my-package@1.1.0
168
+ # ✅ Release complete!
169
+ ```
170
+
171
+ ## Resources
172
+
173
+ - [npm](https://www.npmjs.com/package/pi-release)
174
+ - [GitHub](https://github.com/ZachDreamZ/pi-release)
175
+ - [pi.dev](https://pi.dev/packages/pi-release)
176
+
177
+ ## License
178
+
179
+ MIT
package/biome.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
3
+ "organizeImports": {
4
+ "enabled": true
5
+ },
6
+ "linter": {
7
+ "enabled": true,
8
+ "rules": {
9
+ "recommended": true,
10
+ "correctness": {
11
+ "noUnusedVariables": "warn"
12
+ }
13
+ }
14
+ },
15
+ "files": {
16
+ "include": ["extensions/**/*.ts"],
17
+ "ignore": ["node_modules", "dist"]
18
+ }
19
+ }
@@ -0,0 +1,398 @@
1
+ import { execSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import {
7
+ bumpSemver,
8
+ bumpVersionImpl,
9
+ createGithubReleaseImpl,
10
+ generateBasicChangelog,
11
+ parseGithubRepo,
12
+ publishNpmImpl,
13
+ } from "./index.js";
14
+
15
+ // ── Helpers ─────────────────────────────────────────────────────────
16
+
17
+ function createTmpGitRepo(): string {
18
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "release-test-"));
19
+ execSync("git init", { cwd: tmpDir, stdio: "pipe" });
20
+ execSync("git config user.email test@test.com", {
21
+ cwd: tmpDir,
22
+ stdio: "pipe",
23
+ });
24
+ execSync("git config user.name Test", { cwd: tmpDir, stdio: "pipe" });
25
+
26
+ // Create initial package.json
27
+ const pkg = {
28
+ name: "test-package",
29
+ version: "1.0.0",
30
+ description: "Test package",
31
+ };
32
+ fs.writeFileSync(
33
+ path.join(tmpDir, "package.json"),
34
+ `${JSON.stringify(pkg, null, 2)}\n`,
35
+ "utf-8",
36
+ );
37
+
38
+ execSync("git add .", { cwd: tmpDir, stdio: "pipe" });
39
+ execSync('git commit -m "feat: initial commit"', {
40
+ cwd: tmpDir,
41
+ stdio: "pipe",
42
+ });
43
+
44
+ return tmpDir;
45
+ }
46
+
47
+ function commit(dir: string, message: string, file = "test.txt"): void {
48
+ const filePath = path.join(dir, file);
49
+ fs.writeFileSync(filePath, `${message}\n`, "utf-8");
50
+ execSync(`git add ${file}`, { cwd: dir, stdio: "pipe" });
51
+ execSync(`git commit -m "${message}"`, { cwd: dir, stdio: "pipe" });
52
+ }
53
+
54
+ function tag(dir: string, tagName: string): void {
55
+ execSync(`git tag ${tagName}`, { cwd: dir, stdio: "pipe" });
56
+ }
57
+
58
+ // ── bumpSemver tests ────────────────────────────────────────────────
59
+
60
+ describe("bumpSemver", () => {
61
+ it("should bump patch version", () => {
62
+ expect(bumpSemver("1.0.0", "patch")).toBe("1.0.1");
63
+ });
64
+
65
+ it("should bump minor version", () => {
66
+ expect(bumpSemver("1.0.0", "minor")).toBe("1.1.0");
67
+ });
68
+
69
+ it("should bump major version", () => {
70
+ expect(bumpSemver("1.0.0", "major")).toBe("2.0.0");
71
+ });
72
+
73
+ it("should reset lower segments on major bump", () => {
74
+ expect(bumpSemver("1.2.3", "major")).toBe("2.0.0");
75
+ });
76
+
77
+ it("should reset patch on minor bump", () => {
78
+ expect(bumpSemver("1.2.3", "minor")).toBe("1.3.0");
79
+ });
80
+
81
+ it("should handle large version numbers", () => {
82
+ expect(bumpSemver("9.9.9", "patch")).toBe("9.9.10");
83
+ expect(bumpSemver("9.9.9", "minor")).toBe("9.10.0");
84
+ expect(bumpSemver("9.9.9", "major")).toBe("10.0.0");
85
+ });
86
+
87
+ it("should throw on invalid semver", () => {
88
+ expect(() => bumpSemver("invalid", "patch")).toThrow(
89
+ "Invalid semver version",
90
+ );
91
+ expect(() => bumpSemver("1.2", "patch")).toThrow("Invalid semver version");
92
+ expect(() => bumpSemver("1.2.x", "patch")).toThrow(
93
+ "Invalid semver version",
94
+ );
95
+ });
96
+ });
97
+
98
+ // ── parseGithubRepo tests ───────────────────────────────────────────
99
+
100
+ describe("parseGithubRepo", () => {
101
+ it("should parse SSH remote", () => {
102
+ expect(parseGithubRepo("git@github.com:owner/repo.git")).toBe("owner/repo");
103
+ });
104
+
105
+ it("should parse HTTPS remote with .git", () => {
106
+ expect(parseGithubRepo("https://github.com/owner/repo.git")).toBe(
107
+ "owner/repo",
108
+ );
109
+ });
110
+
111
+ it("should parse HTTPS remote without .git", () => {
112
+ expect(parseGithubRepo("https://github.com/owner/repo")).toBe("owner/repo");
113
+ });
114
+
115
+ it("should return null for non-GitHub remotes", () => {
116
+ expect(parseGithubRepo("https://gitlab.com/owner/repo.git")).toBeNull();
117
+ expect(parseGithubRepo("git@gitlab.com:owner/repo.git")).toBeNull();
118
+ });
119
+ });
120
+
121
+ // ── bumpVersionImpl tests ───────────────────────────────────────────
122
+
123
+ describe("bumpVersionImpl", () => {
124
+ let tmpDir: string;
125
+
126
+ beforeEach(() => {
127
+ tmpDir = createTmpGitRepo();
128
+ });
129
+
130
+ afterEach(() => {
131
+ fs.rmSync(tmpDir, { recursive: true, force: true });
132
+ });
133
+
134
+ it("should bump patch version by default", () => {
135
+ const result = bumpVersionImpl("patch", { cwd: tmpDir });
136
+ expect(result.previousVersion).toBe("1.0.0");
137
+ expect(result.newVersion).toBe("1.0.1");
138
+ expect(result.tag).toBe("v1.0.1");
139
+ expect(result.dryRun).toBe(false);
140
+ expect(result.commitHash).toBeTruthy();
141
+ });
142
+
143
+ it("should bump minor version", () => {
144
+ const result = bumpVersionImpl("minor", { cwd: tmpDir });
145
+ expect(result.newVersion).toBe("1.1.0");
146
+ expect(result.tag).toBe("v1.1.0");
147
+ });
148
+
149
+ it("should bump major version", () => {
150
+ const result = bumpVersionImpl("major", { cwd: tmpDir });
151
+ expect(result.newVersion).toBe("2.0.0");
152
+ expect(result.tag).toBe("v2.0.0");
153
+ });
154
+
155
+ it("should add pre-release tag", () => {
156
+ const result = bumpVersionImpl("patch", {
157
+ cwd: tmpDir,
158
+ preReleaseTag: "beta",
159
+ });
160
+ expect(result.newVersion).toBe("1.0.1-beta");
161
+ expect(result.tag).toBe("v1.0.1-beta");
162
+ });
163
+
164
+ it("should write new version to package.json", () => {
165
+ bumpVersionImpl("minor", { cwd: tmpDir });
166
+ const pkg = JSON.parse(
167
+ fs.readFileSync(path.join(tmpDir, "package.json"), "utf-8"),
168
+ );
169
+ expect(pkg.version).toBe("1.1.0");
170
+ });
171
+
172
+ it("should create git tag", () => {
173
+ bumpVersionImpl("patch", { cwd: tmpDir });
174
+ const tags = execSync("git tag", { cwd: tmpDir, encoding: "utf-8" }).trim();
175
+ expect(tags).toContain("v1.0.1");
176
+ });
177
+
178
+ it("should not modify files in dry run mode", () => {
179
+ const result = bumpVersionImpl("major", { cwd: tmpDir, dryRun: true });
180
+ expect(result.dryRun).toBe(true);
181
+ expect(result.commitHash).toBeNull();
182
+
183
+ // Verify package.json unchanged
184
+ const pkg = JSON.parse(
185
+ fs.readFileSync(path.join(tmpDir, "package.json"), "utf-8"),
186
+ );
187
+ expect(pkg.version).toBe("1.0.0");
188
+ });
189
+
190
+ it("should throw if working directory is dirty", () => {
191
+ fs.writeFileSync(path.join(tmpDir, "dirty.txt"), "dirty", "utf-8");
192
+ expect(() => bumpVersionImpl("patch", { cwd: tmpDir })).toThrow(
193
+ "uncommitted changes",
194
+ );
195
+ });
196
+
197
+ it("should throw if tag already exists", () => {
198
+ tag(tmpDir, "v1.0.1");
199
+ expect(() => bumpVersionImpl("patch", { cwd: tmpDir })).toThrow(
200
+ "already exists",
201
+ );
202
+ });
203
+
204
+ it("should use custom tag prefix", () => {
205
+ const result = bumpVersionImpl("patch", {
206
+ cwd: tmpDir,
207
+ tagPrefix: "release-",
208
+ });
209
+ expect(result.tag).toBe("release-1.0.1");
210
+ });
211
+
212
+ it("should use custom commit message", () => {
213
+ const result = bumpVersionImpl("patch", {
214
+ cwd: tmpDir,
215
+ commitMessage: "release: v{version}",
216
+ });
217
+ expect(result.commitHash).toBeTruthy();
218
+
219
+ const lastMsg = execSync("git log -1 --format=%s", {
220
+ cwd: tmpDir,
221
+ encoding: "utf-8",
222
+ }).trim();
223
+ expect(lastMsg).toBe("release: v1.0.1");
224
+ });
225
+ });
226
+
227
+ // ── generateBasicChangelog tests ────────────────────────────────────
228
+
229
+ describe("generateBasicChangelog", () => {
230
+ let tmpDir: string;
231
+
232
+ beforeEach(() => {
233
+ tmpDir = createTmpGitRepo();
234
+ });
235
+
236
+ afterEach(() => {
237
+ fs.rmSync(tmpDir, { recursive: true, force: true });
238
+ });
239
+
240
+ it("should generate changelog for initial commit", () => {
241
+ const result = generateBasicChangelog("1.0.0", tmpDir);
242
+ expect(result).toContain("# Changelog");
243
+ expect(result).toContain("[1.0.0]");
244
+ expect(result).toContain("initial commit");
245
+ });
246
+
247
+ it("should group commits by type", () => {
248
+ commit(tmpDir, "feat: add login", "login.txt");
249
+ commit(tmpDir, "fix: fix crash", "fix.txt");
250
+ commit(tmpDir, "docs: update README", "docs.txt");
251
+
252
+ const result = generateBasicChangelog("1.1.0", tmpDir);
253
+ expect(result).toContain("Features");
254
+ expect(result).toContain("Bug Fixes");
255
+ expect(result).toContain("Documentation");
256
+ });
257
+
258
+ it("should detect breaking changes", () => {
259
+ commit(tmpDir, "feat!: redesign API", "api.txt");
260
+
261
+ const result = generateBasicChangelog("2.0.0", tmpDir);
262
+ expect(result).toContain("BREAKING CHANGES");
263
+ expect(result).toContain("redesign API");
264
+ });
265
+
266
+ it("should handle scoped commits", () => {
267
+ commit(tmpDir, "feat(auth): add OAuth", "auth.txt");
268
+
269
+ const result = generateBasicChangelog("1.1.0", tmpDir);
270
+ expect(result).toContain("**auth:**");
271
+ expect(result).toContain("add OAuth");
272
+ });
273
+
274
+ it("should handle empty history gracefully", () => {
275
+ // Create a fresh repo with no commits beyond initial
276
+ const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), "changelog-empty-"));
277
+ execSync("git init", { cwd: emptyDir, stdio: "pipe" });
278
+ execSync("git config user.email test@test.com", {
279
+ cwd: emptyDir,
280
+ stdio: "pipe",
281
+ });
282
+ execSync("git config user.name Test", { cwd: emptyDir, stdio: "pipe" });
283
+
284
+ const result = generateBasicChangelog("0.0.1", emptyDir);
285
+ expect(result).toContain("# Changelog");
286
+ expect(result).toContain("No commits found");
287
+
288
+ fs.rmSync(emptyDir, { recursive: true, force: true });
289
+ });
290
+
291
+ it("should limit to commits since last tag", () => {
292
+ commit(tmpDir, "feat: first", "a.txt");
293
+ tag(tmpDir, "v1.0.0");
294
+ commit(tmpDir, "feat: second", "b.txt");
295
+ commit(tmpDir, "fix: third", "c.txt");
296
+
297
+ const result = generateBasicChangelog("1.1.0", tmpDir);
298
+ // Should only contain commits after v1.0.0
299
+ expect(result).toContain("second");
300
+ expect(result).toContain("third");
301
+ expect(result).not.toContain("first");
302
+ });
303
+ });
304
+
305
+ // ── createGithubReleaseImpl tests ───────────────────────────────────
306
+
307
+ describe("createGithubReleaseImpl", () => {
308
+ let tmpDir: string;
309
+
310
+ beforeEach(() => {
311
+ tmpDir = createTmpGitRepo();
312
+ // Add a GitHub remote so the function can detect the repo
313
+ execSync(
314
+ "git remote add origin https://github.com/test-owner/test-repo.git",
315
+ { cwd: tmpDir, stdio: "pipe" },
316
+ );
317
+ tag(tmpDir, "v1.0.0");
318
+ });
319
+
320
+ afterEach(() => {
321
+ fs.rmSync(tmpDir, { recursive: true, force: true });
322
+ });
323
+
324
+ it("should return dry run result without executing", () => {
325
+ const result = createGithubReleaseImpl("v1.0.0", {
326
+ cwd: tmpDir,
327
+ dryRun: true,
328
+ });
329
+ expect(result.dryRun).toBe(true);
330
+ expect(result.tag).toBe("v1.0.0");
331
+ expect(result.url).toContain("test-owner/test-repo");
332
+ expect(result.url).toContain("v1.0.0");
333
+ });
334
+
335
+ it("should throw if tag does not exist (non-dry-run)", () => {
336
+ expect(() =>
337
+ createGithubReleaseImpl("v9.9.9", { cwd: tmpDir, dryRun: false }),
338
+ ).toThrow("does not exist");
339
+ });
340
+ });
341
+
342
+ // ── publishNpmImpl tests ────────────────────────────────────────────
343
+
344
+ describe("publishNpmImpl", () => {
345
+ let tmpDir: string;
346
+
347
+ beforeEach(() => {
348
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "publish-test-"));
349
+ const pkg = {
350
+ name: "test-pi-release-pkg",
351
+ version: "1.0.0",
352
+ };
353
+ fs.writeFileSync(
354
+ path.join(tmpDir, "package.json"),
355
+ JSON.stringify(pkg, null, 2),
356
+ "utf-8",
357
+ );
358
+ });
359
+
360
+ afterEach(() => {
361
+ fs.rmSync(tmpDir, { recursive: true, force: true });
362
+ });
363
+
364
+ it("should return dry run result", () => {
365
+ const result = publishNpmImpl({ cwd: tmpDir, dryRun: true });
366
+ expect(result.dryRun).toBe(true);
367
+ expect(result.package).toBe("test-pi-release-pkg");
368
+ expect(result.version).toBe("1.0.0");
369
+ });
370
+
371
+ it("should use custom registry", () => {
372
+ const result = publishNpmImpl({
373
+ cwd: tmpDir,
374
+ dryRun: true,
375
+ registry: "https://custom.registry.com/",
376
+ });
377
+ expect(result.registry).toBe("https://custom.registry.com/");
378
+ });
379
+ });
380
+
381
+ // ── Extension registration tests ────────────────────────────────────
382
+
383
+ describe("extension registration", () => {
384
+ it("should export a default function", async () => {
385
+ const mod = await import("./index.js");
386
+ expect(typeof mod.default).toBe("function");
387
+ });
388
+
389
+ it("should export helper functions directly", async () => {
390
+ const mod = await import("./index.js");
391
+ expect(typeof mod.bumpSemver).toBe("function");
392
+ expect(typeof mod.bumpVersionImpl).toBe("function");
393
+ expect(typeof mod.createGithubReleaseImpl).toBe("function");
394
+ expect(typeof mod.publishNpmImpl).toBe("function");
395
+ expect(typeof mod.generateBasicChangelog).toBe("function");
396
+ expect(typeof mod.parseGithubRepo).toBe("function");
397
+ });
398
+ });