browser-commander 0.2.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 (82) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +296 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +32 -0
  9. package/LICENSE +24 -0
  10. package/README.md +320 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/eslint.config.js +125 -0
  14. package/examples/react-test-app/index.html +25 -0
  15. package/examples/react-test-app/package.json +19 -0
  16. package/examples/react-test-app/src/App.jsx +473 -0
  17. package/examples/react-test-app/src/main.jsx +10 -0
  18. package/examples/react-test-app/src/styles.css +323 -0
  19. package/examples/react-test-app/vite.config.js +9 -0
  20. package/package.json +89 -0
  21. package/scripts/changeset-version.mjs +38 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +86 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +216 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/merge-changesets.mjs +260 -0
  28. package/scripts/publish-to-npm.mjs +126 -0
  29. package/scripts/setup-npm.mjs +37 -0
  30. package/scripts/validate-changeset.mjs +262 -0
  31. package/scripts/version-and-commit.mjs +237 -0
  32. package/src/ARCHITECTURE.md +270 -0
  33. package/src/README.md +517 -0
  34. package/src/bindings.js +298 -0
  35. package/src/browser/launcher.js +93 -0
  36. package/src/browser/navigation.js +513 -0
  37. package/src/core/constants.js +24 -0
  38. package/src/core/engine-adapter.js +466 -0
  39. package/src/core/engine-detection.js +49 -0
  40. package/src/core/logger.js +21 -0
  41. package/src/core/navigation-manager.js +503 -0
  42. package/src/core/navigation-safety.js +160 -0
  43. package/src/core/network-tracker.js +373 -0
  44. package/src/core/page-session.js +299 -0
  45. package/src/core/page-trigger-manager.js +564 -0
  46. package/src/core/preferences.js +46 -0
  47. package/src/elements/content.js +197 -0
  48. package/src/elements/locators.js +243 -0
  49. package/src/elements/selectors.js +360 -0
  50. package/src/elements/visibility.js +166 -0
  51. package/src/exports.js +121 -0
  52. package/src/factory.js +192 -0
  53. package/src/high-level/universal-logic.js +206 -0
  54. package/src/index.js +17 -0
  55. package/src/interactions/click.js +684 -0
  56. package/src/interactions/fill.js +383 -0
  57. package/src/interactions/scroll.js +341 -0
  58. package/src/utilities/url.js +33 -0
  59. package/src/utilities/wait.js +135 -0
  60. package/tests/e2e/playwright.e2e.test.js +442 -0
  61. package/tests/e2e/puppeteer.e2e.test.js +408 -0
  62. package/tests/helpers/mocks.js +542 -0
  63. package/tests/unit/bindings.test.js +218 -0
  64. package/tests/unit/browser/navigation.test.js +345 -0
  65. package/tests/unit/core/constants.test.js +72 -0
  66. package/tests/unit/core/engine-adapter.test.js +170 -0
  67. package/tests/unit/core/engine-detection.test.js +81 -0
  68. package/tests/unit/core/logger.test.js +80 -0
  69. package/tests/unit/core/navigation-safety.test.js +202 -0
  70. package/tests/unit/core/network-tracker.test.js +198 -0
  71. package/tests/unit/core/page-trigger-manager.test.js +358 -0
  72. package/tests/unit/elements/content.test.js +318 -0
  73. package/tests/unit/elements/locators.test.js +236 -0
  74. package/tests/unit/elements/selectors.test.js +302 -0
  75. package/tests/unit/elements/visibility.test.js +234 -0
  76. package/tests/unit/factory.test.js +174 -0
  77. package/tests/unit/high-level/universal-logic.test.js +299 -0
  78. package/tests/unit/interactions/click.test.js +340 -0
  79. package/tests/unit/interactions/fill.test.js +378 -0
  80. package/tests/unit/interactions/scroll.test.js +330 -0
  81. package/tests/unit/utilities/url.test.js +63 -0
  82. package/tests/unit/utilities/wait.test.js +207 -0
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Format GitHub release notes using the format-release-notes.mjs script
5
+ * Usage: node scripts/format-github-release.mjs --release-version <version> --repository <repository> --commit-sha <commit_sha>
6
+ * release-version: Version number (e.g., 1.0.0)
7
+ * repository: GitHub repository (e.g., owner/repo)
8
+ * commit_sha: Commit SHA for PR detection
9
+ *
10
+ * Uses link-foundation libraries:
11
+ * - use-m: Dynamic package loading without package.json dependencies
12
+ * - command-stream: Modern shell command execution with streaming support
13
+ * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
14
+ */
15
+
16
+ // Load use-m dynamically
17
+ const { use } = eval(
18
+ await (await fetch('https://unpkg.com/use-m/use.js')).text()
19
+ );
20
+
21
+ // Import link-foundation libraries
22
+ const { $ } = await use('command-stream');
23
+ const { makeConfig } = await use('lino-arguments');
24
+
25
+ // Parse CLI arguments using lino-arguments
26
+ // Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag
27
+ const config = makeConfig({
28
+ yargs: ({ yargs, getenv }) =>
29
+ yargs
30
+ .option('release-version', {
31
+ type: 'string',
32
+ default: getenv('VERSION', ''),
33
+ describe: 'Version number (e.g., 1.0.0)',
34
+ })
35
+ .option('repository', {
36
+ type: 'string',
37
+ default: getenv('REPOSITORY', ''),
38
+ describe: 'GitHub repository (e.g., owner/repo)',
39
+ })
40
+ .option('commit-sha', {
41
+ type: 'string',
42
+ default: getenv('COMMIT_SHA', ''),
43
+ describe: 'Commit SHA for PR detection',
44
+ }),
45
+ });
46
+
47
+ const { releaseVersion: version, repository, commitSha } = config;
48
+
49
+ if (!version || !repository || !commitSha) {
50
+ console.error('Error: Missing required arguments');
51
+ console.error(
52
+ 'Usage: node scripts/format-github-release.mjs --release-version <version> --repository <repository> --commit-sha <commit_sha>'
53
+ );
54
+ process.exit(1);
55
+ }
56
+
57
+ const tag = `v${version}`;
58
+
59
+ try {
60
+ // Get the release ID for this version
61
+ let releaseId = '';
62
+ try {
63
+ const result =
64
+ await $`gh api "repos/${repository}/releases/tags/${tag}" --jq '.id'`.run(
65
+ { capture: true }
66
+ );
67
+ releaseId = result.stdout.trim();
68
+ } catch {
69
+ console.log(`\u26A0\uFE0F Could not find release for ${tag}`);
70
+ process.exit(0);
71
+ }
72
+
73
+ if (releaseId) {
74
+ console.log(`Formatting release notes for ${tag}...`);
75
+ // Pass the trigger commit SHA for PR detection
76
+ // This allows proper PR lookup even if the changelog doesn't have a commit hash
77
+ await $`node scripts/format-release-notes.mjs --release-id "${releaseId}" --release-version "${tag}" --repository "${repository}" --commit-sha "${commitSha}"`;
78
+ console.log(`\u2705 Formatted release notes for ${tag}`);
79
+ }
80
+ } catch (error) {
81
+ console.error('Error formatting release:', error.message);
82
+ process.exit(1);
83
+ }
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Script to format GitHub release notes with proper formatting:
5
+ * - Fix special characters like \n
6
+ * - Add link to PR that contains the release commit (if found)
7
+ * - Add shields.io NPM version badge
8
+ * - Format nicely with proper markdown
9
+ *
10
+ * PR Detection Logic:
11
+ * 1. Extract commit hash from changelog entry (if present)
12
+ * 2. Fall back to --commit-sha argument (passed from workflow)
13
+ * 3. Look up PRs that contain the commit via GitHub API
14
+ * 4. If no PR found, simply don't display any PR link (no guessing)
15
+ *
16
+ * Uses link-foundation libraries:
17
+ * - use-m: Dynamic package loading without package.json dependencies
18
+ * - command-stream: Modern shell command execution with streaming support
19
+ * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
20
+ *
21
+ * Note: Uses --release-version instead of --version to avoid conflict with yargs' built-in --version flag.
22
+ */
23
+
24
+ const PACKAGE_NAME = 'browser-commander';
25
+
26
+ // Load use-m dynamically
27
+ const { use } = eval(
28
+ await (await fetch('https://unpkg.com/use-m/use.js')).text()
29
+ );
30
+
31
+ // Import link-foundation libraries
32
+ const { $ } = await use('command-stream');
33
+ const { makeConfig } = await use('lino-arguments');
34
+
35
+ // Parse CLI arguments using lino-arguments
36
+ // Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag
37
+ const config = makeConfig({
38
+ yargs: ({ yargs, getenv }) =>
39
+ yargs
40
+ .option('release-version', {
41
+ type: 'string',
42
+ default: getenv('VERSION', ''),
43
+ describe: 'Version number (e.g., v0.8.36)',
44
+ })
45
+ .option('release-id', {
46
+ type: 'string',
47
+ default: getenv('RELEASE_ID', ''),
48
+ describe: 'GitHub release ID',
49
+ })
50
+ .option('repository', {
51
+ type: 'string',
52
+ default: getenv('REPOSITORY', ''),
53
+ describe: 'GitHub repository (e.g., owner/repo)',
54
+ })
55
+ .option('commit-sha', {
56
+ type: 'string',
57
+ default: getenv('COMMIT_SHA', ''),
58
+ describe: 'Commit SHA for PR detection',
59
+ }),
60
+ });
61
+
62
+ const releaseId = config.releaseId;
63
+ const version = config.releaseVersion;
64
+ const repository = config.repository;
65
+ const passedCommitSha = config.commitSha;
66
+
67
+ if (!releaseId || !version || !repository) {
68
+ console.error(
69
+ 'Usage: format-release-notes.mjs --release-id <releaseId> --release-version <version> --repository <repository> [--commit-sha <sha>]'
70
+ );
71
+ process.exit(1);
72
+ }
73
+
74
+ try {
75
+ // Get current release body
76
+ const result = await $`gh api repos/${repository}/releases/${releaseId}`.run({
77
+ capture: true,
78
+ });
79
+ const releaseData = JSON.parse(result.stdout);
80
+
81
+ const currentBody = releaseData.body || '';
82
+
83
+ // Skip if already formatted (has shields.io badge image)
84
+ if (currentBody.includes('img.shields.io')) {
85
+ console.log('ℹ️ Release notes already formatted');
86
+ process.exit(0);
87
+ }
88
+
89
+ // Extract changes section (Major, Minor, or Patch)
90
+ // This regex handles multiple formats:
91
+ // 1. With commit hash: "### [Major|Minor|Patch] Changes\n- abc1234: Description"
92
+ // 2. Without commit hash: "### [Major|Minor|Patch] Changes\n- Description"
93
+ const changesPattern =
94
+ /### (Major|Minor|Patch) Changes\s*\n\s*-\s+(?:([a-f0-9]+):\s+)?(.+?)$/s;
95
+ const changesMatch = currentBody.match(changesPattern);
96
+
97
+ let commitHash = null;
98
+ let rawDescription = null;
99
+ let changeType = null;
100
+
101
+ if (changesMatch) {
102
+ // Extract: [full match, changeType, commitHash (optional), description]
103
+ [, changeType, commitHash, rawDescription] = changesMatch;
104
+ console.log(`ℹ️ Found ${changeType} Changes section`);
105
+
106
+ // If commitHash is undefined and description contains it, try to extract
107
+ if (!commitHash && rawDescription) {
108
+ // This handles the case where description itself might be null/undefined
109
+ // and we need to safely check for commit hash at the start
110
+ const descWithHashMatch = rawDescription.match(/^([a-f0-9]+):\s+(.+)$/s);
111
+ if (descWithHashMatch) {
112
+ [, commitHash, rawDescription] = descWithHashMatch;
113
+ }
114
+ }
115
+ } else {
116
+ console.log('⚠️ Could not parse changes from release notes');
117
+ console.log(' Looking for pattern: ### [Major|Minor|Patch] Changes');
118
+ process.exit(0);
119
+ }
120
+
121
+ // Clean up the description:
122
+ // 1. Convert literal \n sequences (escaped newlines from GitHub API) to actual newlines
123
+ // 2. Remove leading/trailing quotes (including escaped quotes from command-stream shell escaping)
124
+ // 3. Remove any trailing npm package links or markdown that might be there
125
+ // 4. Normalize whitespace while preserving line breaks
126
+ const cleanDescription = rawDescription
127
+ .replace(/\\n/g, '\n') // Convert escaped \n to actual newlines
128
+ .replace(/^(\\['"])+/g, '') // Remove leading escaped quotes (e.g., \', \", \'', \'')
129
+ .replace(/(['"])+$/g, '') // Remove trailing unescaped quotes (e.g., ', ", '', '')
130
+ .replace(/^(['"])+/g, '') // Remove leading unescaped quotes
131
+ .replace(/📦.*$/s, '') // Remove any existing npm package info
132
+ .replace(/---.*$/s, '') // Remove any existing separators and everything after
133
+ .trim()
134
+ .split('\n') // Split by lines
135
+ .map((line) => line.trim()) // Trim whitespace from each line
136
+ .join('\n') // Rejoin with newlines
137
+ .replace(/\n{3,}/g, '\n\n'); // Normalize excessive blank lines (3+ becomes 2)
138
+
139
+ // Find the PR that contains the release commit
140
+ // Uses commit hash from changelog or passed commit SHA from workflow
141
+ let prNumber = null;
142
+
143
+ // Determine which commit SHA to use for PR lookup
144
+ const commitShaToLookup = commitHash || passedCommitSha;
145
+
146
+ if (commitShaToLookup) {
147
+ const source = commitHash ? 'changelog' : 'workflow';
148
+ console.log(
149
+ `ℹ️ Looking up PR for commit ${commitShaToLookup} (from ${source})`
150
+ );
151
+
152
+ try {
153
+ const prResult =
154
+ await $`gh api "repos/${repository}/commits/${commitShaToLookup}/pulls"`.run(
155
+ { capture: true }
156
+ );
157
+ const prsData = JSON.parse(prResult.stdout);
158
+
159
+ // Find the PR that's not the version bump PR (not "chore: version packages")
160
+ const relevantPr = prsData.find(
161
+ (pr) => !pr.title.includes('version packages')
162
+ );
163
+
164
+ if (relevantPr) {
165
+ prNumber = relevantPr.number;
166
+ console.log(`✅ Found PR #${prNumber} containing commit`);
167
+ } else if (prsData.length > 0) {
168
+ console.log(
169
+ '⚠️ Found PRs but all are version bump PRs, not linking any'
170
+ );
171
+ } else {
172
+ console.log(
173
+ 'ℹ️ No PR found containing this commit - not adding PR link'
174
+ );
175
+ }
176
+ } catch (error) {
177
+ console.log('⚠️ Could not find PR for commit', commitShaToLookup);
178
+ console.log(' Error:', error.message);
179
+ if (process.env.DEBUG) {
180
+ console.error(error);
181
+ }
182
+ }
183
+ } else {
184
+ // No commit hash available from any source
185
+ console.log('ℹ️ No commit SHA available - not adding PR link');
186
+ }
187
+
188
+ // Build formatted release notes
189
+ const versionWithoutV = version.replace(/^v/, '');
190
+ const npmBadge = `[![npm version](https://img.shields.io/badge/npm-${versionWithoutV}-blue.svg)](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`;
191
+
192
+ let formattedBody = `${cleanDescription}`;
193
+
194
+ // Add PR link if available
195
+ if (prNumber) {
196
+ formattedBody += `\n\n**Related Pull Request:** #${prNumber}`;
197
+ }
198
+
199
+ formattedBody += `\n\n---\n\n${npmBadge}`;
200
+
201
+ // Update the release using JSON input to properly handle special characters
202
+ const updatePayload = JSON.stringify({ body: formattedBody });
203
+ await $`gh api repos/${repository}/releases/${releaseId} -X PATCH --input -`.run(
204
+ { stdin: updatePayload }
205
+ );
206
+
207
+ console.log(`✅ Formatted release notes for v${versionWithoutV}`);
208
+ if (prNumber) {
209
+ console.log(` - Added link to PR #${prNumber}`);
210
+ }
211
+ console.log(' - Added shields.io npm badge');
212
+ console.log(' - Cleaned up formatting');
213
+ } catch (error) {
214
+ console.error('❌ Error formatting release notes:', error.message);
215
+ process.exit(1);
216
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Instant version bump script for manual releases
5
+ * Bypasses the changeset workflow and directly updates version and changelog
6
+ *
7
+ * Usage: node scripts/instant-version-bump.mjs --bump-type <major|minor|patch> [--description <description>]
8
+ *
9
+ * Uses link-foundation libraries:
10
+ * - use-m: Dynamic package loading without package.json dependencies
11
+ * - command-stream: Modern shell command execution with streaming support
12
+ * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
13
+ */
14
+
15
+ import { readFileSync, writeFileSync } from 'fs';
16
+
17
+ // Load use-m dynamically
18
+ const { use } = eval(
19
+ await (await fetch('https://unpkg.com/use-m/use.js')).text()
20
+ );
21
+
22
+ // Import link-foundation libraries
23
+ const { $ } = await use('command-stream');
24
+ const { makeConfig } = await use('lino-arguments');
25
+
26
+ // Parse CLI arguments using lino-arguments
27
+ const config = makeConfig({
28
+ yargs: ({ yargs, getenv }) =>
29
+ yargs
30
+ .option('bump-type', {
31
+ type: 'string',
32
+ default: getenv('BUMP_TYPE', ''),
33
+ describe: 'Version bump type: major, minor, or patch',
34
+ choices: ['major', 'minor', 'patch'],
35
+ })
36
+ .option('description', {
37
+ type: 'string',
38
+ default: getenv('DESCRIPTION', ''),
39
+ describe: 'Description for the version bump',
40
+ }),
41
+ });
42
+
43
+ try {
44
+ const { bumpType, description } = config;
45
+ const finalDescription = description || `Manual ${bumpType} release`;
46
+
47
+ if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) {
48
+ console.error(
49
+ 'Usage: node scripts/instant-version-bump.mjs --bump-type <major|minor|patch> [--description <description>]'
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ console.log(`\nBumping version (${bumpType})...`);
55
+
56
+ // Get current version
57
+ const packageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
58
+ const oldVersion = packageJson.version;
59
+ console.log(`Current version: ${oldVersion}`);
60
+
61
+ // Bump version using npm version (doesn't create git tag)
62
+ await $`npm version ${bumpType} --no-git-tag-version`;
63
+
64
+ // Get new version
65
+ const updatedPackageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
66
+ const newVersion = updatedPackageJson.version;
67
+ console.log(`New version: ${newVersion}`);
68
+
69
+ // Update CHANGELOG.md
70
+ console.log('\nUpdating CHANGELOG.md...');
71
+ const changelogPath = 'CHANGELOG.md';
72
+ let changelog = readFileSync(changelogPath, 'utf-8');
73
+
74
+ // Create new changelog entry
75
+ const newEntry = `## ${newVersion}
76
+
77
+ ### ${bumpType.charAt(0).toUpperCase() + bumpType.slice(1)} Changes
78
+
79
+ - ${finalDescription}
80
+
81
+ `;
82
+
83
+ // Insert new entry after the first heading (# Changelog or similar)
84
+ // Look for the first ## heading and insert before it
85
+ const firstVersionMatch = changelog.match(/^## /m);
86
+
87
+ if (firstVersionMatch) {
88
+ const insertPosition = firstVersionMatch.index;
89
+ changelog =
90
+ changelog.slice(0, insertPosition) +
91
+ newEntry +
92
+ changelog.slice(insertPosition);
93
+ } else {
94
+ // If no version headings exist, append after the main heading
95
+ const mainHeadingMatch = changelog.match(/^# .+$/m);
96
+ if (mainHeadingMatch) {
97
+ const insertPosition =
98
+ mainHeadingMatch.index + mainHeadingMatch[0].length;
99
+ changelog = `${changelog.slice(0, insertPosition)}\n\n${newEntry}${changelog.slice(insertPosition)}`;
100
+ } else {
101
+ // If no headings at all, prepend
102
+ changelog = `${newEntry}\n${changelog}`;
103
+ }
104
+ }
105
+
106
+ writeFileSync(changelogPath, changelog, 'utf-8');
107
+ console.log('✅ CHANGELOG.md updated');
108
+
109
+ // Synchronize package-lock.json
110
+ console.log('\nSynchronizing package-lock.json...');
111
+ await $`npm install --package-lock-only`;
112
+
113
+ console.log('\n✅ Instant version bump complete');
114
+ console.log(`Version: ${oldVersion} → ${newVersion}`);
115
+ } catch (error) {
116
+ console.error('Error during instant version bump:', error.message);
117
+ if (process.env.DEBUG) {
118
+ console.error('Stack trace:', error.stack);
119
+ }
120
+ process.exit(1);
121
+ }
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Merge multiple changeset files into a single changeset
5
+ *
6
+ * Key behavior:
7
+ * - Combines all pending changesets into a single changeset file
8
+ * - Uses the highest version bump type (major > minor > patch)
9
+ * - Preserves all descriptions in chronological order (by file modification time)
10
+ * - Removes the individual changeset files after merging
11
+ * - Does nothing if there's only one or no changesets
12
+ *
13
+ * This script is run before `changeset version` to ensure a clean release
14
+ * even when multiple PRs have merged before a release cycle.
15
+ */
16
+
17
+ import {
18
+ readdirSync,
19
+ readFileSync,
20
+ writeFileSync,
21
+ unlinkSync,
22
+ statSync,
23
+ } from 'fs';
24
+ import { join } from 'path';
25
+
26
+ const PACKAGE_NAME = 'browser-commander';
27
+ const CHANGESET_DIR = '.changeset';
28
+
29
+ // Version bump type priority (higher number = higher priority)
30
+ const BUMP_PRIORITY = {
31
+ patch: 1,
32
+ minor: 2,
33
+ major: 3,
34
+ };
35
+
36
+ /**
37
+ * Generate a random changeset file name (similar to what @changesets/cli does)
38
+ * @returns {string}
39
+ */
40
+ function generateChangesetName() {
41
+ const adjectives = [
42
+ 'bright',
43
+ 'calm',
44
+ 'cool',
45
+ 'cyan',
46
+ 'dark',
47
+ 'fast',
48
+ 'gold',
49
+ 'good',
50
+ 'green',
51
+ 'happy',
52
+ 'kind',
53
+ 'loud',
54
+ 'neat',
55
+ 'nice',
56
+ 'pink',
57
+ 'proud',
58
+ 'quick',
59
+ 'red',
60
+ 'rich',
61
+ 'safe',
62
+ 'shy',
63
+ 'soft',
64
+ 'sweet',
65
+ 'tall',
66
+ 'warm',
67
+ 'wise',
68
+ 'young',
69
+ ];
70
+ const nouns = [
71
+ 'apple',
72
+ 'bird',
73
+ 'book',
74
+ 'car',
75
+ 'cat',
76
+ 'cloud',
77
+ 'desk',
78
+ 'dog',
79
+ 'door',
80
+ 'fish',
81
+ 'flower',
82
+ 'frog',
83
+ 'grass',
84
+ 'house',
85
+ 'key',
86
+ 'lake',
87
+ 'leaf',
88
+ 'moon',
89
+ 'mouse',
90
+ 'owl',
91
+ 'park',
92
+ 'rain',
93
+ 'river',
94
+ 'rock',
95
+ 'sea',
96
+ 'star',
97
+ 'sun',
98
+ 'tree',
99
+ 'wave',
100
+ 'wind',
101
+ ];
102
+
103
+ const randomAdjective =
104
+ adjectives[Math.floor(Math.random() * adjectives.length)];
105
+ const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
106
+
107
+ return `${randomAdjective}-${randomNoun}`;
108
+ }
109
+
110
+ /**
111
+ * Parse a changeset file and extract its metadata
112
+ * @param {string} filePath
113
+ * @returns {{type: string, description: string, mtime: Date} | null}
114
+ */
115
+ function parseChangeset(filePath) {
116
+ try {
117
+ const content = readFileSync(filePath, 'utf-8');
118
+ const stats = statSync(filePath);
119
+
120
+ // Extract version type - support both quoted and unquoted package names
121
+ const versionTypeRegex = new RegExp(
122
+ `^['"]?${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]?:\\s+(major|minor|patch)`,
123
+ 'm'
124
+ );
125
+ const versionTypeMatch = content.match(versionTypeRegex);
126
+
127
+ if (!versionTypeMatch) {
128
+ console.warn(
129
+ `Warning: Could not parse version type from ${filePath}, skipping`
130
+ );
131
+ return null;
132
+ }
133
+
134
+ // Extract description
135
+ const parts = content.split('---');
136
+ const description =
137
+ parts.length >= 3 ? parts.slice(2).join('---').trim() : '';
138
+
139
+ return {
140
+ type: versionTypeMatch[1],
141
+ description,
142
+ mtime: stats.mtime,
143
+ };
144
+ } catch (error) {
145
+ console.warn(`Warning: Failed to parse ${filePath}: ${error.message}`);
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get the highest priority bump type
152
+ * @param {string[]} types
153
+ * @returns {string}
154
+ */
155
+ function getHighestBumpType(types) {
156
+ let highest = 'patch';
157
+ for (const type of types) {
158
+ if (BUMP_PRIORITY[type] > BUMP_PRIORITY[highest]) {
159
+ highest = type;
160
+ }
161
+ }
162
+ return highest;
163
+ }
164
+
165
+ /**
166
+ * Create a merged changeset file
167
+ * @param {string} type
168
+ * @param {string[]} descriptions
169
+ * @returns {string}
170
+ */
171
+ function createMergedChangeset(type, descriptions) {
172
+ const combinedDescription = descriptions.join('\n\n');
173
+
174
+ return `---
175
+ '${PACKAGE_NAME}': ${type}
176
+ ---
177
+
178
+ ${combinedDescription}
179
+ `;
180
+ }
181
+
182
+ function main() {
183
+ console.log('Checking for multiple changesets to merge...');
184
+
185
+ // Get all changeset files
186
+ const changesetFiles = readdirSync(CHANGESET_DIR).filter(
187
+ (file) => file.endsWith('.md') && file !== 'README.md'
188
+ );
189
+
190
+ console.log(`Found ${changesetFiles.length} changeset file(s)`);
191
+
192
+ // If 0 or 1 changesets, nothing to merge
193
+ if (changesetFiles.length <= 1) {
194
+ console.log('No merging needed (0 or 1 changeset found)');
195
+ return;
196
+ }
197
+
198
+ console.log('Multiple changesets found, merging...');
199
+ changesetFiles.forEach((file) => console.log(` - ${file}`));
200
+
201
+ // Parse all changesets
202
+ const parsedChangesets = [];
203
+ for (const file of changesetFiles) {
204
+ const filePath = join(CHANGESET_DIR, file);
205
+ const parsed = parseChangeset(filePath);
206
+ if (parsed) {
207
+ parsedChangesets.push({
208
+ file,
209
+ filePath,
210
+ ...parsed,
211
+ });
212
+ }
213
+ }
214
+
215
+ if (parsedChangesets.length === 0) {
216
+ console.error('Error: No valid changesets could be parsed');
217
+ process.exit(1);
218
+ }
219
+
220
+ // Sort by modification time (oldest first) to preserve chronological order
221
+ parsedChangesets.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
222
+
223
+ // Determine the highest bump type
224
+ const bumpTypes = parsedChangesets.map((c) => c.type);
225
+ const highestBumpType = getHighestBumpType(bumpTypes);
226
+
227
+ console.log(`\nMerge summary:`);
228
+ console.log(` Bump types found: ${[...new Set(bumpTypes)].join(', ')}`);
229
+ console.log(` Using highest: ${highestBumpType}`);
230
+
231
+ // Collect descriptions in chronological order
232
+ const descriptions = parsedChangesets
233
+ .filter((c) => c.description)
234
+ .map((c) => c.description);
235
+
236
+ console.log(` Descriptions to merge: ${descriptions.length}`);
237
+
238
+ // Create merged changeset content
239
+ const mergedContent = createMergedChangeset(highestBumpType, descriptions);
240
+
241
+ // Generate a unique name for the merged changeset
242
+ const mergedFileName = `merged-${generateChangesetName()}.md`;
243
+ const mergedFilePath = join(CHANGESET_DIR, mergedFileName);
244
+
245
+ // Write the merged changeset
246
+ writeFileSync(mergedFilePath, mergedContent);
247
+ console.log(`\nCreated merged changeset: ${mergedFileName}`);
248
+
249
+ // Remove the original changeset files
250
+ console.log('\nRemoving original changeset files:');
251
+ for (const changeset of parsedChangesets) {
252
+ unlinkSync(changeset.filePath);
253
+ console.log(` Removed: ${changeset.file}`);
254
+ }
255
+
256
+ console.log('\nChangeset merge completed successfully');
257
+ console.log(`\nMerged changeset content:\n${mergedContent}`);
258
+ }
259
+
260
+ main();