@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.4

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/.agent/workflows/dev-workflow-bugfix.md +250 -250
  2. package/.agent/workflows/dev-workflow-feature.md +201 -201
  3. package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
  4. package/.agent/workflows/dev-workflow-refactor.md +238 -238
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
  7. package/.github/pull_request_template.md +22 -22
  8. package/.github/workflows/github-release.yml +56 -0
  9. package/.github/workflows/npm-publish.yml +50 -50
  10. package/.husky/commit-msg +0 -0
  11. package/CHANGELOG.md +759 -741
  12. package/CODE_OF_CONDUCT.md +79 -79
  13. package/CONTRIBUTING.md +60 -60
  14. package/LICENSE +21 -21
  15. package/README.md +632 -632
  16. package/SECURITY.md +20 -20
  17. package/db/index.js +204 -204
  18. package/docs/NETLIFY_DEPLOYMENT.md +125 -125
  19. package/docs/PLESK_DEPLOYMENT.md +170 -170
  20. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
  21. package/docs/antigravity/EXAMPLES.md +535 -535
  22. package/docs/antigravity/GETTING-STARTED.md +292 -292
  23. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  24. package/docs/antigravity/WALKTHROUGH.md +25 -25
  25. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  26. package/docs/product/PRD.md +67 -67
  27. package/docs/release-notes/RELEASE_NOTES.md +233 -226
  28. package/docs/web-dashboard.md +175 -175
  29. package/exec.js +49 -49
  30. package/index.js +279 -279
  31. package/install.js +73 -73
  32. package/package.json +4 -4
  33. package/release-wrapper.js +192 -192
  34. package/scripts/build-docs.js +63 -63
  35. package/scripts/extract-release-notes.cjs +78 -0
  36. package/scripts/hello-world.js +1 -1
  37. package/scripts/release.cjs +252 -252
  38. package/scripts/release.js +252 -0
  39. package/scripts/update-release-notes.cjs +178 -178
  40. package/scripts/update-release-notes.js +182 -0
  41. package/server.json +23 -23
  42. package/tailwind.config.js +8 -8
  43. package/tests/build.test.js +83 -83
  44. package/tests/exec.test.js +35 -35
  45. package/tests/handlers.test.js +1058 -1058
  46. package/tests/hello-world.test.js +13 -13
  47. package/tests/mcpRegistry.test.js +65 -65
  48. package/tests/patch-release.test.js +7 -0
  49. package/tests/release-wrapper.test.js +107 -107
  50. package/tests/workflowState.test.js +505 -505
  51. package/tools/definitions.js +332 -332
  52. package/tools/handlers.js +1019 -1019
  53. package/vite.config.js +43 -43
  54. package/vite.web.config.js +23 -23
  55. package/web/README.md +36 -36
  56. package/web/app/docs/page.tsx +202 -202
  57. package/web/app/globals.css +90 -90
  58. package/web/app/layout.tsx +36 -36
  59. package/web/app/page.tsx +15 -15
  60. package/web/components/ApiReference.tsx +20 -20
  61. package/web/components/Features.tsx +61 -61
  62. package/web/components/Footer.tsx +9 -9
  63. package/web/components/Hero.tsx +96 -96
  64. package/web/components/History.tsx +143 -143
  65. package/web/components/Navbar.tsx +64 -64
  66. package/web/components/Terminal.tsx +114 -114
  67. package/web/eslint.config.mjs +18 -18
  68. package/web/lib/db.js +131 -131
  69. package/web/lib/docs.js +73 -73
  70. package/web/netlify.toml +3 -3
  71. package/web/next.config.ts +12 -12
  72. package/web/package-lock.json +6560 -6560
  73. package/web/package.json +28 -28
  74. package/web/postcss.config.mjs +9 -9
  75. package/web/server.js +31 -31
  76. package/web/tailwind.config.ts +19 -19
  77. package/web/tsconfig.json +42 -42
  78. package/workflow-state.js +612 -612
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const { updateReleaseNotes } = require('./update-release-notes');
8
+
9
+ const fsFokPreloadPath = path.join(__dirname, 'preload', 'fs-f-ok.cjs');
10
+ const FS_FOK_PRELOAD_FLAG = buildPreloadFlag(fsFokPreloadPath);
11
+
12
+ require('./preload/fs-f-ok.cjs');
13
+
14
+ const VALID_RELEASE_TYPES = new Set([
15
+ 'major',
16
+ 'minor',
17
+ 'patch',
18
+ 'premajor',
19
+ 'preminor',
20
+ 'prepatch',
21
+ 'prerelease'
22
+ ]);
23
+
24
+ const SEMVER_REGEX = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
25
+
26
+ const LOCKFILE_PREFERENCES = [
27
+ { manager: 'pnpm', file: 'pnpm-lock.yaml' },
28
+ { manager: 'yarn', file: 'yarn.lock' },
29
+ { manager: 'bun', file: 'bun.lockb' },
30
+ { manager: 'npm', file: 'package-lock.json' }
31
+ ];
32
+
33
+ function getCliArguments(argv = process.argv) {
34
+ const rawArgs = argv.slice(2);
35
+ if (rawArgs.length === 0) {
36
+ return { releaseType: undefined, extraArgs: [] };
37
+ }
38
+
39
+ if (rawArgs[0].startsWith('-')) {
40
+ return { releaseType: undefined, extraArgs: rawArgs };
41
+ }
42
+
43
+ const [firstArg, ...rest] = rawArgs;
44
+ return { releaseType: firstArg, extraArgs: rest };
45
+ }
46
+
47
+ function getNpmRunArgument(env = process.env) {
48
+ try {
49
+ const npmArgs = JSON.parse(env.npm_config_argv || '{}');
50
+ const original = npmArgs.original || [];
51
+ const releaseIndex = original.lastIndexOf('release');
52
+ if (releaseIndex !== -1) {
53
+ return original[releaseIndex + 1];
54
+ }
55
+ } catch (error) {
56
+ // Ignore JSON parsing issues and fall back to defaults
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ function buildStandardVersionArgs({ releaseType, extraArgs }) {
62
+ const args = [];
63
+ if (releaseType) {
64
+ const normalized = releaseType.trim();
65
+ const isValid = VALID_RELEASE_TYPES.has(normalized) || SEMVER_REGEX.test(normalized);
66
+ if (!isValid) {
67
+ const allowed = Array.from(VALID_RELEASE_TYPES).join(', ');
68
+ throw new Error(`Unknown release type "${normalized}". Use one of ${allowed} or a valid semver version.`);
69
+ }
70
+ args.push('--release-as', normalized);
71
+ }
72
+
73
+ if (Array.isArray(extraArgs) && extraArgs.length > 0) {
74
+ args.push(...extraArgs);
75
+ }
76
+
77
+ return args;
78
+ }
79
+
80
+ function loadPackageJson(cwd = process.cwd()) {
81
+ try {
82
+ const packageJsonPath = path.join(cwd, 'package.json');
83
+ const contents = fs.readFileSync(packageJsonPath, 'utf8');
84
+ return JSON.parse(contents);
85
+ } catch (error) {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ function detectPackageManager({ env = process.env, cwd = process.cwd() } = {}) {
91
+ const userAgent = env.npm_config_user_agent || '';
92
+ if (userAgent.startsWith('pnpm/')) return 'pnpm';
93
+ if (userAgent.startsWith('yarn/')) return 'yarn';
94
+ if (userAgent.startsWith('bun/')) return 'bun';
95
+
96
+ for (const { manager, file } of LOCKFILE_PREFERENCES) {
97
+ if (fs.existsSync(path.join(cwd, file))) {
98
+ return manager;
99
+ }
100
+ }
101
+
102
+ return 'npm';
103
+ }
104
+
105
+ function buildTestCommand(packageManager) {
106
+ switch (packageManager) {
107
+ case 'pnpm':
108
+ return { command: 'pnpm', args: ['test'] };
109
+ case 'yarn':
110
+ return { command: 'yarn', args: ['test'] };
111
+ case 'bun':
112
+ return { command: 'bun', args: ['test'] };
113
+ default:
114
+ return { command: 'npm', args: ['test'] };
115
+ }
116
+ }
117
+
118
+ function runProjectTests({ spawn = spawnSync, env = process.env, cwd = process.cwd() } = {}) {
119
+ const packageJson = loadPackageJson(cwd);
120
+ const scripts = packageJson && packageJson.scripts ? packageJson.scripts : {};
121
+
122
+ if (!scripts.test) {
123
+ console.log('ℹ️ Skipping tests: no "test" script detected in package.json');
124
+ return { status: 0 };
125
+ }
126
+
127
+ const packageManager = detectPackageManager({ env, cwd });
128
+ const { command, args } = buildTestCommand(packageManager);
129
+
130
+ console.log(`🧪 Running tests with ${packageManager} ${args.join(' ')}`.trim());
131
+ return spawn(command, args, {
132
+ stdio: 'inherit',
133
+ env
134
+ });
135
+ }
136
+
137
+ function runRelease({
138
+ argv = process.argv,
139
+ env = process.env,
140
+ spawn = spawnSync,
141
+ cwd = process.cwd(),
142
+ dependencies = {}
143
+ } = {}) {
144
+ const { releaseType: cliReleaseType, extraArgs } = getCliArguments(argv);
145
+ const inferredReleaseType = cliReleaseType || getNpmRunArgument(env);
146
+ const standardVersionArgs = buildStandardVersionArgs({
147
+ releaseType: inferredReleaseType,
148
+ extraArgs
149
+ });
150
+
151
+ const checkWorkingTreeClean = dependencies.isWorkingTreeClean || (() => isWorkingTreeClean({ cwd }));
152
+ const workingTreeClean = checkWorkingTreeClean();
153
+ if (!workingTreeClean) {
154
+ throw new Error('Working tree has uncommitted changes. Commit or stash before running release.');
155
+ }
156
+
157
+ const testResult = runProjectTests({ spawn, env });
158
+ if (testResult && typeof testResult.status === 'number' && testResult.status !== 0) {
159
+ return testResult;
160
+ }
161
+
162
+ const standardVersionBin = require.resolve('standard-version/bin/cli.js');
163
+ const childEnv = appendPreloadToNodeOptions(env, FS_FOK_PRELOAD_FLAG);
164
+ const releaseResult = spawn(process.execPath, [standardVersionBin, ...standardVersionArgs], {
165
+ stdio: 'inherit',
166
+ env: childEnv,
167
+ cwd
168
+ });
169
+
170
+ if (releaseResult && typeof releaseResult.status === 'number' && releaseResult.status !== 0) {
171
+ return releaseResult;
172
+ }
173
+
174
+ const releaseNotesPath = dependencies.releaseNotesPath || path.join('docs', 'release-notes', 'RELEASE_NOTES.md');
175
+ const updateNotes = dependencies.updateReleaseNotes || ((options = {}) => updateReleaseNotes({ rootDir: cwd, ...options }));
176
+
177
+ let notesUpdated = false;
178
+ try {
179
+ notesUpdated = updateNotes();
180
+ } catch (error) {
181
+ console.warn(`⚠️ Skipping release notes update: ${error.message}`);
182
+ }
183
+
184
+ if (notesUpdated) {
185
+ const gitAddResult = spawnSync('git', ['add', releaseNotesPath], { stdio: 'inherit', cwd });
186
+ if (!gitAddResult || typeof gitAddResult.status !== 'number' || gitAddResult.status !== 0) {
187
+ console.warn('⚠️ Release notes updated but failed to stage. Please add manually: git add ' + releaseNotesPath);
188
+ }
189
+ }
190
+
191
+ return releaseResult;
192
+ }
193
+
194
+ if (require.main === module) {
195
+ try {
196
+ const result = runRelease();
197
+ if (result.status !== 0) {
198
+ process.exit(result.status ?? 1);
199
+ }
200
+ } catch (error) {
201
+ console.error(`❌ ${error.message}`);
202
+ process.exit(1);
203
+ }
204
+ }
205
+
206
+ module.exports = {
207
+ VALID_RELEASE_TYPES,
208
+ SEMVER_REGEX,
209
+ getCliArguments,
210
+ getNpmRunArgument,
211
+ buildStandardVersionArgs,
212
+ loadPackageJson,
213
+ detectPackageManager,
214
+ runProjectTests,
215
+ runRelease,
216
+ isWorkingTreeClean
217
+ };
218
+
219
+ function buildPreloadFlag(filePath) {
220
+ const resolved = path.resolve(filePath);
221
+ const escaped = resolved.includes(' ')
222
+ ? `"${resolved.replace(/"/g, '\"')}"`
223
+ : resolved;
224
+ return `--require ${escaped}`;
225
+ }
226
+
227
+ function appendPreloadToNodeOptions(env, preloadFlag) {
228
+ const nextEnv = { ...env };
229
+ const existing = typeof nextEnv.NODE_OPTIONS === 'string' ? nextEnv.NODE_OPTIONS.trim() : '';
230
+ nextEnv.NODE_OPTIONS = existing ? `${existing} ${preloadFlag}` : preloadFlag;
231
+ return nextEnv;
232
+ }
233
+
234
+ function isWorkingTreeClean({ cwd = process.cwd() } = {}) {
235
+ const result = spawnSync('git', ['status', '--porcelain'], {
236
+ cwd,
237
+ encoding: 'utf8'
238
+ });
239
+
240
+ if (result.error) {
241
+ throw result.error;
242
+ }
243
+
244
+ if (typeof result.status === 'number' && result.status !== 0) {
245
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
246
+ const message = stderr || 'Failed to determine working tree status.';
247
+ throw new Error(message);
248
+ }
249
+
250
+ const output = typeof result.stdout === 'string' ? result.stdout : '';
251
+ return output.trim().length === 0;
252
+ }
@@ -1,178 +1,178 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function updateReleaseNotes({
7
- rootDir = process.cwd(),
8
- releaseNotesRelativePath = path.join('docs', 'release-notes', 'RELEASE_NOTES.md'),
9
- changelogRelativePath = 'CHANGELOG.md',
10
- packageJsonRelativePath = 'package.json',
11
- now = () => new Date()
12
- } = {}) {
13
- const releaseNotesPath = path.join(rootDir, releaseNotesRelativePath);
14
- if (!fs.existsSync(releaseNotesPath)) {
15
- console.log(`ℹ️ Release notes file not found at ${releaseNotesPath}; skipping update.`);
16
- return false;
17
- }
18
-
19
- const packageJsonPath = path.join(rootDir, packageJsonRelativePath);
20
- if (!fs.existsSync(packageJsonPath)) {
21
- console.log(`ℹ️ package.json not found at ${packageJsonPath}; skipping release notes update.`);
22
- return false;
23
- }
24
-
25
- const changelogPath = path.join(rootDir, changelogRelativePath);
26
- if (!fs.existsSync(changelogPath)) {
27
- console.log(`ℹ️ CHANGELOG not found at ${changelogPath}; skipping release notes update.`);
28
- return false;
29
- }
30
-
31
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
32
- const version = packageJson.version;
33
- if (!version) {
34
- console.log('ℹ️ package.json version is missing; skipping release notes update.');
35
- return false;
36
- }
37
-
38
- const releaseNotesContent = fs.readFileSync(releaseNotesPath, 'utf8');
39
- if (releaseNotesContent.includes(`| ${version} `) || releaseNotesContent.includes(`## ${version}`)) {
40
- console.log(`ℹ️ Release notes already contain version ${version}; skipping.`);
41
- return false;
42
- }
43
-
44
- const changelogContent = fs.readFileSync(changelogPath, 'utf8');
45
- const changelogInfo = extractChangelogInfo({ changelogContent, version });
46
-
47
- const releaseDate = changelogInfo.date || formatDate(now());
48
- const highlight = changelogInfo.highlight || changelogInfo.heading || 'See CHANGELOG for details.';
49
- const sectionHeading = changelogInfo.heading || '';
50
- const detailBullets = Array.isArray(changelogInfo.bullets) && changelogInfo.bullets.length > 0
51
- ? changelogInfo.bullets
52
- : [highlight];
53
-
54
- const updatedContent = insertReleaseNotesEntry({
55
- content: releaseNotesContent,
56
- version,
57
- releaseDate,
58
- highlight,
59
- sectionHeading,
60
- detailBullets
61
- });
62
-
63
- fs.writeFileSync(releaseNotesPath, updatedContent);
64
- console.log(`✅ Added release notes entry for version ${version}.`);
65
- return true;
66
- }
67
-
68
- function insertReleaseNotesEntry({ content, version, releaseDate, highlight, sectionHeading, detailBullets }) {
69
- const lines = content.split('\n');
70
-
71
- // Find the "## Unreleased" section
72
- const unreleasedIndex = lines.findIndex((line) => line.trim().startsWith('## Unreleased'));
73
- if (unreleasedIndex === -1) {
74
- throw new Error('Unable to locate "## Unreleased" section in release notes.');
75
- }
76
-
77
- const sectionLines = [];
78
- sectionLines.push('');
79
- const trimmedHeading = typeof sectionHeading === 'string' ? sectionHeading.trim() : '';
80
- const headingSuffix = trimmedHeading ? ` — ${trimmedHeading}` : '';
81
-
82
- // Format: ## [Version] — [Date]
83
- sectionLines.push(`## ${version} — ${releaseDate}${headingSuffix}`);
84
-
85
- for (const bullet of detailBullets) {
86
- sectionLines.push(`- ${bullet}`);
87
- }
88
-
89
- // Insert after the Unreleased header (and potentially some blank lines)
90
- // We want to keep "## Unreleased" empty and add the new version below it.
91
- // So we insert after the Unreleased line.
92
- lines.splice(unreleasedIndex + 1, 0, ...sectionLines);
93
-
94
- return lines.join('\n');
95
- }
96
-
97
- function extractChangelogInfo({ changelogContent, version }) {
98
- const versionHeadingRegex = new RegExp(`^### \\[${escapeRegExp(version)}\\][^\n]*`, 'm');
99
- const match = changelogContent.match(versionHeadingRegex);
100
- if (!match) {
101
- return {};
102
- }
103
-
104
- const headingIndex = match.index;
105
- const rest = changelogContent.slice(headingIndex + match[0].length);
106
- const nextVersionRegex = /^### \[[^\n]*\]/m;
107
- const nextVersionMatch = nextVersionRegex.exec(rest);
108
- const section = nextVersionMatch ? rest.slice(0, nextVersionMatch.index) : rest;
109
-
110
- const dateMatch = match[0].match(/\((\d{4}-\d{2}-\d{2})\)/);
111
- const date = dateMatch ? dateMatch[1] : undefined;
112
-
113
- const categoryRegex = /^###\s+([^\n]+)$/m;
114
- const categoryMatch = section.match(categoryRegex);
115
- let heading;
116
- if (categoryMatch) {
117
- heading = sanitizeText(categoryMatch[1]);
118
- }
119
-
120
- const bullets = [];
121
- const bulletRegex = /^\*\s+([^\n]+)$/gm;
122
- let bulletMatch;
123
- while ((bulletMatch = bulletRegex.exec(section)) !== null) {
124
- const sanitized = sanitizeText(bulletMatch[1]);
125
- if (!bullets.includes(sanitized)) {
126
- bullets.push(sanitized);
127
- }
128
- }
129
-
130
- const highlight = bullets.length > 0 ? bullets[0] : undefined;
131
-
132
- return {
133
- date,
134
- highlight,
135
- heading,
136
- bullets
137
- };
138
- }
139
-
140
- function sanitizeText(text) {
141
- return text
142
- .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
143
- .replace(/\s+/g, ' ')
144
- .trim();
145
- }
146
-
147
- function escapeRegExp(string) {
148
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
149
- }
150
-
151
- function escapePipes(text) {
152
- return text.replace(/\|/g, '\\|');
153
- }
154
-
155
- function formatDate(date) {
156
- const year = date.getFullYear();
157
- const month = String(date.getMonth() + 1).padStart(2, '0');
158
- const day = String(date.getDate()).padStart(2, '0');
159
- return `${year}-${month}-${day}`;
160
- }
161
-
162
- module.exports = {
163
- updateReleaseNotes,
164
- insertReleaseNotesEntry,
165
- extractChangelogInfo,
166
- sanitizeText,
167
- escapePipes,
168
- formatDate
169
- };
170
-
171
- if (require.main === module) {
172
- try {
173
- updateReleaseNotes();
174
- } catch (error) {
175
- console.error(`❌ Failed to update release notes: ${error.message}`);
176
- process.exit(1);
177
- }
178
- }
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function updateReleaseNotes({
7
+ rootDir = process.cwd(),
8
+ releaseNotesRelativePath = path.join('docs', 'release-notes', 'RELEASE_NOTES.md'),
9
+ changelogRelativePath = 'CHANGELOG.md',
10
+ packageJsonRelativePath = 'package.json',
11
+ now = () => new Date()
12
+ } = {}) {
13
+ const releaseNotesPath = path.join(rootDir, releaseNotesRelativePath);
14
+ if (!fs.existsSync(releaseNotesPath)) {
15
+ console.log(`ℹ️ Release notes file not found at ${releaseNotesPath}; skipping update.`);
16
+ return false;
17
+ }
18
+
19
+ const packageJsonPath = path.join(rootDir, packageJsonRelativePath);
20
+ if (!fs.existsSync(packageJsonPath)) {
21
+ console.log(`ℹ️ package.json not found at ${packageJsonPath}; skipping release notes update.`);
22
+ return false;
23
+ }
24
+
25
+ const changelogPath = path.join(rootDir, changelogRelativePath);
26
+ if (!fs.existsSync(changelogPath)) {
27
+ console.log(`ℹ️ CHANGELOG not found at ${changelogPath}; skipping release notes update.`);
28
+ return false;
29
+ }
30
+
31
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
32
+ const version = packageJson.version;
33
+ if (!version) {
34
+ console.log('ℹ️ package.json version is missing; skipping release notes update.');
35
+ return false;
36
+ }
37
+
38
+ const releaseNotesContent = fs.readFileSync(releaseNotesPath, 'utf8');
39
+ if (releaseNotesContent.includes(`| ${version} `) || releaseNotesContent.includes(`## ${version}`)) {
40
+ console.log(`ℹ️ Release notes already contain version ${version}; skipping.`);
41
+ return false;
42
+ }
43
+
44
+ const changelogContent = fs.readFileSync(changelogPath, 'utf8');
45
+ const changelogInfo = extractChangelogInfo({ changelogContent, version });
46
+
47
+ const releaseDate = changelogInfo.date || formatDate(now());
48
+ const highlight = changelogInfo.highlight || changelogInfo.heading || 'See CHANGELOG for details.';
49
+ const sectionHeading = changelogInfo.heading || '';
50
+ const detailBullets = Array.isArray(changelogInfo.bullets) && changelogInfo.bullets.length > 0
51
+ ? changelogInfo.bullets
52
+ : [highlight];
53
+
54
+ const updatedContent = insertReleaseNotesEntry({
55
+ content: releaseNotesContent,
56
+ version,
57
+ releaseDate,
58
+ highlight,
59
+ sectionHeading,
60
+ detailBullets
61
+ });
62
+
63
+ fs.writeFileSync(releaseNotesPath, updatedContent);
64
+ console.log(`✅ Added release notes entry for version ${version}.`);
65
+ return true;
66
+ }
67
+
68
+ function insertReleaseNotesEntry({ content, version, releaseDate, highlight, sectionHeading, detailBullets }) {
69
+ const lines = content.split('\n');
70
+
71
+ // Find the "## Unreleased" section
72
+ const unreleasedIndex = lines.findIndex((line) => line.trim().startsWith('## Unreleased'));
73
+ if (unreleasedIndex === -1) {
74
+ throw new Error('Unable to locate "## Unreleased" section in release notes.');
75
+ }
76
+
77
+ const sectionLines = [];
78
+ sectionLines.push('');
79
+ const trimmedHeading = typeof sectionHeading === 'string' ? sectionHeading.trim() : '';
80
+ const headingSuffix = trimmedHeading ? ` — ${trimmedHeading}` : '';
81
+
82
+ // Format: ## [Version] — [Date]
83
+ sectionLines.push(`## ${version} — ${releaseDate}${headingSuffix}`);
84
+
85
+ for (const bullet of detailBullets) {
86
+ sectionLines.push(`- ${bullet}`);
87
+ }
88
+
89
+ // Insert after the Unreleased header (and potentially some blank lines)
90
+ // We want to keep "## Unreleased" empty and add the new version below it.
91
+ // So we insert after the Unreleased line.
92
+ lines.splice(unreleasedIndex + 1, 0, ...sectionLines);
93
+
94
+ return lines.join('\n');
95
+ }
96
+
97
+ function extractChangelogInfo({ changelogContent, version }) {
98
+ const versionHeadingRegex = new RegExp(`^### \\[${escapeRegExp(version)}\\][^\n]*`, 'm');
99
+ const match = changelogContent.match(versionHeadingRegex);
100
+ if (!match) {
101
+ return {};
102
+ }
103
+
104
+ const headingIndex = match.index;
105
+ const rest = changelogContent.slice(headingIndex + match[0].length);
106
+ const nextVersionRegex = /^### \[[^\n]*\]/m;
107
+ const nextVersionMatch = nextVersionRegex.exec(rest);
108
+ const section = nextVersionMatch ? rest.slice(0, nextVersionMatch.index) : rest;
109
+
110
+ const dateMatch = match[0].match(/\((\d{4}-\d{2}-\d{2})\)/);
111
+ const date = dateMatch ? dateMatch[1] : undefined;
112
+
113
+ const categoryRegex = /^###\s+([^\n]+)$/m;
114
+ const categoryMatch = section.match(categoryRegex);
115
+ let heading;
116
+ if (categoryMatch) {
117
+ heading = sanitizeText(categoryMatch[1]);
118
+ }
119
+
120
+ const bullets = [];
121
+ const bulletRegex = /^\*\s+([^\n]+)$/gm;
122
+ let bulletMatch;
123
+ while ((bulletMatch = bulletRegex.exec(section)) !== null) {
124
+ const sanitized = sanitizeText(bulletMatch[1]);
125
+ if (!bullets.includes(sanitized)) {
126
+ bullets.push(sanitized);
127
+ }
128
+ }
129
+
130
+ const highlight = bullets.length > 0 ? bullets[0] : undefined;
131
+
132
+ return {
133
+ date,
134
+ highlight,
135
+ heading,
136
+ bullets
137
+ };
138
+ }
139
+
140
+ function sanitizeText(text) {
141
+ return text
142
+ .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
143
+ .replace(/\s+/g, ' ')
144
+ .trim();
145
+ }
146
+
147
+ function escapeRegExp(string) {
148
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
149
+ }
150
+
151
+ function escapePipes(text) {
152
+ return text.replace(/\|/g, '\\|');
153
+ }
154
+
155
+ function formatDate(date) {
156
+ const year = date.getFullYear();
157
+ const month = String(date.getMonth() + 1).padStart(2, '0');
158
+ const day = String(date.getDate()).padStart(2, '0');
159
+ return `${year}-${month}-${day}`;
160
+ }
161
+
162
+ module.exports = {
163
+ updateReleaseNotes,
164
+ insertReleaseNotesEntry,
165
+ extractChangelogInfo,
166
+ sanitizeText,
167
+ escapePipes,
168
+ formatDate
169
+ };
170
+
171
+ if (require.main === module) {
172
+ try {
173
+ updateReleaseNotes();
174
+ } catch (error) {
175
+ console.error(`❌ Failed to update release notes: ${error.message}`);
176
+ process.exit(1);
177
+ }
178
+ }