@wpelevator/wp-release 0.12.0 → 0.14.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.
Files changed (78) hide show
  1. package/README.md +150 -19
  2. package/dist/changeset-config.d.ts +91 -5
  3. package/dist/changeset-config.js +108 -11
  4. package/dist/changeset-dependents.d.ts +19 -8
  5. package/dist/changeset-dependents.js +96 -34
  6. package/dist/changeset-files.d.ts +6 -5
  7. package/dist/changeset-files.js +7 -5
  8. package/dist/changeset-packages.d.ts +55 -8
  9. package/dist/changeset-packages.js +85 -14
  10. package/dist/changeset-prompt.d.ts +119 -0
  11. package/dist/changeset-prompt.js +203 -0
  12. package/dist/changeset-release.d.ts +43 -5
  13. package/dist/changeset-release.js +99 -16
  14. package/dist/changesets.d.ts +6 -2
  15. package/dist/changesets.js +18 -12
  16. package/dist/cli.d.ts +7 -1
  17. package/dist/cli.js +51 -14
  18. package/dist/commands/args.d.ts +0 -21
  19. package/dist/commands/args.js +1 -35
  20. package/dist/commands/changeset-add.d.ts +5 -2
  21. package/dist/commands/changeset-add.js +109 -9
  22. package/dist/commands/changeset-args.d.ts +24 -5
  23. package/dist/commands/changeset-args.js +57 -14
  24. package/dist/commands/changeset-status.d.ts +3 -1
  25. package/dist/commands/changeset-status.js +2 -1
  26. package/dist/commands/changeset-tag.d.ts +1 -1
  27. package/dist/commands/changeset-tag.js +6 -3
  28. package/dist/commands/changeset-version.d.ts +7 -1
  29. package/dist/commands/changeset-version.js +17 -4
  30. package/dist/commands/checksums.js +2 -2
  31. package/dist/commands/command.d.ts +5 -0
  32. package/dist/commands/deploy-git.d.ts +24 -4
  33. package/dist/commands/deploy-git.js +57 -3
  34. package/dist/commands/deploy-svn.d.ts +24 -4
  35. package/dist/commands/deploy-svn.js +57 -2
  36. package/dist/commands/keygen.d.ts +3 -0
  37. package/dist/commands/keygen.js +6 -1
  38. package/dist/commands/package-deploys.d.ts +86 -0
  39. package/dist/commands/package-deploys.js +113 -0
  40. package/dist/commands/sign.d.ts +19 -1
  41. package/dist/commands/sign.js +47 -12
  42. package/dist/commands/verify.d.ts +3 -0
  43. package/dist/commands/verify.js +43 -24
  44. package/dist/commands/zip.d.ts +12 -8
  45. package/dist/commands/zip.js +135 -78
  46. package/dist/ed25519.d.ts +25 -0
  47. package/dist/ed25519.js +44 -0
  48. package/dist/file-selection.d.ts +21 -0
  49. package/dist/file-selection.js +35 -0
  50. package/dist/git-deploy.d.ts +32 -0
  51. package/dist/git-deploy.js +57 -1
  52. package/dist/git.d.ts +17 -0
  53. package/dist/git.js +23 -0
  54. package/dist/index.d.ts +13 -5
  55. package/dist/index.js +7 -3
  56. package/dist/minisign.d.ts +41 -0
  57. package/dist/minisign.js +128 -0
  58. package/dist/package-deploy.d.ts +133 -0
  59. package/dist/package-deploy.js +323 -0
  60. package/dist/package-zip.d.ts +52 -0
  61. package/dist/package-zip.js +78 -0
  62. package/dist/release-repository.d.ts +18 -5
  63. package/dist/release-repository.js +51 -11
  64. package/dist/release-tags.d.ts +3 -1
  65. package/dist/release-tags.js +5 -2
  66. package/dist/signing.d.ts +64 -9
  67. package/dist/signing.js +113 -40
  68. package/dist/ssh-signature.d.ts +43 -0
  69. package/dist/ssh-signature.js +156 -0
  70. package/dist/summary.d.ts +3 -3
  71. package/dist/summary.js +2 -2
  72. package/dist/tag-format.d.ts +23 -14
  73. package/dist/tag-format.js +26 -40
  74. package/dist/version-files.d.ts +8 -8
  75. package/dist/version-files.js +22 -18
  76. package/dist/versions.d.ts +28 -3
  77. package/dist/versions.js +84 -9
  78. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -43,6 +43,7 @@ const globalArgs = {
43
43
  json: { type: 'boolean' },
44
44
  'github-output': { type: 'boolean' },
45
45
  config: { type: 'string' },
46
+ 'tag-format': { type: 'string' },
46
47
  };
47
48
  const globalOptions = `Global options:
48
49
  -h, --help Show help for the command
@@ -55,7 +56,32 @@ const globalOptions = `Global options:
55
56
  --json Print the result as JSON
56
57
  --github-output Set the result as JSON to the "result" output of
57
58
  the GitHub Actions step
58
- --config <path> Path to a JavaScript release config file`;
59
+ --config <path> Path to a JavaScript release config file
60
+ --tag-format <format>
61
+ Tag format with {name}, {path} and {version} that
62
+ replaces the tagFormat of the config, like
63
+ {path}/{version}`;
64
+ /**
65
+ * Get the config file and the tag format from the global options.
66
+ *
67
+ * @param values Parsed CLI values.
68
+ * @return The --config and --tag-format values.
69
+ * @throws {ValidationError} For an empty --config or --tag-format.
70
+ */
71
+ function getConfigArgs(values) {
72
+ const configFile = getString(values, 'config');
73
+ const tagFormat = getString(values, 'tag-format');
74
+ if ('' === configFile?.trim()) {
75
+ throw new ValidationError('--config <path> must not be empty.');
76
+ }
77
+ if ('' === tagFormat?.trim()) {
78
+ throw new ValidationError('--tag-format <format> must not be empty.');
79
+ }
80
+ return {
81
+ ...(configFile ? { configFile } : {}),
82
+ ...(tagFormat ? { tagFormat } : {}),
83
+ };
84
+ }
59
85
  /**
60
86
  * Get the top-level help text listing all commands.
61
87
  *
@@ -176,11 +202,11 @@ function getGitHubOutputFile(values) {
176
202
  *
177
203
  * @param command Command definition with options and run().
178
204
  * @param args Arguments after the command name.
179
- * @param io Output streams and working directory.
205
+ * @param io Input and output streams and working directory.
180
206
  * @return Exit code.
181
207
  */
182
208
  async function runCommand(command, args, io) {
183
- const { stdout, stderr, cwd } = io;
209
+ const { stdin, stdout, stderr, cwd } = io;
184
210
  let logger = new Logger({ writer: new TextWriter({ stdout, stderr }) });
185
211
  try {
186
212
  const { values, positionals } = parseArgs({
@@ -188,22 +214,28 @@ async function runCommand(command, args, io) {
188
214
  options: { ...globalArgs, ...command.options },
189
215
  allowPositionals: true,
190
216
  });
191
- const configFile = getString(values, 'config');
192
- if ('' === configFile?.trim()) {
193
- throw new ValidationError('--config <path> must not be empty.');
194
- }
217
+ const { configFile, tagFormat } = getConfigArgs(values);
195
218
  const githubOutputFile = getGitHubOutputFile(values);
196
219
  logger = new Logger({
197
220
  writer: createLogWriter(values, { stdout, stderr }),
198
221
  quiet: getBoolean(values, 'quiet') || getBoolean(values, 'json'),
199
222
  verbose: getBoolean(values, 'verbose'),
200
223
  });
224
+ // Prompts need a person at a terminal, and would corrupt JSON output.
225
+ const interactive = true === stdin.isTTY &&
226
+ true === stdout.isTTY &&
227
+ !getBoolean(values, 'json') &&
228
+ !githubOutputFile;
201
229
  const { exitCode, result } = await command.run({
202
230
  values,
203
231
  positionals,
204
232
  cwd,
205
233
  ...(configFile ? { configFile } : {}),
234
+ ...(tagFormat ? { tagFormat } : {}),
206
235
  logger,
236
+ ...(interactive
237
+ ? { terminal: { input: stdin, output: stdout } }
238
+ : {}),
207
239
  });
208
240
  if (getBoolean(values, 'json')) {
209
241
  stdout.write(JSON.stringify(result, null, '\t') + '\n');
@@ -243,27 +275,27 @@ export async function execute(name, args, context) {
243
275
  options: { ...globalArgs, ...command.options },
244
276
  allowPositionals: true,
245
277
  });
246
- const configFile = getString(values, 'config') ?? context.configFile;
247
- if ('' === configFile?.trim()) {
248
- throw new ValidationError('--config <path> must not be empty.');
249
- }
278
+ const configArgs = getConfigArgs(values);
279
+ const configFile = configArgs.configFile ?? context.configFile;
280
+ const { tagFormat } = configArgs;
250
281
  // The command table is keyed by name, so the result matches CommandResults.
251
282
  return command.run({
252
283
  values,
253
284
  positionals,
254
285
  ...context,
255
286
  ...(configFile ? { configFile } : {}),
287
+ ...(tagFormat ? { tagFormat } : {}),
256
288
  });
257
289
  }
258
290
  /**
259
291
  * Run the CLI.
260
292
  *
261
293
  * @param args CLI arguments without the node and script paths.
262
- * @param io Output streams and working directory.
294
+ * @param io Input and output streams and working directory.
263
295
  * @return Exit code.
264
296
  */
265
297
  export async function run(args, io = {}) {
266
- const { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), } = io;
298
+ const { stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), } = io;
267
299
  const wantsHelp = args.includes('--help') || args.includes('-h');
268
300
  const command = findCommand(args);
269
301
  if (command && wantsHelp) {
@@ -271,7 +303,12 @@ export async function run(args, io = {}) {
271
303
  return EXIT_SUCCESS;
272
304
  }
273
305
  if (command) {
274
- return runCommand(command, args.slice(1), { stdout, stderr, cwd });
306
+ return runCommand(command, args.slice(1), {
307
+ stdin,
308
+ stdout,
309
+ stderr,
310
+ cwd,
311
+ });
275
312
  }
276
313
  if (0 === args.length || wantsHelp || 'help' === args[0]) {
277
314
  stdout.write(getHelp() + '\n');
@@ -1,5 +1,4 @@
1
1
  import type { ParseArgsOptionsConfig } from 'node:util';
2
- import type { FileRules } from '../file-selection.ts';
3
2
  import type { ParsedValues } from './command.ts';
4
3
  /**
5
4
  * Options shared by the commands that select files with .distignore and
@@ -40,26 +39,6 @@ export declare function checkDirectory(path: string): Promise<void>;
40
39
  * @return Whether the path is a directory.
41
40
  */
42
41
  export declare function isDirectory(path: string): Promise<boolean>;
43
- /**
44
- * Ignore a file that the command writes or reads, such as the checksums
45
- * manifest, when it's inside the selected directory.
46
- *
47
- * @param rules Matcher from loadFileRules().
48
- * @param sourceDir Selected directory.
49
- * @param path Absolute file path.
50
- * @return The same matcher.
51
- */
52
- export declare function ignoreOwnFile(rules: FileRules, sourceDir: string, path: string): FileRules;
53
- /**
54
- * Ignore the signature file that "wp-release sign" writes next to a file the
55
- * command writes, when it's inside the selected directory.
56
- *
57
- * @param rules Matcher from loadFileRules().
58
- * @param sourceDir Selected directory.
59
- * @param path Absolute path of the signed file.
60
- * @return The same matcher.
61
- */
62
- export declare function ignoreOwnSignature(rules: FileRules, sourceDir: string, path: string): FileRules;
63
42
  /**
64
43
  * Option for the source directory, which can also be the first positional
65
44
  * argument.
@@ -1,7 +1,6 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { isAbsolute, relative, resolve } from 'node:path';
2
+ import { resolve } from 'node:path';
3
3
  import { ValidationError } from "../errors.js";
4
- import { SIGNATURE_EXTENSION } from "../signing.js";
5
4
  import { getBoolean, getString, getStrings } from "./command.js";
6
5
  /**
7
6
  * Options shared by the commands that select files with .distignore and
@@ -63,39 +62,6 @@ export async function isDirectory(path) {
63
62
  const stats = await stat(path).catch(() => null);
64
63
  return Boolean(stats?.isDirectory());
65
64
  }
66
- /**
67
- * Ignore a file that the command writes or reads, such as the checksums
68
- * manifest, when it's inside the selected directory.
69
- *
70
- * @param rules Matcher from loadFileRules().
71
- * @param sourceDir Selected directory.
72
- * @param path Absolute file path.
73
- * @return The same matcher.
74
- */
75
- export function ignoreOwnFile(rules, sourceDir, path) {
76
- const relativePath = relative(sourceDir, path).split('\\').join('/');
77
- const isInside = relativePath &&
78
- '..' !== relativePath &&
79
- !relativePath.startsWith('../') &&
80
- !isAbsolute(relativePath);
81
- if (isInside) {
82
- // Escape the gitignore wildcards so that the pattern matches only this file.
83
- rules.add(`/${relativePath.replace(/[\\*?[\]!#]/g, '\\$&')}`);
84
- }
85
- return rules;
86
- }
87
- /**
88
- * Ignore the signature file that "wp-release sign" writes next to a file the
89
- * command writes, when it's inside the selected directory.
90
- *
91
- * @param rules Matcher from loadFileRules().
92
- * @param sourceDir Selected directory.
93
- * @param path Absolute path of the signed file.
94
- * @return The same matcher.
95
- */
96
- export function ignoreOwnSignature(rules, sourceDir, path) {
97
- return ignoreOwnFile(rules, sourceDir, path + SIGNATURE_EXTENSION);
98
- }
99
65
  /**
100
66
  * Option for the source directory, which can also be the first positional
101
67
  * argument.
@@ -22,15 +22,18 @@ declare const _default: {
22
22
  readonly empty: {
23
23
  readonly type: "boolean";
24
24
  };
25
+ readonly since: {
26
+ readonly type: "string";
27
+ };
25
28
  readonly name: {
26
29
  readonly type: "string";
27
30
  };
28
31
  };
29
- run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
32
+ run({ values, cwd, configFile, tagFormat, logger, terminal }: import("./command.ts").CommandContext): Promise<{
30
33
  exitCode: number;
31
34
  result: {
32
35
  file: string;
33
- packages: import("../changesets.ts").ChangesetPackages;
36
+ packages: any;
34
37
  };
35
38
  }>;
36
39
  };
@@ -1,34 +1,134 @@
1
- import { addChangeset } from "../changeset-release.js";
1
+ import { resolvePackageNames } from "../changeset-packages.js";
2
+ import { ChangesetPrompt } from "../changeset-prompt.js";
3
+ import { ChangesetRelease } from "../changeset-release.js";
4
+ import { ValidationError } from "../errors.js";
2
5
  import { parseChangesetAddArgs, changesetAddArgs } from "./changeset-args.js";
6
+ /**
7
+ * Get the bumps of the changed packages that have no changeset yet.
8
+ *
9
+ * @param release Changeset release of the repository.
10
+ * @param input Parsed options with the base ref.
11
+ * @param bump Bump of the changed packages.
12
+ * @return Bump of each changed package.
13
+ * @throws {ValidationError} If no changed package needs a changeset.
14
+ */
15
+ async function getChangedPackageBumps(release, input, bump) {
16
+ const status = await release.getStatus({
17
+ ...(input.since ? { since: input.since } : {}),
18
+ uncommitted: true,
19
+ });
20
+ if (!status.missingChangesets.length) {
21
+ const since = status.since ? ` since ${status.since}` : '';
22
+ throw new ValidationError(`No package changed${since} without a changeset. Pass --package <name> to choose the packages.`);
23
+ }
24
+ return Object.fromEntries(status.missingChangesets.map((name) => [name, bump]));
25
+ }
26
+ /**
27
+ * Ask for the changeset, preselecting the changed packages without one.
28
+ *
29
+ * @param release Changeset release of the repository.
30
+ * @param input Parsed options with the base ref and release note.
31
+ * @param terminal Terminal to ask in.
32
+ * @return Package bumps and release note.
33
+ */
34
+ async function askChangeset(release, input, terminal) {
35
+ const packages = await release.getPackages();
36
+ const status = await release.getStatus({
37
+ ...(input.since ? { since: input.since } : {}),
38
+ uncommitted: true,
39
+ });
40
+ const prompt = new ChangesetPrompt(terminal);
41
+ try {
42
+ return await prompt.ask({
43
+ packages,
44
+ changed: status.missingChangesets,
45
+ ...(status.since ? { since: status.since } : {}),
46
+ ...(input.summary ? { summary: input.summary } : {}),
47
+ });
48
+ }
49
+ finally {
50
+ prompt.close();
51
+ }
52
+ }
53
+ /**
54
+ * Get the package bumps and release note from the options, the changed
55
+ * packages or the prompts.
56
+ *
57
+ * @param release Changeset release of the repository.
58
+ * @param input Parsed options.
59
+ * @param terminal Terminal to ask in, when the CLI runs in one.
60
+ * @return Package bumps, empty for no release, and release note.
61
+ * @throws {ValidationError} Without packages outside a terminal.
62
+ */
63
+ async function getChangeset(release, input, terminal) {
64
+ const summary = input.summary ?? '';
65
+ let changeset;
66
+ if (input.empty) {
67
+ changeset = { packages: {}, summary };
68
+ }
69
+ else if (Object.keys(input.packages).length) {
70
+ changeset = { packages: input.packages, summary };
71
+ }
72
+ else if (input.bump) {
73
+ changeset = {
74
+ packages: await getChangedPackageBumps(release, input, input.bump),
75
+ summary,
76
+ };
77
+ }
78
+ else if (terminal) {
79
+ changeset = await askChangeset(release, input, terminal);
80
+ }
81
+ else {
82
+ throw new ValidationError('Pass matching repeated --package <name> and --bump <patch|minor|major> options, a single --bump for the changed packages, or --empty.');
83
+ }
84
+ return changeset;
85
+ }
3
86
  export default {
4
87
  name: 'changeset-add',
5
- usage: 'changeset-add (--empty | --package <name> --bump <bump> ...) [options]',
88
+ usage: 'changeset-add [--empty | --package <name> --bump <bump> ... | --bump <bump>] [options]',
6
89
  summary: 'Add a package release note for the next release PR.',
7
90
  help: `Options:
8
- --package <name> Package identifier (repeatable)
91
+ --package <name> Package identifier, or its path under the configured
92
+ root (repeatable)
9
93
  --bump <bump> patch, minor or major (repeatable, in package order)
10
94
  --summary <text> Customer-facing release note
11
95
  --empty Record that this change does not need a release
96
+ --since <ref> Compare changed files with this git ref (default:
97
+ <remote>/<baseBranch>)
12
98
  --name <name> Changeset filename (default: random identifier)
13
99
 
14
100
  Use --package and --bump pairs for package releases, or --empty for changes
15
- that should pass the status check without producing a release.`,
101
+ that should pass the status check without producing a release. A single
102
+ --bump without --package releases the packages that changed since the base
103
+ ref, including uncommitted files, and have no changeset yet. Without
104
+ --package and --bump in a terminal, the command asks for the packages, with
105
+ the changed ones preselected, their bumps and the release note.`,
16
106
  options: changesetAddArgs,
17
- async run({ values, cwd, configFile, logger }) {
107
+ async run({ values, cwd, configFile, tagFormat, logger, terminal }) {
18
108
  const input = parseChangesetAddArgs(values);
19
- const file = await addChangeset({
109
+ const release = new ChangesetRelease({
20
110
  cwd,
21
111
  ...(configFile ? { configFile } : {}),
22
- packages: input.packages,
23
- summary: input.summary,
112
+ ...(tagFormat ? { tagFormat } : {}),
113
+ });
114
+ const changeset = await getChangeset(release, input, terminal);
115
+ // Adding resolves the package paths and rejects a package passed twice.
116
+ const file = await release.add({
117
+ packages: changeset.packages,
118
+ summary: changeset.summary,
24
119
  ...(input.name ? { name: input.name } : {}),
25
120
  });
121
+ const names = resolvePackageNames(await release.getPackages(), Object.keys(changeset.packages));
122
+ const packages = Object.fromEntries(Object.values(changeset.packages).map((bump, index) => [
123
+ names[index],
124
+ bump,
125
+ ]));
26
126
  logger.info(`Created ${file}.`);
27
127
  return {
28
128
  exitCode: 0,
29
129
  result: {
30
130
  file,
31
- packages: input.packages,
131
+ packages,
32
132
  },
33
133
  };
34
134
  },
@@ -1,10 +1,14 @@
1
- import type { ChangesetPackages } from '../changesets.ts';
1
+ import type { ChangesetBump, ChangesetPackages } from '../changesets.ts';
2
2
  import type { ParsedValues } from './command.ts';
3
3
  /** Parsed input for `changeset-add`. */
4
4
  export interface ChangesetAddArgs {
5
+ /** Bump of each package passed with --package, by identifier or path. */
5
6
  packages: ChangesetPackages;
6
- summary: string;
7
+ /** Bump for the changed packages without a changeset, passed without --package. */
8
+ bump?: ChangesetBump;
9
+ summary?: string;
7
10
  empty: boolean;
11
+ since?: string;
8
12
  name?: string;
9
13
  }
10
14
  /** Parse-args options for `changeset-add`. */
@@ -23,16 +27,21 @@ export declare const changesetAddArgs: {
23
27
  readonly empty: {
24
28
  readonly type: "boolean";
25
29
  };
30
+ readonly since: {
31
+ readonly type: "string";
32
+ };
26
33
  readonly name: {
27
34
  readonly type: "string";
28
35
  };
29
36
  };
30
37
  /**
31
- * Parse and validate the `changeset-add` options.
38
+ * Parse and validate the `changeset-add` options. Without --package, a
39
+ * single --bump applies to the changed packages without a changeset, and
40
+ * without --bump either the command asks for the changeset in a terminal.
32
41
  *
33
42
  * @param values Parsed CLI options.
34
43
  * @return Normalized changeset input.
35
- * @throws {ValidationError} If required options are missing or conflicting.
44
+ * @throws {ValidationError} If options are conflicting or incomplete.
36
45
  */
37
46
  export declare function parseChangesetAddArgs(values: ParsedValues): ChangesetAddArgs;
38
47
  /** Parse-args options for `changeset-status`. */
@@ -68,6 +77,12 @@ export declare const changesetVersionArgs: {
68
77
  readonly 'no-dependents': {
69
78
  readonly type: "boolean";
70
79
  };
80
+ readonly prerelease: {
81
+ readonly type: "string";
82
+ };
83
+ readonly snapshot: {
84
+ readonly type: "string";
85
+ };
71
86
  };
72
87
  /** Parsed input for `changeset-version`. */
73
88
  export interface ChangesetVersionArgs {
@@ -76,6 +91,10 @@ export interface ChangesetVersionArgs {
76
91
  packages?: string[];
77
92
  /** Whether the selected sources release their configured dependents; defaults to true. */
78
93
  dependents: boolean;
94
+ /** Pre-release identifier with an optional number, like rc or rc.1. */
95
+ prerelease?: string;
96
+ /** Snapshot identifier, like pr-251. */
97
+ snapshot?: string;
79
98
  }
80
99
  /**
81
100
  * Validate and normalize a release date option.
@@ -90,7 +109,7 @@ export declare function parseChangesetDate(value: string | undefined): string |
90
109
  *
91
110
  * @param values Parsed CLI options.
92
111
  * @return Normalized versioning input.
93
- * @throws {ValidationError} If --date is invalid.
112
+ * @throws {ValidationError} If --date is invalid, or an option is empty or conflicting.
94
113
  */
95
114
  export declare function parseChangesetVersionArgs(values: ParsedValues): ChangesetVersionArgs;
96
115
  /** Parse-args options for `changeset-tag`. */
@@ -7,48 +7,76 @@ export const changesetAddArgs = {
7
7
  bump: { type: 'string', multiple: true },
8
8
  summary: { type: 'string' },
9
9
  empty: { type: 'boolean' },
10
+ since: { type: 'string' },
10
11
  name: { type: 'string' },
11
12
  };
12
13
  /**
13
- * Parse and validate the `changeset-add` options.
14
+ * Check that a bump is patch, minor or major.
15
+ *
16
+ * @param bump Bump option value.
17
+ * @param packageName Package of the bump, for the error.
18
+ * @return The bump.
19
+ * @throws {ValidationError} For another value.
20
+ */
21
+ function parseBump(bump, packageName) {
22
+ if (!CHANGESET_BUMPS.includes(bump)) {
23
+ const target = packageName ? ` for package "${packageName}"` : '';
24
+ throw new ValidationError(`Invalid bump "${bump}"${target}; expected patch, minor or major.`);
25
+ }
26
+ return bump;
27
+ }
28
+ /**
29
+ * Parse and validate the `changeset-add` options. Without --package, a
30
+ * single --bump applies to the changed packages without a changeset, and
31
+ * without --bump either the command asks for the changeset in a terminal.
14
32
  *
15
33
  * @param values Parsed CLI options.
16
34
  * @return Normalized changeset input.
17
- * @throws {ValidationError} If required options are missing or conflicting.
35
+ * @throws {ValidationError} If options are conflicting or incomplete.
18
36
  */
19
37
  export function parseChangesetAddArgs(values) {
20
38
  const names = getStrings(values, 'package');
21
39
  const bumps = getStrings(values, 'bump');
22
40
  const summary = getString(values, 'summary')?.trim();
23
41
  const empty = getBoolean(values, 'empty');
42
+ const since = getString(values, 'since');
24
43
  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.');
44
+ if (empty && (names.length || bumps.length || summary || since)) {
45
+ throw new ValidationError('--empty cannot be combined with --package, --bump, --summary or --since.');
27
46
  }
28
- if (!empty && (!names.length || names.length !== bumps.length)) {
47
+ if (names.length && names.length !== bumps.length) {
29
48
  throw new ValidationError('Pass matching repeated --package <name> and --bump <patch|minor|major> options, or use --empty.');
30
49
  }
31
- if (!empty && !summary) {
32
- throw new ValidationError('--summary <text> is required unless --empty is set.');
50
+ if (!names.length && bumps.length > 1) {
51
+ throw new ValidationError('Pass a single --bump without --package to release the changed packages, or pair each --bump with a --package.');
52
+ }
53
+ if (names.length && undefined !== since) {
54
+ throw new ValidationError('--since selects the changed packages, so it cannot be combined with --package.');
55
+ }
56
+ if ('' === since?.trim()) {
57
+ throw new ValidationError('--since <ref> must not be empty.');
58
+ }
59
+ if (bumps.length && !summary) {
60
+ throw new ValidationError('--summary <text> is required with --bump.');
33
61
  }
34
62
  if (name && !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
35
63
  throw new ValidationError('--name must contain only letters, numbers, dots, underscores or hyphens.');
36
64
  }
37
65
  const packages = {};
38
66
  names.forEach((packageName, index) => {
39
- const bump = bumps[index];
40
67
  if (Object.hasOwn(packages, packageName)) {
41
68
  throw new ValidationError(`Package "${packageName}" was passed more than once.`);
42
69
  }
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;
70
+ packages[packageName] = parseBump(bumps[index] ?? '', packageName);
47
71
  });
48
72
  return {
49
73
  packages,
50
- summary: summary ?? '',
51
74
  empty,
75
+ ...(!names.length && bumps.length
76
+ ? { bump: parseBump(bumps[0] ?? '') }
77
+ : {}),
78
+ ...(summary ? { summary } : {}),
79
+ ...(since ? { since } : {}),
52
80
  ...(name ? { name } : {}),
53
81
  };
54
82
  }
@@ -76,6 +104,8 @@ export const changesetVersionArgs = {
76
104
  date: { type: 'string' },
77
105
  package: { type: 'string', multiple: true },
78
106
  'no-dependents': { type: 'boolean' },
107
+ prerelease: { type: 'string' },
108
+ snapshot: { type: 'string' },
79
109
  };
80
110
  /**
81
111
  * Validate and normalize a release date option.
@@ -102,19 +132,32 @@ export function parseChangesetDate(value) {
102
132
  *
103
133
  * @param values Parsed CLI options.
104
134
  * @return Normalized versioning input.
105
- * @throws {ValidationError} If --date is invalid.
135
+ * @throws {ValidationError} If --date is invalid, or an option is empty or conflicting.
106
136
  */
107
137
  export function parseChangesetVersionArgs(values) {
108
138
  const date = parseChangesetDate(getString(values, 'date'));
109
139
  const packages = getStrings(values, 'package');
140
+ const prerelease = getString(values, 'prerelease')?.trim();
110
141
  if (packages.some((name) => !name.trim())) {
111
142
  throw new ValidationError('--package <name> must not be empty.');
112
143
  }
144
+ const snapshot = getString(values, 'snapshot')?.trim();
145
+ if ('' === prerelease) {
146
+ throw new ValidationError('--prerelease <id> must not be empty.');
147
+ }
148
+ if ('' === snapshot) {
149
+ throw new ValidationError('--snapshot <id> must not be empty.');
150
+ }
151
+ if (prerelease && snapshot) {
152
+ throw new ValidationError('--prerelease cannot be combined with --snapshot.');
153
+ }
113
154
  return {
114
155
  dryRun: getBoolean(values, 'dry-run'),
115
156
  dependents: !getBoolean(values, 'no-dependents'),
116
157
  ...(date ? { date } : {}),
117
158
  ...(packages.length ? { packages } : {}),
159
+ ...(prerelease ? { prerelease } : {}),
160
+ ...(snapshot ? { snapshot } : {}),
118
161
  };
119
162
  }
120
163
  /** Parse-args options for `changeset-tag`. */
@@ -1,4 +1,5 @@
1
1
  export interface ChangesetStatusResult {
2
+ since?: string;
2
3
  changesets: Array<{
3
4
  fileName: string;
4
5
  packages: Record<string, string>;
@@ -25,7 +26,7 @@ declare const _default: {
25
26
  readonly type: "string";
26
27
  };
27
28
  };
28
- run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
29
+ run({ values, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
29
30
  exitCode: number;
30
31
  result: {
31
32
  releases: {
@@ -34,6 +35,7 @@ declare const _default: {
34
35
  currentVersion: string;
35
36
  nextVersion: string;
36
37
  }[];
38
+ since?: string;
37
39
  changesets: import("../changesets.ts").Changeset[];
38
40
  changedPackages: string[];
39
41
  changedPackageFiles: Record<string, string[]>;
@@ -10,11 +10,12 @@ export default {
10
10
  Exit status is 2 when a changed package has no changeset added since the comparison base.
11
11
  An empty changeset added since the comparison base counts as an explicit no-release decision.`,
12
12
  options: changesetStatusArgs,
13
- async run({ values, cwd, configFile, logger }) {
13
+ async run({ values, cwd, configFile, tagFormat, logger }) {
14
14
  const { since } = parseChangesetStatusArgs(values);
15
15
  const result = await getChangesetStatus({
16
16
  cwd,
17
17
  ...(configFile ? { configFile } : {}),
18
+ ...(tagFormat ? { tagFormat } : {}),
18
19
  ...(since ? { since } : {}),
19
20
  });
20
21
  logger.summary(() => {
@@ -24,7 +24,7 @@ declare const _default: {
24
24
  readonly type: "string";
25
25
  };
26
26
  };
27
- run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
27
+ run({ values, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
28
28
  exitCode: number;
29
29
  result: import("../changeset-release.ts").ChangesetTagResult;
30
30
  }>;
@@ -10,10 +10,12 @@ export default {
10
10
  creating them
11
11
  --untagged List only releases without a tag, without creating tags
12
12
  --since <ref> Tag packages changed since this git ref
13
- --package <name> Tag only this package (repeatable)
13
+ --package <name> Tag only this package, by identifier or path under
14
+ the configured root (repeatable)
14
15
  --version <version> Fail unless the single --package is at this version;
15
16
  the version to tag for packages versioned by git tags
16
- --push Push newly created tags to origin, one at a time
17
+ --push Push newly created tags to the configured remote
18
+ (default: origin), one at a time
17
19
 
18
20
  Tags are created only when absent, so rerunning after a partial failure is
19
21
  safe. Before a tag is created, every configured version target must match the
@@ -22,11 +24,12 @@ and pre-release flag of its release, for a workflow step that creates the
22
24
  GitHub Release. --untagged lists only the releases that still need a tag, for
23
25
  example to release each of them with a separate workflow.`,
24
26
  options: changesetTagArgs,
25
- async run({ values, cwd, configFile, logger }) {
27
+ async run({ values, cwd, configFile, tagFormat, logger }) {
26
28
  const args = parseChangesetTagArgs(values);
27
29
  const result = await tagChangesets({
28
30
  cwd,
29
31
  ...(configFile ? { configFile } : {}),
32
+ ...(tagFormat ? { tagFormat } : {}),
30
33
  ...args,
31
34
  });
32
35
  result.tags.forEach((item) => {