@wpelevator/wp-release 0.5.0 → 0.6.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.
@@ -0,0 +1,72 @@
1
+ export type { ChangesetConfig } from './changeset-config.ts';
2
+ export declare const CHANGESET_BUMPS: readonly ["patch", "minor", "major"];
3
+ export type ChangesetBump = (typeof CHANGESET_BUMPS)[number];
4
+ export type ChangesetPackages = Record<string, ChangesetBump>;
5
+ /** A parsed Changesets-compatible Markdown file. */
6
+ export interface Changeset {
7
+ /** File name relative to .changeset/, including the .md extension. */
8
+ fileName: string;
9
+ /** Package names and their requested version bumps; empty for a no-release changeset. */
10
+ packages: ChangesetPackages;
11
+ /** Customer-facing release note. */
12
+ summary: string;
13
+ }
14
+ export type ChangesetPackageType = 'plugin' | 'theme' | 'npm' | 'other';
15
+ /** A package discovered from repository configuration and its version files. */
16
+ export interface ChangesetPackage {
17
+ /** Changeset-facing identifier (package.json name or configured path id). */
18
+ name: string;
19
+ /** Absolute package directory. */
20
+ directory: string;
21
+ /** Package path relative to the configured root. */
22
+ path: string;
23
+ type: ChangesetPackageType;
24
+ /** Version read from the package's authoritative version source. */
25
+ currentVersion: string;
26
+ }
27
+ /** One package's proposed release in a release plan. */
28
+ export interface PackageRelease {
29
+ package: ChangesetPackage;
30
+ bump: ChangesetBump;
31
+ currentVersion: string;
32
+ nextVersion: string;
33
+ changesets: Changeset[];
34
+ }
35
+ /** Deterministic release plan assembled from pending changesets. */
36
+ export interface ChangesetReleasePlan {
37
+ changesets: Changeset[];
38
+ releases: PackageRelease[];
39
+ }
40
+ /**
41
+ * Parse the supported subset of Changesets' Markdown format.
42
+ *
43
+ * @param contents Markdown contents.
44
+ * @param fileName File name relative to .changeset/.
45
+ * @return Parsed changeset.
46
+ * @throws {ValidationError} If the frontmatter or package bumps are invalid.
47
+ */
48
+ export declare function parseChangeset(contents: string, fileName?: string): Changeset;
49
+ /**
50
+ * Serialize a changeset using the standard Markdown frontmatter format.
51
+ *
52
+ * @param changeset Parsed changeset values.
53
+ * @return Markdown contents with a trailing newline.
54
+ */
55
+ export declare function formatChangeset(changeset: Omit<Changeset, 'fileName'>): string;
56
+ /**
57
+ * Return the strongest bump requested for a package across changesets.
58
+ *
59
+ * @param changesets Pending changesets.
60
+ * @param packageName Package identifier.
61
+ * @return Highest bump, or undefined when no changeset mentions the package.
62
+ */
63
+ export declare function getChangesetBump(changesets: Changeset[], packageName: string): ChangesetBump | undefined;
64
+ /**
65
+ * Calculate the next stable semantic version using literal SemVer bump rules.
66
+ *
67
+ * @param currentVersion Current package version.
68
+ * @param bump Requested bump.
69
+ * @return Next version.
70
+ * @throws {ValidationError} If currentVersion is not a stable x.y.z version.
71
+ */
72
+ export declare function getNextVersion(currentVersion: string, bump: ChangesetBump): string;
@@ -0,0 +1,103 @@
1
+ import { ValidationError } from "./errors.js";
2
+ export const CHANGESET_BUMPS = ['patch', 'minor', 'major'];
3
+ const BUMP_RANK = {
4
+ patch: 0,
5
+ minor: 1,
6
+ major: 2,
7
+ };
8
+ /**
9
+ * Parse the supported subset of Changesets' Markdown format.
10
+ *
11
+ * @param contents Markdown contents.
12
+ * @param fileName File name relative to .changeset/.
13
+ * @return Parsed changeset.
14
+ * @throws {ValidationError} If the frontmatter or package bumps are invalid.
15
+ */
16
+ export function parseChangeset(contents, fileName = 'changeset.md') {
17
+ const lines = contents.replace(/^\uFEFF/, '').split(/\r?\n/);
18
+ if ('---' !== lines[0]) {
19
+ throw new ValidationError(`Changeset ${fileName} must start with YAML frontmatter (---).`);
20
+ }
21
+ const closingLine = lines.indexOf('---', 1);
22
+ if (-1 === closingLine) {
23
+ throw new ValidationError(`Changeset ${fileName} is missing the closing frontmatter delimiter.`);
24
+ }
25
+ const packages = {};
26
+ const frontmatter = lines.slice(1, closingLine);
27
+ frontmatter.forEach((line, index) => {
28
+ if (line.trim()) {
29
+ const match = /^\s*(?:"((?:[^"\\]|\\.)+)"|'([^']+)'|([^:#]+))\s*:\s*(patch|minor|major)\s*$/.exec(line);
30
+ if (!match) {
31
+ throw new ValidationError(`Invalid package bump on line ${index + 2} of changeset ${fileName}.`);
32
+ }
33
+ const name = (match[1] ?? match[2] ?? match[3] ?? '')
34
+ .replaceAll('\\"', '"')
35
+ .trim();
36
+ if (!name) {
37
+ throw new ValidationError(`Package name is empty on line ${index + 2} of changeset ${fileName}.`);
38
+ }
39
+ if (Object.hasOwn(packages, name)) {
40
+ throw new ValidationError(`Package "${name}" appears more than once in changeset ${fileName}.`);
41
+ }
42
+ packages[name] = match[4];
43
+ }
44
+ });
45
+ return {
46
+ fileName,
47
+ packages,
48
+ summary: lines
49
+ .slice(closingLine + 1)
50
+ .join('\n')
51
+ .trim(),
52
+ };
53
+ }
54
+ /**
55
+ * Serialize a changeset using the standard Markdown frontmatter format.
56
+ *
57
+ * @param changeset Parsed changeset values.
58
+ * @return Markdown contents with a trailing newline.
59
+ */
60
+ export function formatChangeset(changeset) {
61
+ const entries = Object.entries(changeset.packages).sort((a, b) => a[0].localeCompare(b[0]));
62
+ const frontmatter = entries
63
+ .map(([name, bump]) => `${JSON.stringify(name)}: ${bump}`)
64
+ .join('\n');
65
+ return `---\n${frontmatter}\n---\n\n${changeset.summary.trim()}\n`;
66
+ }
67
+ /**
68
+ * Return the strongest bump requested for a package across changesets.
69
+ *
70
+ * @param changesets Pending changesets.
71
+ * @param packageName Package identifier.
72
+ * @return Highest bump, or undefined when no changeset mentions the package.
73
+ */
74
+ export function getChangesetBump(changesets, packageName) {
75
+ const bumps = changesets
76
+ .map((changeset) => changeset.packages[packageName])
77
+ .filter((bump) => Boolean(bump));
78
+ return bumps.sort((a, b) => BUMP_RANK[b] - BUMP_RANK[a])[0];
79
+ }
80
+ /**
81
+ * Calculate the next stable semantic version using literal SemVer bump rules.
82
+ *
83
+ * @param currentVersion Current package version.
84
+ * @param bump Requested bump.
85
+ * @return Next version.
86
+ * @throws {ValidationError} If currentVersion is not a stable x.y.z version.
87
+ */
88
+ export function getNextVersion(currentVersion, bump) {
89
+ if (!CHANGESET_BUMPS.includes(bump)) {
90
+ throw new ValidationError(`Invalid bump "${bump}"; expected patch, minor or major.`);
91
+ }
92
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(currentVersion);
93
+ if (!match) {
94
+ throw new ValidationError(`Cannot bump "${currentVersion}": expected a stable semantic version in x.y.z format.`);
95
+ }
96
+ if ('major' === bump) {
97
+ return `${Number(match[1]) + 1}.0.0`;
98
+ }
99
+ if ('minor' === bump) {
100
+ return `${match[1]}.${Number(match[2]) + 1}.0`;
101
+ }
102
+ return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
103
+ }
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ChecksumsResult } from './commands/checksums.ts';
2
2
  import type { SignResult } from './commands/sign.ts';
3
3
  import type { ZipResult } from './commands/zip.ts';
4
+ import type { ChangesetVersionResult } from './commands/changeset-version.ts';
4
5
  import { Logger } from './logger.ts';
5
6
  import type { OutputStream } from './logger.ts';
6
7
  import type { Command, CommandOutcome } from './commands/command.ts';
@@ -43,23 +44,29 @@ export interface CommandResults {
43
44
  zip: ZipResult;
44
45
  checksums: ChecksumsResult;
45
46
  sign: SignResult;
47
+ 'changeset-add': import('./commands/changeset-add.ts').ChangesetAddResult;
48
+ 'changeset-status': import('./commands/changeset-status.ts').ChangesetStatusResult;
49
+ 'changeset-version': ChangesetVersionResult;
50
+ 'changeset-tag': import('./changeset-release.ts').ChangesetTagResult;
46
51
  }
47
52
  /**
48
53
  * Run a command by name with the given arguments, for wrappers like the
49
54
  * GitHub Actions that need the result and the thrown errors instead of an
50
55
  * exit code.
51
56
  *
52
- * @param name Command name, such as zip.
53
- * @param args Arguments after the command name.
54
- * @param context Working directory and logger.
55
- * @param context.cwd Working directory that relative paths resolve against.
56
- * @param context.logger Logger for the command output.
57
+ * @param name Command name, such as zip.
58
+ * @param args Arguments after the command name.
59
+ * @param context Working directory and logger.
60
+ * @param context.cwd Working directory that relative paths resolve against.
61
+ * @param context.configFile Explicit config file path, when supplied by a wrapper.
62
+ * @param context.logger Logger for the command output.
57
63
  * @return Exit code and result of the command.
58
64
  * @throws {Error} If the command is unknown, the arguments are invalid or the command fails.
59
65
  */
60
66
  export declare function execute<Name extends keyof CommandResults>(name: Name, args: string[], context: {
61
67
  cwd: string;
62
68
  logger: Logger;
69
+ configFile?: string;
63
70
  }): Promise<CommandOutcome<CommandResults[Name]>>;
64
71
  /**
65
72
  * Run the CLI.
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { appendFileSync } from 'node:fs';
1
2
  import { parseArgs } from 'node:util';
2
3
  import zip from "./commands/zip.js";
3
4
  import checksums from "./commands/checksums.js";
@@ -10,6 +11,10 @@ import { getBoolean, getString } from "./commands/command.js";
10
11
  import { ValidationError } from "./errors.js";
11
12
  import { GitHubActionsWriter } from "./github-actions-writer.js";
12
13
  import { logError } from "./report.js";
14
+ import changesetAdd from "./commands/changeset-add.js";
15
+ import changesetStatus from "./commands/changeset-status.js";
16
+ import changesetVersion from "./commands/changeset-version.js";
17
+ import changesetTag from "./commands/changeset-tag.js";
13
18
  export const EXIT_SUCCESS = 0;
14
19
  export const EXIT_FAILURE = 1;
15
20
  export const EXIT_INVALID = 2;
@@ -20,6 +25,10 @@ export const commands = [
20
25
  verify,
21
26
  versionCheck,
22
27
  deploySvn,
28
+ changesetAdd,
29
+ changesetStatus,
30
+ changesetVersion,
31
+ changesetTag,
23
32
  ];
24
33
  const globalArgs = {
25
34
  help: { type: 'boolean', short: 'h' },
@@ -28,6 +37,8 @@ const globalArgs = {
28
37
  'log-format': { type: 'string' },
29
38
  ci: { type: 'boolean' },
30
39
  json: { type: 'boolean' },
40
+ 'github-output': { type: 'boolean' },
41
+ config: { type: 'string' },
31
42
  };
32
43
  const globalOptions = `Global options:
33
44
  -h, --help Show help for the command
@@ -37,7 +48,10 @@ const globalOptions = `Global options:
37
48
  annotations and job summaries (default: github
38
49
  when GITHUB_ACTIONS=true, otherwise text)
39
50
  --ci Same as --log-format github
40
- --json Print the result as JSON`;
51
+ --json Print the result as JSON
52
+ --github-output Set the result as JSON to the "result" output of
53
+ the GitHub Actions step
54
+ --config <path> Path to a JavaScript release config file`;
41
55
  /**
42
56
  * Get the top-level help text listing all commands.
43
57
  *
@@ -133,6 +147,21 @@ function createLogWriter(values, streams) {
133
147
  }
134
148
  return create(streams);
135
149
  }
150
+ /**
151
+ * Get the GitHub Actions step output file for --github-output, checked before
152
+ * the command runs.
153
+ *
154
+ * @param values Parsed option values.
155
+ * @return Output file path, or undefined without --github-output.
156
+ * @throws {ValidationError} For --github-output without GITHUB_OUTPUT.
157
+ */
158
+ function getGitHubOutputFile(values) {
159
+ const file = process.env.GITHUB_OUTPUT;
160
+ if (getBoolean(values, 'github-output') && !file) {
161
+ throw new ValidationError('--github-output requires the GITHUB_OUTPUT environment variable of GitHub Actions.');
162
+ }
163
+ return getBoolean(values, 'github-output') ? file : undefined;
164
+ }
136
165
  /**
137
166
  * Parse the arguments of a command and run it.
138
167
  *
@@ -155,6 +184,11 @@ async function runCommand(command, args, io) {
155
184
  options: { ...globalArgs, ...command.options },
156
185
  allowPositionals: true,
157
186
  });
187
+ const configFile = getString(values, 'config');
188
+ if ('' === configFile?.trim()) {
189
+ throw new ValidationError('--config <path> must not be empty.');
190
+ }
191
+ const githubOutputFile = getGitHubOutputFile(values);
158
192
  logger = new Logger({
159
193
  writer: createLogWriter(values, { stdout, stderr }),
160
194
  quiet: getBoolean(values, 'quiet') || getBoolean(values, 'json'),
@@ -164,11 +198,16 @@ async function runCommand(command, args, io) {
164
198
  values,
165
199
  positionals,
166
200
  cwd,
201
+ ...(configFile ? { configFile } : {}),
167
202
  logger,
168
203
  });
169
204
  if (getBoolean(values, 'json')) {
170
205
  stdout.write(JSON.stringify(result, null, '\t') + '\n');
171
206
  }
207
+ if (githubOutputFile) {
208
+ // Compact JSON has no line breaks, so it fits a single output line.
209
+ appendFileSync(githubOutputFile, `result=${JSON.stringify(result)}\n`);
210
+ }
172
211
  return exitCode;
173
212
  }
174
213
  catch (error) {
@@ -181,11 +220,12 @@ async function runCommand(command, args, io) {
181
220
  * GitHub Actions that need the result and the thrown errors instead of an
182
221
  * exit code.
183
222
  *
184
- * @param name Command name, such as zip.
185
- * @param args Arguments after the command name.
186
- * @param context Working directory and logger.
187
- * @param context.cwd Working directory that relative paths resolve against.
188
- * @param context.logger Logger for the command output.
223
+ * @param name Command name, such as zip.
224
+ * @param args Arguments after the command name.
225
+ * @param context Working directory and logger.
226
+ * @param context.cwd Working directory that relative paths resolve against.
227
+ * @param context.configFile Explicit config file path, when supplied by a wrapper.
228
+ * @param context.logger Logger for the command output.
189
229
  * @return Exit code and result of the command.
190
230
  * @throws {Error} If the command is unknown, the arguments are invalid or the command fails.
191
231
  */
@@ -196,14 +236,19 @@ export async function execute(name, args, context) {
196
236
  }
197
237
  const { values, positionals } = parseArgs({
198
238
  args,
199
- options: command.options,
239
+ options: { ...globalArgs, ...command.options },
200
240
  allowPositionals: true,
201
241
  });
242
+ const configFile = getString(values, 'config') ?? context.configFile;
243
+ if ('' === configFile?.trim()) {
244
+ throw new ValidationError('--config <path> must not be empty.');
245
+ }
202
246
  // The command table is keyed by name, so the result matches CommandResults.
203
247
  return command.run({
204
248
  values,
205
249
  positionals,
206
250
  ...context,
251
+ ...(configFile ? { configFile } : {}),
207
252
  });
208
253
  }
209
254
  /**
@@ -0,0 +1,37 @@
1
+ export interface ChangesetAddResult {
2
+ file: string;
3
+ packages: Record<string, string>;
4
+ }
5
+ declare const _default: {
6
+ name: string;
7
+ usage: string;
8
+ summary: string;
9
+ help: string;
10
+ options: {
11
+ readonly package: {
12
+ readonly type: "string";
13
+ readonly multiple: true;
14
+ };
15
+ readonly bump: {
16
+ readonly type: "string";
17
+ readonly multiple: true;
18
+ };
19
+ readonly summary: {
20
+ readonly type: "string";
21
+ };
22
+ readonly empty: {
23
+ readonly type: "boolean";
24
+ };
25
+ readonly name: {
26
+ readonly type: "string";
27
+ };
28
+ };
29
+ run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
30
+ exitCode: number;
31
+ result: {
32
+ file: string;
33
+ packages: import("../changesets.ts").ChangesetPackages;
34
+ };
35
+ }>;
36
+ };
37
+ export default _default;
@@ -0,0 +1,35 @@
1
+ import { addChangeset } from "../changeset-release.js";
2
+ import { parseChangesetAddArgs, changesetAddArgs } from "./changeset-args.js";
3
+ export default {
4
+ name: 'changeset-add',
5
+ usage: 'changeset-add (--empty | --package <name> --bump <bump> ...) [options]',
6
+ summary: 'Add a package release note for the next release PR.',
7
+ help: `Options:
8
+ --package <name> Package identifier (repeatable)
9
+ --bump <bump> patch, minor or major (repeatable, in package order)
10
+ --summary <text> Customer-facing release note
11
+ --empty Record that this change does not need a release
12
+ --name <name> Changeset filename (default: random identifier)
13
+
14
+ Use --package and --bump pairs for package releases, or --empty for changes
15
+ that should pass the status check without producing a release.`,
16
+ options: changesetAddArgs,
17
+ async run({ values, cwd, configFile, logger }) {
18
+ const input = parseChangesetAddArgs(values);
19
+ const file = await addChangeset({
20
+ cwd,
21
+ ...(configFile ? { configFile } : {}),
22
+ packages: input.packages,
23
+ summary: input.summary,
24
+ ...(input.name ? { name: input.name } : {}),
25
+ });
26
+ logger.info(`Created ${file}.`);
27
+ return {
28
+ exitCode: 0,
29
+ result: {
30
+ file,
31
+ packages: input.packages,
32
+ },
33
+ };
34
+ },
35
+ };
@@ -0,0 +1,118 @@
1
+ import type { ChangesetPackages } from '../changesets.ts';
2
+ import type { ParsedValues } from './command.ts';
3
+ /** Parsed input for `changeset-add`. */
4
+ export interface ChangesetAddArgs {
5
+ packages: ChangesetPackages;
6
+ summary: string;
7
+ empty: boolean;
8
+ name?: string;
9
+ }
10
+ /** Parse-args options for `changeset-add`. */
11
+ export declare const changesetAddArgs: {
12
+ readonly package: {
13
+ readonly type: "string";
14
+ readonly multiple: true;
15
+ };
16
+ readonly bump: {
17
+ readonly type: "string";
18
+ readonly multiple: true;
19
+ };
20
+ readonly summary: {
21
+ readonly type: "string";
22
+ };
23
+ readonly empty: {
24
+ readonly type: "boolean";
25
+ };
26
+ readonly name: {
27
+ readonly type: "string";
28
+ };
29
+ };
30
+ /**
31
+ * Parse and validate the `changeset-add` options.
32
+ *
33
+ * @param values Parsed CLI options.
34
+ * @return Normalized changeset input.
35
+ * @throws {ValidationError} If required options are missing or conflicting.
36
+ */
37
+ export declare function parseChangesetAddArgs(values: ParsedValues): ChangesetAddArgs;
38
+ /** Parse-args options for `changeset-status`. */
39
+ export declare const changesetStatusArgs: {
40
+ readonly since: {
41
+ readonly type: "string";
42
+ };
43
+ };
44
+ /** Parsed input for `changeset-status`. */
45
+ export interface ChangesetStatusArgs {
46
+ since?: string;
47
+ }
48
+ /**
49
+ * Parse the `changeset-status` options.
50
+ *
51
+ * @param values Parsed CLI options.
52
+ * @return Normalized status input.
53
+ * @throws {ValidationError} If --since is empty.
54
+ */
55
+ export declare function parseChangesetStatusArgs(values: ParsedValues): ChangesetStatusArgs;
56
+ /** Parse-args options for `changeset-version`. */
57
+ export declare const changesetVersionArgs: {
58
+ readonly 'dry-run': {
59
+ readonly type: "boolean";
60
+ };
61
+ readonly date: {
62
+ readonly type: "string";
63
+ };
64
+ };
65
+ /** Parsed input for `changeset-version`. */
66
+ export interface ChangesetVersionArgs {
67
+ dryRun: boolean;
68
+ date?: string;
69
+ }
70
+ /**
71
+ * Validate and normalize a release date option.
72
+ *
73
+ * @param value Value from --date.
74
+ * @return A validated ISO date or undefined.
75
+ * @throws {ValidationError} If the date is not a real YYYY-MM-DD date.
76
+ */
77
+ export declare function parseChangesetDate(value: string | undefined): string | undefined;
78
+ /**
79
+ * Parse the `changeset-version` options.
80
+ *
81
+ * @param values Parsed CLI options.
82
+ * @return Normalized versioning input.
83
+ * @throws {ValidationError} If --date is invalid.
84
+ */
85
+ export declare function parseChangesetVersionArgs(values: ParsedValues): ChangesetVersionArgs;
86
+ /** Parse-args options for `changeset-tag`. */
87
+ export declare const changesetTagArgs: {
88
+ readonly push: {
89
+ readonly type: "boolean";
90
+ };
91
+ readonly 'dry-run': {
92
+ readonly type: "boolean";
93
+ };
94
+ readonly untagged: {
95
+ readonly type: "boolean";
96
+ };
97
+ readonly 'github-release': {
98
+ readonly type: "boolean";
99
+ };
100
+ readonly since: {
101
+ readonly type: "string";
102
+ };
103
+ };
104
+ /** Parsed input for `changeset-tag`. */
105
+ export interface ChangesetTagArgs {
106
+ push: boolean;
107
+ dryRun: boolean;
108
+ untagged: boolean;
109
+ githubRelease: boolean;
110
+ since?: string;
111
+ }
112
+ /**
113
+ * Parse the `changeset-tag` options.
114
+ *
115
+ * @param values Parsed CLI options.
116
+ * @return Normalized tag input.
117
+ */
118
+ export declare function parseChangesetTagArgs(values: ParsedValues): ChangesetTagArgs;
@@ -0,0 +1,144 @@
1
+ import { ValidationError } from "../errors.js";
2
+ import { CHANGESET_BUMPS } from "../changesets.js";
3
+ import { getBoolean, getString, getStrings } from "./command.js";
4
+ /** Parse-args options for `changeset-add`. */
5
+ export const changesetAddArgs = {
6
+ package: { type: 'string', multiple: true },
7
+ bump: { type: 'string', multiple: true },
8
+ summary: { type: 'string' },
9
+ empty: { type: 'boolean' },
10
+ name: { type: 'string' },
11
+ };
12
+ /**
13
+ * Parse and validate the `changeset-add` options.
14
+ *
15
+ * @param values Parsed CLI options.
16
+ * @return Normalized changeset input.
17
+ * @throws {ValidationError} If required options are missing or conflicting.
18
+ */
19
+ export function parseChangesetAddArgs(values) {
20
+ const names = getStrings(values, 'package');
21
+ const bumps = getStrings(values, 'bump');
22
+ const summary = getString(values, 'summary')?.trim();
23
+ const empty = getBoolean(values, 'empty');
24
+ const name = getString(values, 'name');
25
+ if (empty && (names.length || bumps.length || summary)) {
26
+ throw new ValidationError('--empty cannot be combined with --package, --bump or --summary.');
27
+ }
28
+ if (!empty && (!names.length || names.length !== bumps.length)) {
29
+ throw new ValidationError('Pass matching repeated --package <name> and --bump <patch|minor|major> options, or use --empty.');
30
+ }
31
+ if (!empty && !summary) {
32
+ throw new ValidationError('--summary <text> is required unless --empty is set.');
33
+ }
34
+ if (name && !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
35
+ throw new ValidationError('--name must contain only letters, numbers, dots, underscores or hyphens.');
36
+ }
37
+ const packages = {};
38
+ names.forEach((packageName, index) => {
39
+ const bump = bumps[index];
40
+ if (Object.hasOwn(packages, packageName)) {
41
+ throw new ValidationError(`Package "${packageName}" was passed more than once.`);
42
+ }
43
+ if (!CHANGESET_BUMPS.includes(bump)) {
44
+ throw new ValidationError(`Invalid bump "${bump}" for package "${packageName}"; expected patch, minor or major.`);
45
+ }
46
+ packages[packageName] = bump;
47
+ });
48
+ return {
49
+ packages,
50
+ summary: summary ?? '',
51
+ empty,
52
+ ...(name ? { name } : {}),
53
+ };
54
+ }
55
+ /** Parse-args options for `changeset-status`. */
56
+ export const changesetStatusArgs = {
57
+ since: { type: 'string' },
58
+ };
59
+ /**
60
+ * Parse the `changeset-status` options.
61
+ *
62
+ * @param values Parsed CLI options.
63
+ * @return Normalized status input.
64
+ * @throws {ValidationError} If --since is empty.
65
+ */
66
+ export function parseChangesetStatusArgs(values) {
67
+ const since = getString(values, 'since');
68
+ if ('' === since?.trim()) {
69
+ throw new ValidationError('--since <ref> must not be empty.');
70
+ }
71
+ return since ? { since } : {};
72
+ }
73
+ /** Parse-args options for `changeset-version`. */
74
+ export const changesetVersionArgs = {
75
+ 'dry-run': { type: 'boolean' },
76
+ date: { type: 'string' },
77
+ };
78
+ /**
79
+ * Validate and normalize a release date option.
80
+ *
81
+ * @param value Value from --date.
82
+ * @return A validated ISO date or undefined.
83
+ * @throws {ValidationError} If the date is not a real YYYY-MM-DD date.
84
+ */
85
+ export function parseChangesetDate(value) {
86
+ if (undefined === value) {
87
+ return undefined;
88
+ }
89
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
90
+ const parsed = new Date(`${value}T00:00:00.000Z`);
91
+ if (!match ||
92
+ Number.isNaN(parsed.getTime()) ||
93
+ parsed.toISOString().slice(0, 10) !== value) {
94
+ throw new ValidationError(`Invalid release date "${value}"; expected a real date in YYYY-MM-DD format.`);
95
+ }
96
+ return value;
97
+ }
98
+ /**
99
+ * Parse the `changeset-version` options.
100
+ *
101
+ * @param values Parsed CLI options.
102
+ * @return Normalized versioning input.
103
+ * @throws {ValidationError} If --date is invalid.
104
+ */
105
+ export function parseChangesetVersionArgs(values) {
106
+ const date = parseChangesetDate(getString(values, 'date'));
107
+ return {
108
+ dryRun: getBoolean(values, 'dry-run'),
109
+ ...(date ? { date } : {}),
110
+ };
111
+ }
112
+ /** Parse-args options for `changeset-tag`. */
113
+ export const changesetTagArgs = {
114
+ push: { type: 'boolean' },
115
+ 'dry-run': { type: 'boolean' },
116
+ untagged: { type: 'boolean' },
117
+ 'github-release': { type: 'boolean' },
118
+ since: { type: 'string' },
119
+ };
120
+ /**
121
+ * Parse the `changeset-tag` options.
122
+ *
123
+ * @param values Parsed CLI options.
124
+ * @return Normalized tag input.
125
+ */
126
+ export function parseChangesetTagArgs(values) {
127
+ const args = {
128
+ push: getBoolean(values, 'push'),
129
+ dryRun: getBoolean(values, 'dry-run'),
130
+ untagged: getBoolean(values, 'untagged'),
131
+ githubRelease: getBoolean(values, 'github-release'),
132
+ since: getString(values, 'since'),
133
+ };
134
+ if ('' === args.since?.trim()) {
135
+ throw new ValidationError('--since <ref> must not be empty.');
136
+ }
137
+ if (args.githubRelease && !args.push) {
138
+ throw new ValidationError('--github-release requires --push.');
139
+ }
140
+ if (args.untagged && args.push) {
141
+ throw new ValidationError('--untagged cannot be combined with --push.');
142
+ }
143
+ return args;
144
+ }