@williamthorsen/release-kit 0.2.2 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +202 -219
  3. package/dist/esm/.cache +1 -1
  4. package/dist/esm/bin/release-kit.d.ts +2 -0
  5. package/dist/esm/bin/release-kit.js +73 -0
  6. package/dist/esm/component.d.ts +2 -0
  7. package/dist/esm/component.js +15 -0
  8. package/dist/esm/defaults.d.ts +3 -2
  9. package/dist/esm/defaults.js +17 -12
  10. package/dist/esm/determineBumpType.d.ts +2 -2
  11. package/dist/esm/determineBumpType.js +11 -13
  12. package/dist/esm/discoverWorkspaces.d.ts +1 -0
  13. package/dist/esm/discoverWorkspaces.js +45 -0
  14. package/dist/esm/getCommitsSinceTarget.js +2 -1
  15. package/dist/esm/index.d.ts +5 -2
  16. package/dist/esm/index.js +11 -2
  17. package/dist/esm/init/checks.d.ts +9 -0
  18. package/dist/esm/init/checks.js +56 -0
  19. package/dist/esm/init/detectRepoType.d.ts +2 -0
  20. package/dist/esm/init/detectRepoType.js +19 -0
  21. package/dist/esm/init/initCommand.d.ts +5 -0
  22. package/dist/esm/init/initCommand.js +65 -0
  23. package/dist/esm/init/parseJsonRecord.d.ts +1 -0
  24. package/dist/esm/init/parseJsonRecord.js +15 -0
  25. package/dist/esm/init/prompt.d.ts +5 -0
  26. package/dist/esm/init/prompt.js +30 -0
  27. package/dist/esm/init/scaffold.d.ts +9 -0
  28. package/dist/esm/init/scaffold.js +65 -0
  29. package/dist/esm/init/templates.d.ts +3 -0
  30. package/dist/esm/init/templates.js +105 -0
  31. package/dist/esm/loadConfig.d.ts +5 -0
  32. package/dist/esm/loadConfig.js +91 -0
  33. package/dist/esm/parseCommitMessage.d.ts +1 -1
  34. package/dist/esm/parseCommitMessage.js +4 -4
  35. package/dist/esm/prepareCommand.d.ts +1 -0
  36. package/dist/esm/prepareCommand.js +77 -0
  37. package/dist/esm/releasePrepare.d.ts +1 -1
  38. package/dist/esm/releasePrepare.js +7 -3
  39. package/dist/esm/releasePrepareMono.d.ts +1 -1
  40. package/dist/esm/releasePrepareMono.js +16 -10
  41. package/dist/esm/runReleasePrepare.d.ts +9 -0
  42. package/dist/esm/runReleasePrepare.js +112 -0
  43. package/dist/esm/types.d.ts +22 -4
  44. package/dist/esm/validateConfig.d.ts +5 -0
  45. package/dist/esm/validateConfig.js +143 -0
  46. package/dist/tsconfig.generate-typings.tsbuildinfo +1 -1
  47. package/package.json +12 -4
@@ -0,0 +1,91 @@
1
+ import { existsSync } from "node:fs";
2
+ import { component } from "./component.js";
3
+ import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
4
+ const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
5
+ async function loadConfig() {
6
+ if (!existsSync(CONFIG_FILE_PATH)) {
7
+ return void 0;
8
+ }
9
+ const { createJiti } = await import("jiti");
10
+ const jiti = createJiti(import.meta.url);
11
+ const imported = await jiti.import(CONFIG_FILE_PATH);
12
+ if (!isRecord(imported)) {
13
+ throw new Error(`Config file must export an object, got ${Array.isArray(imported) ? "array" : typeof imported}`);
14
+ }
15
+ const resolved = imported.default ?? imported.config;
16
+ if (resolved === void 0) {
17
+ throw new Error(
18
+ "Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)"
19
+ );
20
+ }
21
+ return resolved;
22
+ }
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function mergeMonorepoConfig(discoveredPaths, userConfig) {
27
+ let components = discoveredPaths.map((workspacePath) => component(workspacePath));
28
+ if (userConfig?.components !== void 0) {
29
+ const overrides = new Map(userConfig.components.map((c) => [c.dir, c]));
30
+ components = components.filter((c) => {
31
+ const override = overrides.get(c.dir);
32
+ return override?.shouldExclude !== true;
33
+ }).map((c) => {
34
+ const override = overrides.get(c.dir);
35
+ if (override?.tagPrefix !== void 0) {
36
+ return { ...c, tagPrefix: override.tagPrefix };
37
+ }
38
+ return c;
39
+ });
40
+ }
41
+ const workTypes = userConfig?.workTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userConfig.workTypes };
42
+ const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
43
+ const result = {
44
+ components,
45
+ workTypes,
46
+ versionPatterns
47
+ };
48
+ const formatCommand = userConfig?.formatCommand;
49
+ if (formatCommand !== void 0) {
50
+ result.formatCommand = formatCommand;
51
+ }
52
+ const cliffConfigPath = userConfig?.cliffConfigPath;
53
+ if (cliffConfigPath !== void 0) {
54
+ result.cliffConfigPath = cliffConfigPath;
55
+ }
56
+ const workspaceAliases = userConfig?.workspaceAliases;
57
+ if (workspaceAliases !== void 0) {
58
+ result.workspaceAliases = workspaceAliases;
59
+ }
60
+ return result;
61
+ }
62
+ function mergeSinglePackageConfig(userConfig) {
63
+ const workTypes = userConfig?.workTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userConfig.workTypes };
64
+ const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
65
+ const result = {
66
+ tagPrefix: "v",
67
+ packageFiles: ["package.json"],
68
+ changelogPaths: ["."],
69
+ workTypes,
70
+ versionPatterns
71
+ };
72
+ const formatCommand = userConfig?.formatCommand;
73
+ if (formatCommand !== void 0) {
74
+ result.formatCommand = formatCommand;
75
+ }
76
+ const cliffConfigPath = userConfig?.cliffConfigPath;
77
+ if (cliffConfigPath !== void 0) {
78
+ result.cliffConfigPath = cliffConfigPath;
79
+ }
80
+ const workspaceAliases = userConfig?.workspaceAliases;
81
+ if (workspaceAliases !== void 0) {
82
+ result.workspaceAliases = workspaceAliases;
83
+ }
84
+ return result;
85
+ }
86
+ export {
87
+ CONFIG_FILE_PATH,
88
+ loadConfig,
89
+ mergeMonorepoConfig,
90
+ mergeSinglePackageConfig
91
+ };
@@ -1,2 +1,2 @@
1
1
  import type { ParsedCommit, WorkTypeConfig } from './types.ts';
2
- export declare function parseCommitMessage(message: string, hash: string, workTypes: readonly WorkTypeConfig[], workspaceAliases?: Record<string, string>): ParsedCommit | undefined;
2
+ export declare function parseCommitMessage(message: string, hash: string, workTypes: Record<string, WorkTypeConfig>, workspaceAliases?: Record<string, string>): ParsedCommit | undefined;
@@ -27,14 +27,14 @@ function parseCommitMessage(message, hash, workTypes, workspaceAliases) {
27
27
  }
28
28
  function resolveType(rawType, workTypes) {
29
29
  const lowered = rawType.toLowerCase();
30
- for (const config of workTypes) {
31
- if (config.type === lowered) {
32
- return config.type;
30
+ for (const [key, config] of Object.entries(workTypes)) {
31
+ if (key === lowered) {
32
+ return key;
33
33
  }
34
34
  if (config.aliases !== void 0) {
35
35
  for (const alias of config.aliases) {
36
36
  if (alias === lowered) {
37
- return config.type;
37
+ return key;
38
38
  }
39
39
  }
40
40
  }
@@ -0,0 +1 @@
1
+ export declare function prepareCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,77 @@
1
+ import { discoverWorkspaces } from "./discoverWorkspaces.js";
2
+ import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig } from "./loadConfig.js";
3
+ import { releasePrepare } from "./releasePrepare.js";
4
+ import { releasePrepareMono } from "./releasePrepareMono.js";
5
+ import { parseArgs, RELEASE_TAGS_FILE, writeReleaseTags } from "./runReleasePrepare.js";
6
+ import { validateConfig } from "./validateConfig.js";
7
+ async function prepareCommand(argv) {
8
+ const { dryRun, bumpOverride, only } = parseArgs(argv);
9
+ const options = { dryRun, ...bumpOverride === void 0 ? {} : { bumpOverride } };
10
+ let rawConfig;
11
+ try {
12
+ rawConfig = await loadConfig();
13
+ } catch (error) {
14
+ console.error(`Error loading config: ${error instanceof Error ? error.message : String(error)}`);
15
+ process.exit(1);
16
+ }
17
+ let userConfig;
18
+ if (rawConfig !== void 0) {
19
+ const { config, errors } = validateConfig(rawConfig);
20
+ if (errors.length > 0) {
21
+ console.error("Invalid config:");
22
+ for (const err of errors) {
23
+ console.error(` - ${err}`);
24
+ }
25
+ process.exit(1);
26
+ }
27
+ userConfig = config;
28
+ }
29
+ let discoveredPaths;
30
+ try {
31
+ discoveredPaths = await discoverWorkspaces();
32
+ } catch (error) {
33
+ console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
34
+ process.exit(1);
35
+ }
36
+ let tags = [];
37
+ if (discoveredPaths === void 0) {
38
+ if (only !== void 0) {
39
+ console.error("Error: --only is only supported for monorepo configurations");
40
+ process.exit(1);
41
+ }
42
+ const config = mergeSinglePackageConfig(userConfig);
43
+ try {
44
+ tags = releasePrepare(config, options);
45
+ writeReleaseTags(tags, dryRun);
46
+ } catch (error) {
47
+ console.error("Error preparing release:", error instanceof Error ? error.message : String(error));
48
+ process.exit(1);
49
+ }
50
+ } else {
51
+ const config = mergeMonorepoConfig(discoveredPaths, userConfig);
52
+ if (only !== void 0) {
53
+ const knownNames = config.components.map((c) => c.dir);
54
+ for (const name of only) {
55
+ if (!knownNames.includes(name)) {
56
+ console.error(`Error: Unknown component "${name}". Known components: ${knownNames.join(", ")}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+ config.components = config.components.filter((c) => only.includes(c.dir));
61
+ }
62
+ try {
63
+ tags = releasePrepareMono(config, options);
64
+ writeReleaseTags(tags, dryRun);
65
+ } catch (error) {
66
+ console.error("Error preparing release:", error instanceof Error ? error.message : String(error));
67
+ process.exit(1);
68
+ }
69
+ }
70
+ if (tags.length > 0) {
71
+ console.info(`
72
+ Release tags file: ${RELEASE_TAGS_FILE}`);
73
+ }
74
+ }
75
+ export {
76
+ prepareCommand
77
+ };
@@ -3,4 +3,4 @@ export interface ReleasePrepareOptions {
3
3
  dryRun: boolean;
4
4
  bumpOverride?: ReleaseType;
5
5
  }
6
- export declare function releasePrepare(config: ReleaseConfig, options: ReleasePrepareOptions): void;
6
+ export declare function releasePrepare(config: ReleaseConfig, options: ReleasePrepareOptions): string[];
@@ -1,26 +1,29 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { bumpAllVersions } from "./bumpAllVersions.js";
3
+ import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
3
4
  import { determineBumpType } from "./determineBumpType.js";
4
5
  import { generateChangelogs } from "./generateChangelogs.js";
5
6
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
6
7
  import { parseCommitMessage } from "./parseCommitMessage.js";
7
8
  function releasePrepare(config, options) {
8
9
  const { dryRun, bumpOverride } = options;
10
+ const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
11
+ const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
9
12
  console.info("Finding commits since last release...");
10
13
  const { tag, commits } = getCommitsSinceTarget(config.tagPrefix);
11
14
  console.info(` Found ${commits.length} commits since ${tag ?? "the beginning"}`);
12
15
  let releaseType;
13
16
  if (bumpOverride === void 0) {
14
- const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, config.workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
17
+ const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
15
18
  console.info(` Parsed ${parsedCommits.length} typed commits`);
16
- releaseType = determineBumpType(parsedCommits, config.workTypes);
19
+ releaseType = determineBumpType(parsedCommits, workTypes, versionPatterns);
17
20
  } else {
18
21
  releaseType = bumpOverride;
19
22
  console.info(` Using bump override: ${releaseType}`);
20
23
  }
21
24
  if (releaseType === void 0) {
22
25
  console.info("No release-worthy changes found. Skipping.");
23
- return;
26
+ return [];
24
27
  }
25
28
  console.info(`Bumping versions (${releaseType})...`);
26
29
  const newVersion = bumpAllVersions(config.packageFiles, releaseType, dryRun);
@@ -42,6 +45,7 @@ function releasePrepare(config, options) {
42
45
  }
43
46
  }
44
47
  console.info(`Release preparation complete: ${newTag}`);
48
+ return [newTag];
45
49
  }
46
50
  export {
47
51
  releasePrepare
@@ -1,3 +1,3 @@
1
1
  import type { ReleasePrepareOptions } from './releasePrepare.ts';
2
2
  import type { MonorepoReleaseConfig } from './types.ts';
3
- export declare function releasePrepareMono(config: MonorepoReleaseConfig, options: ReleasePrepareOptions): void;
3
+ export declare function releasePrepareMono(config: MonorepoReleaseConfig, options: ReleasePrepareOptions): string[];
@@ -1,46 +1,51 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { bumpAllVersions } from "./bumpAllVersions.js";
3
+ import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
3
4
  import { determineBumpType } from "./determineBumpType.js";
4
5
  import { generateChangelog } from "./generateChangelogs.js";
5
6
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
6
7
  import { parseCommitMessage } from "./parseCommitMessage.js";
7
8
  function releasePrepareMono(config, options) {
8
9
  const { dryRun, bumpOverride } = options;
9
- let anyComponentProcessed = false;
10
+ const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
11
+ const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
12
+ const tags = [];
10
13
  for (const component of config.components) {
14
+ const name = component.dir;
11
15
  console.info(`
12
- Processing component: ${component.tagPrefix}`);
16
+ Processing component: ${name}`);
13
17
  console.info(" Finding commits since last release...");
14
18
  const { tag, commits } = getCommitsSinceTarget(component.tagPrefix, component.paths);
15
- console.info(` Found ${commits.length} commits since ${tag ?? "the beginning"}`);
19
+ const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
20
+ console.info(` Found ${commits.length} commits ${since}`);
16
21
  if (commits.length === 0) {
17
- console.info(` No changes for ${component.tagPrefix}. Skipping.`);
22
+ console.info(` No changes for ${name} ${since}. Skipping.`);
18
23
  continue;
19
24
  }
20
25
  let releaseType;
21
26
  if (bumpOverride === void 0) {
22
- const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, config.workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
27
+ const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
23
28
  console.info(` Parsed ${parsedCommits.length} typed commits`);
24
- releaseType = determineBumpType(parsedCommits, config.workTypes);
29
+ releaseType = determineBumpType(parsedCommits, workTypes, versionPatterns);
25
30
  } else {
26
31
  releaseType = bumpOverride;
27
32
  console.info(` Using bump override: ${releaseType}`);
28
33
  }
29
34
  if (releaseType === void 0) {
30
- console.info(` No release-worthy changes for ${component.tagPrefix}. Skipping.`);
35
+ console.info(` No release-worthy changes for ${name} ${since}. Skipping.`);
31
36
  continue;
32
37
  }
33
38
  console.info(` Bumping versions (${releaseType})...`);
34
39
  const newVersion = bumpAllVersions(component.packageFiles, releaseType, dryRun);
35
40
  const newTag = `${component.tagPrefix}${newVersion}`;
36
- anyComponentProcessed = true;
41
+ tags.push(newTag);
37
42
  console.info(" Generating changelogs...");
38
43
  for (const changelogPath of component.changelogPaths) {
39
44
  generateChangelog(config, changelogPath, newTag, dryRun, { includePaths: component.paths });
40
45
  }
41
46
  console.info(` Component release prepared: ${newTag}`);
42
47
  }
43
- if (anyComponentProcessed && config.formatCommand !== void 0) {
48
+ if (tags.length > 0 && config.formatCommand !== void 0) {
44
49
  if (dryRun) {
45
50
  console.info(`
46
51
  [dry-run] Would run format command: ${config.formatCommand}`);
@@ -56,9 +61,10 @@ Processing component: ${component.tagPrefix}`);
56
61
  }
57
62
  }
58
63
  }
59
- const summary = anyComponentProcessed ? "Monorepo release preparation complete." : "No components had release-worthy changes.";
64
+ const summary = tags.length > 0 ? "Monorepo release preparation complete." : "No components had release-worthy changes.";
60
65
  console.info(`
61
66
  ${summary}`);
67
+ return tags;
62
68
  }
63
69
  export {
64
70
  releasePrepareMono
@@ -0,0 +1,9 @@
1
+ import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseType } from './types.ts';
2
+ export declare const RELEASE_TAGS_FILE = "/tmp/release-kit/.release-tags";
3
+ export declare function parseArgs(argv: string[]): {
4
+ dryRun: boolean;
5
+ bumpOverride: ReleaseType | undefined;
6
+ only: string[] | undefined;
7
+ };
8
+ export declare function runReleasePrepare(config: MonorepoReleaseConfig | ReleaseConfig): void;
9
+ export declare function writeReleaseTags(tags: string[], dryRun: boolean): void;
@@ -0,0 +1,112 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { releasePrepare } from "./releasePrepare.js";
4
+ import { releasePrepareMono } from "./releasePrepareMono.js";
5
+ const RELEASE_TAGS_FILE = "/tmp/release-kit/.release-tags";
6
+ const VALID_BUMP_TYPES = ["major", "minor", "patch"];
7
+ function isReleaseType(value) {
8
+ return VALID_BUMP_TYPES.includes(value);
9
+ }
10
+ function isMonorepoConfig(config) {
11
+ return "components" in config;
12
+ }
13
+ function showHelp() {
14
+ console.info(`
15
+ Usage: runReleasePrepare [options]
16
+
17
+ Legacy entry point for release preparation. Prefer the CLI:
18
+ npx @williamthorsen/release-kit prepare
19
+
20
+ Options:
21
+ --dry-run Run without modifying any files
22
+ --bump=major|minor|patch Override the bump type for all components
23
+ --only=name1,name2 Only process the named components (comma-separated, monorepo only)
24
+ --help Show this help message
25
+ `);
26
+ }
27
+ function parseArgs(argv) {
28
+ let dryRun = false;
29
+ let bumpOverride;
30
+ let only;
31
+ for (const arg of argv) {
32
+ if (arg === "--dry-run") {
33
+ dryRun = true;
34
+ } else if (arg.startsWith("--bump=")) {
35
+ const value = arg.slice("--bump=".length);
36
+ if (!isReleaseType(value)) {
37
+ console.error(`Error: Invalid bump type "${value}". Must be one of: ${VALID_BUMP_TYPES.join(", ")}`);
38
+ process.exit(1);
39
+ }
40
+ bumpOverride = value;
41
+ } else if (arg.startsWith("--only=")) {
42
+ const value = arg.slice("--only=".length);
43
+ if (!value) {
44
+ console.error("Error: --only requires a comma-separated list of component names");
45
+ process.exit(1);
46
+ }
47
+ only = value.split(",");
48
+ } else if (arg === "--help" || arg === "-h") {
49
+ showHelp();
50
+ process.exit(0);
51
+ } else {
52
+ console.error(`Error: Unknown argument: ${arg}`);
53
+ process.exit(1);
54
+ }
55
+ }
56
+ return { dryRun, bumpOverride, only };
57
+ }
58
+ function runReleasePrepare(config) {
59
+ const { dryRun, bumpOverride, only } = parseArgs(process.argv.slice(2));
60
+ const options = { dryRun, ...bumpOverride === void 0 ? {} : { bumpOverride } };
61
+ if (!isMonorepoConfig(config)) {
62
+ if (only !== void 0) {
63
+ console.error("Error: --only is only supported for monorepo configurations");
64
+ process.exit(1);
65
+ }
66
+ try {
67
+ const tags = releasePrepare(config, options);
68
+ writeReleaseTags(tags, dryRun);
69
+ } catch (error) {
70
+ console.error("Error preparing release:", error instanceof Error ? error.message : String(error));
71
+ process.exit(1);
72
+ }
73
+ return;
74
+ }
75
+ let effectiveConfig = config;
76
+ if (only !== void 0) {
77
+ const knownNames = config.components.map((c) => c.dir);
78
+ for (const name of only) {
79
+ if (!knownNames.includes(name)) {
80
+ console.error(`Error: Unknown component "${name}". Known components: ${knownNames.join(", ")}`);
81
+ process.exit(1);
82
+ }
83
+ }
84
+ const filtered = config.components.filter((c) => only.includes(c.dir));
85
+ effectiveConfig = { ...config, components: filtered };
86
+ }
87
+ try {
88
+ const tags = releasePrepareMono(effectiveConfig, options);
89
+ writeReleaseTags(tags, dryRun);
90
+ } catch (error) {
91
+ console.error("Error preparing release:", error instanceof Error ? error.message : String(error));
92
+ process.exit(1);
93
+ }
94
+ }
95
+ function writeReleaseTags(tags, dryRun) {
96
+ if (tags.length === 0) {
97
+ return;
98
+ }
99
+ if (dryRun) {
100
+ console.info(` [dry-run] Would write ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`);
101
+ return;
102
+ }
103
+ mkdirSync(dirname(RELEASE_TAGS_FILE), { recursive: true });
104
+ writeFileSync(RELEASE_TAGS_FILE, tags.join("\n"), "utf8");
105
+ console.info(` Wrote ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`);
106
+ }
107
+ export {
108
+ RELEASE_TAGS_FILE,
109
+ parseArgs,
110
+ runReleasePrepare,
111
+ writeReleaseTags
112
+ };
@@ -1,10 +1,25 @@
1
1
  export type ReleaseType = 'major' | 'minor' | 'patch';
2
2
  export interface WorkTypeConfig {
3
- type: string;
4
3
  header: string;
5
- bump: ReleaseType;
6
4
  aliases?: string[];
7
5
  }
6
+ export interface VersionPatterns {
7
+ major: string[];
8
+ minor: string[];
9
+ }
10
+ export interface ReleaseKitConfig {
11
+ components?: ComponentOverride[];
12
+ versionPatterns?: VersionPatterns;
13
+ workTypes?: Record<string, WorkTypeConfig>;
14
+ formatCommand?: string;
15
+ cliffConfigPath?: string;
16
+ workspaceAliases?: Record<string, string>;
17
+ }
18
+ export interface ComponentOverride {
19
+ dir: string;
20
+ tagPrefix?: string;
21
+ shouldExclude?: boolean;
22
+ }
8
23
  export interface Commit {
9
24
  message: string;
10
25
  hash: string;
@@ -18,6 +33,7 @@ export interface ParsedCommit {
18
33
  breaking: boolean;
19
34
  }
20
35
  export interface ComponentConfig {
36
+ dir: string;
21
37
  tagPrefix: string;
22
38
  packageFiles: string[];
23
39
  changelogPaths: string[];
@@ -25,7 +41,8 @@ export interface ComponentConfig {
25
41
  }
26
42
  export interface MonorepoReleaseConfig {
27
43
  components: ComponentConfig[];
28
- workTypes: WorkTypeConfig[];
44
+ workTypes?: Record<string, WorkTypeConfig>;
45
+ versionPatterns?: VersionPatterns;
29
46
  formatCommand?: string;
30
47
  cliffConfigPath?: string;
31
48
  workspaceAliases?: Record<string, string>;
@@ -34,7 +51,8 @@ export interface ReleaseConfig {
34
51
  tagPrefix: string;
35
52
  packageFiles: string[];
36
53
  changelogPaths: string[];
37
- workTypes: WorkTypeConfig[];
54
+ workTypes?: Record<string, WorkTypeConfig>;
55
+ versionPatterns?: VersionPatterns;
38
56
  formatCommand?: string;
39
57
  cliffConfigPath?: string;
40
58
  workspaceAliases?: Record<string, string>;
@@ -0,0 +1,5 @@
1
+ import type { ReleaseKitConfig } from './types.ts';
2
+ export declare function validateConfig(raw: unknown): {
3
+ config: ReleaseKitConfig;
4
+ errors: string[];
5
+ };
@@ -0,0 +1,143 @@
1
+ function validateConfig(raw) {
2
+ const errors = [];
3
+ if (!isRecord(raw)) {
4
+ return { config: {}, errors: ["Config must be an object"] };
5
+ }
6
+ const config = {};
7
+ const knownFields = /* @__PURE__ */ new Set([
8
+ "components",
9
+ "versionPatterns",
10
+ "workTypes",
11
+ "formatCommand",
12
+ "cliffConfigPath",
13
+ "workspaceAliases"
14
+ ]);
15
+ for (const key of Object.keys(raw)) {
16
+ if (!knownFields.has(key)) {
17
+ errors.push(`Unknown field: '${key}'`);
18
+ }
19
+ }
20
+ validateComponents(raw.components, config, errors);
21
+ validateVersionPatterns(raw.versionPatterns, config, errors);
22
+ validateWorkTypes(raw.workTypes, config, errors);
23
+ validateStringField("formatCommand", raw.formatCommand, config, errors);
24
+ validateStringField("cliffConfigPath", raw.cliffConfigPath, config, errors);
25
+ validateWorkspaceAliases(raw.workspaceAliases, config, errors);
26
+ return { config, errors };
27
+ }
28
+ function isRecord(value) {
29
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30
+ }
31
+ function isStringArray(value) {
32
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
33
+ }
34
+ function validateComponents(value, config, errors) {
35
+ if (value === void 0) return;
36
+ if (!Array.isArray(value)) {
37
+ errors.push("'components' must be an array");
38
+ return;
39
+ }
40
+ const components = [];
41
+ for (const [i, entry] of value.entries()) {
42
+ if (!isRecord(entry)) {
43
+ errors.push(`components[${i}]: must be an object`);
44
+ continue;
45
+ }
46
+ if (typeof entry.dir !== "string" || entry.dir === "") {
47
+ errors.push(`components[${i}]: 'dir' is required`);
48
+ continue;
49
+ }
50
+ const component = { dir: entry.dir };
51
+ if (entry.tagPrefix !== void 0) {
52
+ if (typeof entry.tagPrefix === "string") {
53
+ component.tagPrefix = entry.tagPrefix;
54
+ } else {
55
+ errors.push(`components[${i}]: 'tagPrefix' must be a string`);
56
+ }
57
+ }
58
+ if (entry.shouldExclude !== void 0) {
59
+ if (typeof entry.shouldExclude === "boolean") {
60
+ component.shouldExclude = entry.shouldExclude;
61
+ } else {
62
+ errors.push(`components[${i}]: 'shouldExclude' must be a boolean`);
63
+ }
64
+ }
65
+ components.push(component);
66
+ }
67
+ config.components = components;
68
+ }
69
+ function validateVersionPatterns(value, config, errors) {
70
+ if (value === void 0) return;
71
+ if (!isRecord(value)) {
72
+ errors.push("'versionPatterns' must be an object");
73
+ return;
74
+ }
75
+ if (!isStringArray(value.major)) {
76
+ errors.push("versionPatterns.major: expected string array");
77
+ }
78
+ if (!isStringArray(value.minor)) {
79
+ errors.push("versionPatterns.minor: expected string array");
80
+ }
81
+ if (isStringArray(value.major) && isStringArray(value.minor)) {
82
+ config.versionPatterns = { major: value.major, minor: value.minor };
83
+ }
84
+ }
85
+ function validateWorkTypes(value, config, errors) {
86
+ if (value === void 0) return;
87
+ if (!isRecord(value) || Array.isArray(value)) {
88
+ errors.push("'workTypes' must be a record (object)");
89
+ return;
90
+ }
91
+ const workTypes = {};
92
+ for (const [key, entry] of Object.entries(value)) {
93
+ if (!isRecord(entry)) {
94
+ errors.push(`workTypes.${key}: must be an object`);
95
+ continue;
96
+ }
97
+ if (typeof entry.header !== "string") {
98
+ errors.push(`workTypes.${key}: 'header' is required and must be a string`);
99
+ continue;
100
+ }
101
+ const wtEntry = { header: entry.header };
102
+ if (entry.aliases !== void 0) {
103
+ if (isStringArray(entry.aliases)) {
104
+ wtEntry.aliases = entry.aliases;
105
+ } else {
106
+ errors.push(`workTypes.${key}: 'aliases' must be a string array`);
107
+ }
108
+ }
109
+ workTypes[key] = wtEntry;
110
+ }
111
+ config.workTypes = workTypes;
112
+ }
113
+ function validateStringField(fieldName, value, config, errors) {
114
+ if (value === void 0) return;
115
+ if (typeof value !== "string") {
116
+ errors.push(`'${fieldName}' must be a string`);
117
+ return;
118
+ }
119
+ config[fieldName] = value;
120
+ }
121
+ function validateWorkspaceAliases(value, config, errors) {
122
+ if (value === void 0) return;
123
+ if (!isRecord(value)) {
124
+ errors.push("'workspaceAliases' must be a record (object)");
125
+ return;
126
+ }
127
+ const aliases = {};
128
+ let valid = true;
129
+ for (const [key, v] of Object.entries(value)) {
130
+ if (typeof v === "string") {
131
+ aliases[key] = v;
132
+ } else {
133
+ errors.push(`workspaceAliases.${key}: value must be a string`);
134
+ valid = false;
135
+ }
136
+ }
137
+ if (valid) {
138
+ config.workspaceAliases = aliases;
139
+ }
140
+ }
141
+ export {
142
+ validateConfig
143
+ };