@pob/version 1.0.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.
Files changed (69) hide show
  1. package/.editorconfig +13 -0
  2. package/.yo-rc.json +13 -0
  3. package/CHANGELOG.md +35 -0
  4. package/LICENSE +21 -0
  5. package/README.md +163 -0
  6. package/dist/index-node.mjs +1419 -0
  7. package/dist/index-node.mjs.map +1 -0
  8. package/package.json +81 -0
  9. package/rollup.config.mjs +12 -0
  10. package/src/__fixtures__/bun-basic/package.json +4 -0
  11. package/src/__fixtures__/bun-no-version/package.json +3 -0
  12. package/src/__fixtures__/yarn-basic/package.json +4 -0
  13. package/src/__fixtures__/yarn-basic/yarn.lock +12 -0
  14. package/src/__fixtures__/yarn-monorepo/package.json +7 -0
  15. package/src/__fixtures__/yarn-monorepo/packages/package-1/package.json +4 -0
  16. package/src/__fixtures__/yarn-monorepo/packages/package-2/package.json +7 -0
  17. package/src/__fixtures__/yarn-monorepo/packages/package-3/package.json +7 -0
  18. package/src/__fixtures__/yarn-monorepo/packages/package-4/package.json +7 -0
  19. package/src/__fixtures__/yarn-monorepo/packages/package-5/package.json +8 -0
  20. package/src/__fixtures__/yarn-monorepo/yarn.lock +0 -0
  21. package/src/__fixtures__/yarn-monorepo-duplicate-names/package.json +7 -0
  22. package/src/__fixtures__/yarn-monorepo-duplicate-names/packages/pkg-a/package.json +4 -0
  23. package/src/__fixtures__/yarn-monorepo-duplicate-names/packages/pkg-b/package.json +4 -0
  24. package/src/__fixtures__/yarn-monorepo-invalid-package-json/package.json +7 -0
  25. package/src/__fixtures__/yarn-monorepo-invalid-package-json/packages/pkg-with-invalid-json/package.json +5 -0
  26. package/src/__fixtures__/yarn-monorepo-nested/package.json +9 -0
  27. package/src/__fixtures__/yarn-monorepo-nested/packages/deep-nested/package.json +4 -0
  28. package/src/__fixtures__/yarn-monorepo-nested/packages/nested-1/packages/package-nested.json +4 -0
  29. package/src/__fixtures__/yarn-monorepo-nested/packages/nested-1/packages/package.json +4 -0
  30. package/src/__fixtures__/yarn-monorepo-nested/packages/package-1/package.json +4 -0
  31. package/src/__fixtures__/yarn-monorepo-no-name/package.json +7 -0
  32. package/src/__fixtures__/yarn-monorepo-no-name/packages/no-name-package/package.json +3 -0
  33. package/src/__fixtures__/yarn-monorepo-object-form/package.json +9 -0
  34. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-1/package.json +4 -0
  35. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-2/package.json +4 -0
  36. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-3/package.json +4 -0
  37. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-4/package.json +4 -0
  38. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-5/package.json +4 -0
  39. package/src/__fixtures__/yarn-no-version/package.json +3 -0
  40. package/src/__fixtures__/yarn-no-version/yarn.lock +0 -0
  41. package/src/__fixtures__/yarn-with-lifecycle/.yarn/install-state.gz +0 -0
  42. package/src/__fixtures__/yarn-with-lifecycle/package.json +7 -0
  43. package/src/__fixtures__/yarn-with-lifecycle/yarn.lock +11 -0
  44. package/src/commands/version.test.ts +114 -0
  45. package/src/commands/version.ts +936 -0
  46. package/src/index.ts +13 -0
  47. package/src/missing-types.d.ts +10 -0
  48. package/src/pm/BunPackageManager.ts +12 -0
  49. package/src/pm/PackageManager.ts +66 -0
  50. package/src/pm/YarnPackageManager.ts +34 -0
  51. package/src/utils/UsageError.ts +6 -0
  52. package/src/utils/bumpTypeUtils.ts +113 -0
  53. package/src/utils/conventionalChangelogUtils.test.ts +762 -0
  54. package/src/utils/conventionalChangelogUtils.ts +104 -0
  55. package/src/utils/conventionalCommitConfigUtils.ts +20 -0
  56. package/src/utils/execCommand.ts +214 -0
  57. package/src/utils/generatorUtils.ts +11 -0
  58. package/src/utils/gitUtils.ts +193 -0
  59. package/src/utils/githubUtils.ts +114 -0
  60. package/src/utils/mapUtils.ts +7 -0
  61. package/src/utils/mapWorkspaces.test.ts +89 -0
  62. package/src/utils/packageDependenciesUtils.test.ts +25 -0
  63. package/src/utils/packageDependenciesUtils.ts +80 -0
  64. package/src/utils/packageUtils.test.ts +32 -0
  65. package/src/utils/packageUtils.ts +127 -0
  66. package/src/utils/updateChangelog.ts +39 -0
  67. package/src/utils/workspaceUtils.ts +142 -0
  68. package/tsconfig.json +21 -0
  69. package/vitest.config.js +9 -0
@@ -0,0 +1,104 @@
1
+ import { writeChangelogString } from "conventional-changelog-writer";
2
+ import { parseCommits as createConventionalCommitsParser } from "conventional-commits-parser";
3
+ import type { Commit, ParserStreamOptions } from "conventional-commits-parser";
4
+ import type { PackageJson } from "type-fest";
5
+ import type { ConventionalChangelogConfig } from "./conventionalCommitConfigUtils.ts";
6
+ import type { Workspace } from "./packageUtils.ts";
7
+
8
+ export interface WhatBump {
9
+ level: 0 | 1 | 2;
10
+ reason: string;
11
+ }
12
+ export type WhatBumpResult = WhatBump | null | undefined;
13
+ export interface BumperRecommendation extends WhatBump {
14
+ releaseType: "major" | "minor" | "patch";
15
+ commits: Commit[];
16
+ }
17
+ export interface EmptyBumperRecommendation {
18
+ commits: Commit[];
19
+ }
20
+ export type BumperRecommendationResult =
21
+ | BumperRecommendation
22
+ | EmptyBumperRecommendation;
23
+
24
+ export interface Preset {
25
+ whatBump: (commits: Commit[]) => Promise<WhatBumpResult> | WhatBumpResult;
26
+ tags?: unknown;
27
+ commits?: unknown;
28
+ parser?: ParserStreamOptions;
29
+ }
30
+
31
+ const versions: BumperRecommendation["releaseType"][] = [
32
+ "major",
33
+ "minor",
34
+ "patch",
35
+ ];
36
+
37
+ export const recommendBump = async (
38
+ commits: Parameters<ConventionalChangelogConfig["whatBump"]>[0],
39
+ config: ConventionalChangelogConfig,
40
+ ): Promise<Partial<BumperRecommendation>> => {
41
+ const whatBump = config.whatBump;
42
+ if (!whatBump) {
43
+ throw new Error("whatBump method is missing in config");
44
+ }
45
+ const result = { ...(await whatBump(commits)) };
46
+
47
+ return {
48
+ ...result,
49
+ releaseType: result.level != null ? versions[result.level] : undefined,
50
+ };
51
+ };
52
+
53
+ export type Commits = Parameters<ConventionalChangelogConfig["whatBump"]>[0];
54
+
55
+ export const generateChangelog = (
56
+ workspace: Workspace,
57
+ pkg: PackageJson,
58
+ config: ConventionalChangelogConfig,
59
+ previousTag: string | null,
60
+ newTag: string | null,
61
+ commits: Commits | undefined,
62
+ date: string,
63
+ // eslint-disable-next-line @typescript-eslint/max-params
64
+ ): Promise<string> => {
65
+ if (!newTag) {
66
+ throw new Error(`Missing new tag for package "${pkg.name ?? ""}"`);
67
+ }
68
+
69
+ const originUrl =
70
+ typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url;
71
+ const match =
72
+ originUrl &&
73
+ typeof originUrl === "string" &&
74
+ /^(?:git@|https?:\/\/)(?:([^./:]+(?:\.com)?)[/:])?([^/:]+)\/([^./:]+)(?:.git)?/.exec(
75
+ originUrl,
76
+ );
77
+ const [, gitHost, gitAccount, repoName] = match || [
78
+ undefined,
79
+ undefined,
80
+ undefined,
81
+ undefined,
82
+ ];
83
+
84
+ return writeChangelogString(
85
+ commits ?? [],
86
+ {
87
+ // @ts-expect-error - missing types
88
+ previousTag,
89
+ currentTag: newTag,
90
+ linkCompare: previousTag != null,
91
+ version: pkg.version,
92
+ host: gitHost ? `https://${gitHost}` : undefined,
93
+ owner: gitAccount,
94
+ repository: repoName,
95
+ date,
96
+ },
97
+ // @ts-expect-error - missing types
98
+ config.writer,
99
+ );
100
+ };
101
+
102
+ export const createCommitsParser = (config: ConventionalChangelogConfig) => {
103
+ return createConventionalCommitsParser(config.parser);
104
+ };
@@ -0,0 +1,20 @@
1
+ // eslint-disable-next-line import/no-unresolved
2
+ import { loadPreset } from "conventional-changelog-preset-loader";
3
+ import { UsageError } from "./UsageError.ts";
4
+ import type { Preset } from "./conventionalChangelogUtils.ts";
5
+ import type { Workspace } from "./packageUtils.ts";
6
+
7
+ export type ConventionalChangelogConfig = Preset;
8
+
9
+ export const loadConventionalCommitConfig = async (
10
+ rootWorkspace: Workspace,
11
+ preset: string,
12
+ ): Promise<ConventionalChangelogConfig> => {
13
+ try {
14
+ return await loadPreset<ConventionalChangelogConfig>(preset);
15
+ } catch (error: any) {
16
+ throw new UsageError(
17
+ `Failed to require preset "${preset}": ${error.message as string}`,
18
+ );
19
+ }
20
+ };
@@ -0,0 +1,214 @@
1
+ import childProcess from "node:child_process";
2
+ import type { Workspace } from "./packageUtils.ts";
3
+
4
+ interface ExecResultIfFailed {
5
+ code: number | null;
6
+ signal: string | null;
7
+ stdout: string;
8
+ stderr: string;
9
+ }
10
+
11
+ interface ExecResultIfSuccess {
12
+ code: null;
13
+ signal: null;
14
+ stdout: string;
15
+ stderr: string;
16
+ }
17
+
18
+ type ExecResult<Strict extends boolean> = Strict extends true
19
+ ? ExecResultIfSuccess
20
+ : ExecResultIfFailed;
21
+
22
+ async function execvp<const Strict extends boolean>(
23
+ command: string,
24
+ args: string[],
25
+ {
26
+ cwd = process.cwd(),
27
+ env = process.env,
28
+ encoding,
29
+ strict,
30
+ stdo = "pipe",
31
+ input,
32
+ }: {
33
+ cwd?: string;
34
+ env?: typeof process.env;
35
+ encoding?: BufferEncoding;
36
+ strict?: Strict;
37
+ stdo?: "inherit" | "pipe";
38
+ input?: string;
39
+ },
40
+ ): Promise<ExecResult<Strict>> {
41
+ const stdoutChunks: Uint8Array[] = [];
42
+ const stderrChunks: Uint8Array[] = [];
43
+ if (env.PWD !== undefined) {
44
+ env = { ...env, PWD: cwd };
45
+ }
46
+ const subprocess = childProcess.spawn(command, args, {
47
+ cwd,
48
+ env,
49
+ stdio: [input ? "pipe" : "ignore", stdo, stdo],
50
+ });
51
+ if (input && subprocess.stdin) {
52
+ // write input to stdin and close it
53
+ subprocess.stdin.write(input);
54
+ subprocess.stdin.end();
55
+ }
56
+ subprocess.stdout?.on("data", (chunk) => {
57
+ stdoutChunks.push(chunk);
58
+ });
59
+ subprocess.stderr?.on("data", (chunk) => {
60
+ stderrChunks.push(chunk);
61
+ });
62
+ return new Promise((resolve, reject) => {
63
+ subprocess.on("error", (err) => {
64
+ reject(new Error(`Process ${command} failed to spawn`));
65
+ });
66
+ subprocess.on("close", (code, signal) => {
67
+ const chunksToString = (chunks: Uint8Array[]): string =>
68
+ stdo === "inherit"
69
+ ? ""
70
+ : Buffer.concat(chunks).toString(encoding ?? "utf8");
71
+ const stdout = chunksToString(stdoutChunks);
72
+ const stderr = chunksToString(stderrChunks);
73
+ if (code === 0 || !strict) {
74
+ resolve({
75
+ code,
76
+ signal,
77
+ stdout,
78
+ stderr,
79
+ } as ExecResult<Strict>);
80
+ } else {
81
+ reject(
82
+ new Error(
83
+ `Process ${[command, ...args].join(" ")} exited ${code !== null ? `with code ${code}` : `with signal ${signal || ""}`}:\nstdout: ${stdout}\nstderr: ${stderr}`,
84
+ ),
85
+ );
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ async function* spawnStreamStdout<const Strict extends boolean>(
92
+ command: string,
93
+ args: string[],
94
+ {
95
+ cwd = process.cwd(),
96
+ env = process.env,
97
+ encoding,
98
+ strict,
99
+ separator,
100
+ }: {
101
+ cwd?: string;
102
+ env?: typeof process.env;
103
+ encoding?: BufferEncoding;
104
+ strict?: Strict;
105
+ separator: string;
106
+ },
107
+ ): AsyncGenerator<string, ExecResult<Strict>, unknown> {
108
+ const stderrChunks: Uint8Array[] = [];
109
+ if (env.PWD !== undefined) {
110
+ env = { ...env, PWD: cwd };
111
+ }
112
+ const subprocess = childProcess.spawn(command, args, {
113
+ cwd,
114
+ env,
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ });
117
+
118
+ subprocess.stderr.on("data", (chunk) => {
119
+ stderrChunks.push(chunk);
120
+ });
121
+
122
+ let chunk: Buffer;
123
+ let payload: string[];
124
+ let buffer = "";
125
+
126
+ for await (chunk of subprocess.stdout) {
127
+ buffer += chunk.toString();
128
+
129
+ if (buffer.includes(separator)) {
130
+ payload = buffer.split(separator);
131
+ buffer = payload.pop() || "";
132
+
133
+ yield* payload;
134
+ }
135
+ }
136
+
137
+ if (buffer) {
138
+ yield buffer;
139
+ }
140
+
141
+ return new Promise((resolve, reject) => {
142
+ subprocess.on("error", (err) => {
143
+ reject(new Error(`Process ${command} failed to spawn`));
144
+ });
145
+ subprocess.on("close", (code, signal) => {
146
+ const chunksToString = (chunks: Uint8Array[]): string =>
147
+ Buffer.concat(chunks).toString(encoding ?? "utf8");
148
+ const stderr = chunksToString(stderrChunks);
149
+ if (code === 0 || !strict) {
150
+ resolve({
151
+ code,
152
+ signal,
153
+ stderr,
154
+ } as ExecResult<Strict>);
155
+ } else {
156
+ reject(
157
+ new Error(
158
+ `Process ${[command, ...args].join(" ")} exited ${code !== null ? `with code ${code}` : `with signal ${signal || ""}`}:\nstderr: ${stderr}`,
159
+ ),
160
+ );
161
+ }
162
+ });
163
+ });
164
+ }
165
+
166
+ export const execCommand = (
167
+ workspace: Workspace,
168
+ commandAndArgs: string[],
169
+ stdo: "inherit" | "pipe" = "pipe",
170
+ ): ReturnType<typeof execvp> => {
171
+ const [command, ...args] = commandAndArgs;
172
+ if (command === undefined) {
173
+ throw new Error("Command is required");
174
+ }
175
+ return execvp(command, args, {
176
+ cwd: workspace.cwd,
177
+ strict: true,
178
+ stdo,
179
+ });
180
+ };
181
+
182
+ export const execCommandWithInput = (
183
+ workspace: Workspace,
184
+ commandAndArgs: string[],
185
+ input: string,
186
+ stdo: "inherit" | "pipe" = "pipe",
187
+ ): ReturnType<typeof execvp> => {
188
+ const [command, ...args] = commandAndArgs;
189
+ if (command === undefined) {
190
+ throw new Error("Command is required");
191
+ }
192
+ return execvp(command, args, {
193
+ cwd: workspace.cwd,
194
+ strict: true,
195
+ stdo,
196
+ input,
197
+ });
198
+ };
199
+
200
+ export const execCommandStreamStdout = (
201
+ workspace: Workspace,
202
+ commandAndArgs: string[],
203
+ separator = "\n",
204
+ ): AsyncGenerator<string, ExecResult<true>, unknown> => {
205
+ const [command, ...args] = commandAndArgs;
206
+ if (command === undefined) {
207
+ throw new Error("Command is required");
208
+ }
209
+ return spawnStreamStdout(command, args, {
210
+ cwd: workspace.cwd,
211
+ strict: true,
212
+ separator,
213
+ });
214
+ };
@@ -0,0 +1,11 @@
1
+ export async function asyncIterableToArray<T>(
2
+ asyncIterable: AsyncIterable<T>,
3
+ ): Promise<T[]> {
4
+ const result: T[] = [];
5
+
6
+ for await (const value of asyncIterable) {
7
+ result.push(value);
8
+ }
9
+
10
+ return result;
11
+ }
@@ -0,0 +1,193 @@
1
+ import semver from "semver";
2
+ import { UsageError } from "./UsageError.ts";
3
+ import { execCommand, execCommandStreamStdout } from "./execCommand.ts";
4
+ import type { Workspace } from "./packageUtils.ts";
5
+
6
+ export const getGitCurrentBranch = async (
7
+ workspace: Workspace,
8
+ ): Promise<string> => {
9
+ const { stdout } = await execCommand(workspace, [
10
+ "git",
11
+ "rev-parse",
12
+ "--abbrev-ref",
13
+ "HEAD",
14
+ ]);
15
+ const currentBranch = stdout.trim();
16
+
17
+ if (currentBranch === "HEAD") {
18
+ throw new UsageError("HEAD is detached. Please checkout a branch.");
19
+ }
20
+
21
+ return currentBranch;
22
+ };
23
+
24
+ export const createGitCommit = async (
25
+ workspace: Workspace,
26
+ commitMessage: string,
27
+ ): Promise<void> => {
28
+ await execCommand(workspace, ["git", "add", "-A"]);
29
+
30
+ await execCommand(workspace, [
31
+ "git",
32
+ "commit",
33
+ "--no-verify",
34
+ "-m",
35
+ commitMessage,
36
+ ]);
37
+ };
38
+
39
+ export const createGitTag = async (
40
+ workspace: Workspace,
41
+ newTag: string,
42
+ ): Promise<void> => {
43
+ await execCommand(workspace, ["git", "tag", newTag, "-m", newTag]);
44
+ };
45
+
46
+ export const pushCommitsAndTags = async (
47
+ workspace: Workspace,
48
+ gitRemote: string,
49
+ currentBranch: string,
50
+ ): Promise<void> => {
51
+ await execCommand(workspace, [
52
+ "git",
53
+ "push",
54
+ "--follow-tags",
55
+ "--no-verify",
56
+ "--atomic",
57
+ gitRemote,
58
+ currentBranch,
59
+ ]);
60
+ };
61
+
62
+ export const isBehindRemote = async (
63
+ workspace: Workspace,
64
+ gitRemote: string,
65
+ currentBranch: string,
66
+ ): Promise<boolean> => {
67
+ await execCommand(workspace, ["git", "remote", "update", gitRemote]);
68
+ const { stdout } = await execCommand(workspace, [
69
+ "git",
70
+ "rev-list",
71
+ "--left-right",
72
+ "--count",
73
+ `${gitRemote}/${currentBranch}..${currentBranch}`,
74
+ ]);
75
+
76
+ const [behind] = stdout.split("\t").map((val) => parseInt(val, 10));
77
+
78
+ return behind ? behind > 0 : false;
79
+ };
80
+
81
+ export const getDirtyFiles = async (workspace: Workspace): Promise<string> => {
82
+ const { stdout: dirtyFiles } = await execCommand(workspace, [
83
+ "git",
84
+ "status",
85
+ "--porcelain",
86
+ ]);
87
+ return dirtyFiles;
88
+ };
89
+
90
+ const toArray = <T>(input: T | T[]): T[] =>
91
+ Array.isArray(input) ? input : [input];
92
+
93
+ export interface GitTagVersion {
94
+ tag: string;
95
+ version: string;
96
+ }
97
+
98
+ export const getGitLatestTagVersion = async (
99
+ workspace: Workspace,
100
+ {
101
+ prefix,
102
+ skipUnstable,
103
+ since,
104
+ from = "",
105
+ to = "HEAD",
106
+ path,
107
+ }: {
108
+ prefix?: string;
109
+ skipUnstable?: boolean;
110
+ since?: Date | string;
111
+ from?: string;
112
+ to?: string;
113
+ path?: string[] | string;
114
+ } = {},
115
+ ): Promise<GitTagVersion | null> => {
116
+ // eslint-disable-next-line regexp/no-super-linear-backtracking
117
+ const tagRegex = /tag:\s*(.+?)[,)]/gi;
118
+ for await (const chunk of execCommandStreamStdout(workspace, [
119
+ "git",
120
+ "log",
121
+ "--no-color",
122
+ "--date-order",
123
+ "--format=%d",
124
+ ...(since
125
+ ? [`--since=${since instanceof Date ? since.toISOString() : since}`]
126
+ : []),
127
+ [from, to].filter(Boolean).join(".."),
128
+ ...(path ? ["--", ...toArray(path)] : []),
129
+ ])) {
130
+ const trimmed = chunk.trim();
131
+ if (!trimmed) continue;
132
+ const matches = trimmed.matchAll(tagRegex);
133
+ for (const match of matches) {
134
+ const tag = match[1];
135
+ if (!tag) continue;
136
+ if (prefix && !tag.startsWith(prefix)) {
137
+ continue;
138
+ }
139
+ const version = prefix ? tag.slice(prefix.length) : tag;
140
+ if (!semver.valid(version)) {
141
+ continue;
142
+ }
143
+ if (skipUnstable && semver.prerelease(version)) {
144
+ continue;
145
+ }
146
+ return { tag, version };
147
+ }
148
+ }
149
+
150
+ return null;
151
+ };
152
+
153
+ const GIT_COMMIT_SEPARATOR =
154
+ "------------------------ >8 ------------------------";
155
+
156
+ export const getGitCommits = (
157
+ workspace: Workspace,
158
+ {
159
+ path,
160
+ from = "",
161
+ to = "HEAD",
162
+ format = "%B",
163
+ reverse,
164
+ merges,
165
+ since,
166
+ }: {
167
+ format?: string;
168
+ path?: string[] | string;
169
+ from?: string;
170
+ to?: string;
171
+ reverse?: boolean;
172
+ merges?: boolean;
173
+ since?: Date | string;
174
+ } = {},
175
+ ) => {
176
+ return execCommandStreamStdout(
177
+ workspace,
178
+ [
179
+ "git",
180
+ "log",
181
+ `--format=${format}%n${GIT_COMMIT_SEPARATOR}`,
182
+ ...(since
183
+ ? [`--since=${since instanceof Date ? since.toISOString() : since}`]
184
+ : []),
185
+ ...(reverse ? ["--reverse"] : []),
186
+ ...(merges ? ["--merges"] : []),
187
+ ...(merges === false ? ["--no-merges"] : []),
188
+ [from, to].filter(Boolean).join(".."),
189
+ ...(path ? ["--", ...toArray(path)] : []),
190
+ ],
191
+ GIT_COMMIT_SEPARATOR,
192
+ );
193
+ };
@@ -0,0 +1,114 @@
1
+ import { UsageError } from "./UsageError.ts";
2
+ import { execCommandWithInput } from "./execCommand.ts";
3
+ import type { Workspace } from "./packageUtils.ts";
4
+
5
+ export async function ensureGhCliAvailable(
6
+ workspace: Workspace,
7
+ ): Promise<void> {
8
+ const { GH_TOKEN } = process.env;
9
+
10
+ if (!GH_TOKEN) {
11
+ throw new UsageError('"GH_TOKEN" environment variable required');
12
+ }
13
+
14
+ try {
15
+ // Use existing execCommand helper pattern by spawning gh --version
16
+ // We import execCommand lazily to avoid cycles when utils are used elsewhere
17
+ const { execCommand } = await import("./execCommand.ts");
18
+ await execCommand(workspace, ["gh", "--version"]);
19
+ } catch {
20
+ throw new UsageError(
21
+ 'The "gh" CLI is required. Please install GitHub CLI: https://cli.github.com/',
22
+ );
23
+ }
24
+ }
25
+
26
+ interface ParsedGithubUrl {
27
+ username: string;
28
+ reponame: string;
29
+ }
30
+
31
+ const githubRegex = /^https?:\/\/github.com\/([^#/]+)\/([^#/]+?)(?:\.git)?$/;
32
+ export const parseGithubRepoUrl = (workspace: Workspace): ParsedGithubUrl => {
33
+ const repository = workspace.pkg.repository;
34
+
35
+ const url: string | undefined =
36
+ typeof repository === "string" ? repository : repository?.url;
37
+
38
+ if (!url) {
39
+ throw new Error(
40
+ "No repository URL found in manifest. Please add one and try again. https://docs.npmjs.com/cli/v9/configuring-npm/package-json#repository",
41
+ );
42
+ }
43
+ const match = githubRegex.exec(url);
44
+
45
+ if (!match) {
46
+ throw new Error(`Invalid GitHub repository URL: "${url}"`);
47
+ }
48
+ const [, username, reponame] = match;
49
+ if (!username || !reponame) {
50
+ throw new Error(`Invalid GitHub repository URL: "${url}"`);
51
+ }
52
+ return { username, reponame };
53
+ };
54
+
55
+ const extractHostnameFromGheApiUrl = (gheApiUrl: string): string | null => {
56
+ try {
57
+ const url = new URL(gheApiUrl);
58
+ return url.hostname || null;
59
+ } catch {
60
+ // fallback: try to strip protocol and path
61
+ const m = /^https?:\/\/([^/]+)/.exec(gheApiUrl);
62
+ return m?.[1] || null;
63
+ }
64
+ };
65
+
66
+ export const createGhRelease = async (
67
+ workspace: Workspace,
68
+ opts: {
69
+ parsedRepoUrl: ParsedGithubUrl;
70
+ tag: string;
71
+ body: string;
72
+ prerelease: boolean;
73
+ },
74
+ ): Promise<void> => {
75
+ const { parsedRepoUrl, tag, body, prerelease } = opts;
76
+
77
+ const { GH_TOKEN, GHE_API_URL } = process.env;
78
+
79
+ if (!GH_TOKEN) {
80
+ await ensureGhCliAvailable(workspace);
81
+ return;
82
+ }
83
+
84
+ // Determine hostname for GHE if provided
85
+ const args: string[] = [
86
+ "release",
87
+ "create",
88
+ tag,
89
+ "--repo",
90
+ `${parsedRepoUrl.username}/${parsedRepoUrl.reponame}`,
91
+ "--notes",
92
+ "-",
93
+ ];
94
+
95
+ if (prerelease) {
96
+ args.push("--prerelease");
97
+ }
98
+
99
+ if (GHE_API_URL) {
100
+ const hostname = extractHostnameFromGheApiUrl(GHE_API_URL);
101
+ if (hostname) {
102
+ args.push("--hostname", hostname);
103
+ }
104
+ }
105
+
106
+ try {
107
+ await execCommandWithInput(workspace, ["gh", ...args], body);
108
+ } catch (error) {
109
+ // execCommandWithInput will include stdout/stderr in its error message when strict
110
+ throw new Error(
111
+ `gh release create failed: ${String(error instanceof Error ? error.message : error)}`,
112
+ );
113
+ }
114
+ };
@@ -0,0 +1,7 @@
1
+ export function getMapArrayItemForKey<K, V>(map: Map<K, V[]>, key: K): V[] {
2
+ let value: V[] | undefined = map.get(key);
3
+ if (value === undefined) {
4
+ map.set(key, (value = [] as V[]));
5
+ }
6
+ return value;
7
+ }