@programinglive/dev-workflow-mcp-server 1.5.1 โ†’ 1.5.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.5.3](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.5.2...v1.5.3) (2025-12-05)
6
+
7
+
8
+ ### ๐Ÿ› Bug Fixes
9
+
10
+ * correct package.json version from null to 1.5.2 for npm publish ([be60c99](https://github.com/programinglive/dev-workflow-mcp-server/commit/be60c99f1e79cdaf5d3b9cb7c780edd30356f420))
11
+ * **news:** improve error handling and form accessibility ([5e0a1a8](https://github.com/programinglive/dev-workflow-mcp-server/commit/5e0a1a83ae18054f967ff9041c7ed81fd7d6d4eb))
12
+ * switch to static export for Netlify deployment ([acddfd7](https://github.com/programinglive/dev-workflow-mcp-server/commit/acddfd78da86859febdb1c46b04a5e79fd8f176f))
13
+
14
+
15
+ ### ๐Ÿงน Chores
16
+
17
+ * **release:** null ๐Ÿš€ ([55d7f99](https://github.com/programinglive/dev-workflow-mcp-server/commit/55d7f9980ea8f188d284facb7c9d1d5d4a6d6210))
18
+ * **release:** prepare patch release 1.5.3 ([5b92c42](https://github.com/programinglive/dev-workflow-mcp-server/commit/5b92c4215e4188a3dc6714d3f2999c65f460d2c9))
19
+
20
+ ### [1.5.2](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.5.1...v1.5.2) (2025-12-02)
21
+
5
22
  ### [1.5.1](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.5.0...v1.5.1) (2025-12-02)
6
23
 
7
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "homepage": "https://devworkflow.programinglive.com/",
6
6
  "mcpName": "io.github.programinglive/dev-workflow-mcp-server",
@@ -22,10 +22,10 @@
22
22
  "check": "node --check index.js",
23
23
  "prepare": "husky",
24
24
  "postinstall": "node install.js",
25
- "release": "node scripts/release.cjs",
26
- "release:major": "node release-wrapper.js major",
27
- "release:minor": "node release-wrapper.js minor",
28
- "release:patch": "node release-wrapper.js patch",
25
+ "release": "node scripts/release.js",
26
+ "release:major": "node scripts/release.js major",
27
+ "release:minor": "node scripts/release.js minor",
28
+ "release:patch": "node scripts/release.js patch",
29
29
  "web": "npm start --prefix web"
30
30
  },
31
31
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "license": "MIT",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/programinglive/dev-workflow-mcp-server.git"
41
+ "url": "git+https://github.com/programinglive/dev-workflow-mcp-server.git"
42
42
  },
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -52,8 +52,8 @@
52
52
  "access": "public"
53
53
  },
54
54
  "devDependencies": {
55
- "@commitlint/cli": "^20.1.0",
56
- "@commitlint/config-conventional": "^20.0.0",
55
+ "@commitlint/cli": "^20.2.0",
56
+ "@commitlint/config-conventional": "^20.2.0",
57
57
  "@programinglive/commiter": "^1.1.8",
58
58
  "autoprefixer": "^10.4.22",
59
59
  "husky": "^9.1.7",
@@ -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
+ }
@@ -0,0 +1,182 @@
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
+ const headerSeparatorIndex = lines.findIndex((line) => line.trim().startsWith('|---------'));
71
+ if (headerSeparatorIndex === -1) {
72
+ throw new Error('Unable to locate release notes table header.');
73
+ }
74
+
75
+ const newRow = `| ${version} | ${releaseDate} | ${escapePipes(highlight)} |`;
76
+ lines.splice(headerSeparatorIndex + 1, 0, newRow);
77
+
78
+ const sectionLines = [];
79
+ sectionLines.push('');
80
+ const trimmedHeading = typeof sectionHeading === 'string' ? sectionHeading.trim() : '';
81
+ const headingSuffix = trimmedHeading ? ` โ€“ ${trimmedHeading}` : '';
82
+ sectionLines.push(`## ${version}${headingSuffix}`);
83
+ sectionLines.push('');
84
+ sectionLines.push(`Released on **${releaseDate}**.`);
85
+ sectionLines.push('');
86
+ for (const bullet of detailBullets) {
87
+ sectionLines.push(`- ${bullet}`);
88
+ }
89
+ sectionLines.push('');
90
+
91
+ const firstSectionIndex = lines.findIndex((line, idx) => idx > headerSeparatorIndex && line.startsWith('## '));
92
+ if (firstSectionIndex === -1) {
93
+ lines.push(...sectionLines);
94
+ } else {
95
+ lines.splice(firstSectionIndex, 0, ...sectionLines);
96
+ }
97
+
98
+ return lines.join('\n');
99
+ }
100
+
101
+ function extractChangelogInfo({ changelogContent, version }) {
102
+ const versionHeadingRegex = new RegExp(`^### \\[${escapeRegExp(version)}\\][^\n]*`, 'm');
103
+ const match = changelogContent.match(versionHeadingRegex);
104
+ if (!match) {
105
+ return {};
106
+ }
107
+
108
+ const headingIndex = match.index;
109
+ const rest = changelogContent.slice(headingIndex + match[0].length);
110
+ const nextVersionRegex = /^### \[[^\n]*\]/m;
111
+ const nextVersionMatch = nextVersionRegex.exec(rest);
112
+ const section = nextVersionMatch ? rest.slice(0, nextVersionMatch.index) : rest;
113
+
114
+ const dateMatch = match[0].match(/\((\d{4}-\d{2}-\d{2})\)/);
115
+ const date = dateMatch ? dateMatch[1] : undefined;
116
+
117
+ const categoryRegex = /^###\s+([^\n]+)$/m;
118
+ const categoryMatch = section.match(categoryRegex);
119
+ let heading;
120
+ if (categoryMatch) {
121
+ heading = sanitizeText(categoryMatch[1]);
122
+ }
123
+
124
+ const bullets = [];
125
+ const bulletRegex = /^\*\s+([^\n]+)$/gm;
126
+ let bulletMatch;
127
+ while ((bulletMatch = bulletRegex.exec(section)) !== null) {
128
+ const sanitized = sanitizeText(bulletMatch[1]);
129
+ if (!bullets.includes(sanitized)) {
130
+ bullets.push(sanitized);
131
+ }
132
+ }
133
+
134
+ const highlight = bullets.length > 0 ? bullets[0] : undefined;
135
+
136
+ return {
137
+ date,
138
+ highlight,
139
+ heading,
140
+ bullets
141
+ };
142
+ }
143
+
144
+ function sanitizeText(text) {
145
+ return text
146
+ .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
147
+ .replace(/\s+/g, ' ')
148
+ .trim();
149
+ }
150
+
151
+ function escapeRegExp(string) {
152
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
153
+ }
154
+
155
+ function escapePipes(text) {
156
+ return text.replace(/\|/g, '\\|');
157
+ }
158
+
159
+ function formatDate(date) {
160
+ const year = date.getFullYear();
161
+ const month = String(date.getMonth() + 1).padStart(2, '0');
162
+ const day = String(date.getDate()).padStart(2, '0');
163
+ return `${year}-${month}-${day}`;
164
+ }
165
+
166
+ module.exports = {
167
+ updateReleaseNotes,
168
+ insertReleaseNotesEntry,
169
+ extractChangelogInfo,
170
+ sanitizeText,
171
+ escapePipes,
172
+ formatDate
173
+ };
174
+
175
+ if (require.main === module) {
176
+ try {
177
+ updateReleaseNotes();
178
+ } catch (error) {
179
+ console.error(`โŒ Failed to update release notes: ${error.message}`);
180
+ process.exit(1);
181
+ }
182
+ }
@@ -47,27 +47,23 @@ test('build script exists in package.json', async () => {
47
47
  assert(pkg.scripts['build:server'].includes('vite build'));
48
48
  });
49
49
 
50
- test('Hero component fetches version dynamically', async () => {
50
+ test('Hero component fetches npm downloads directly', async () => {
51
51
  const heroPath = path.join(process.cwd(), 'web', 'components', 'Hero.tsx');
52
52
  const heroContent = await readFile(heroPath, 'utf-8');
53
53
 
54
- // Verify no hardcoded version strings
55
- assert(!heroContent.includes('v1.3.12'), 'Hero should not have hardcoded v1.3.12');
56
- assert(!heroContent.includes('"1.3.12"'), 'Hero should not have hardcoded "1.3.12"');
57
-
58
- // Verify it fetches from /api/version
59
- assert(heroContent.includes('fetch("/api/version")'), 'Hero should fetch version from API');
54
+ // Verify it fetches npm downloads directly from npm API
55
+ assert(heroContent.includes('api.npmjs.org'), 'Hero should fetch downloads from npm API');
60
56
  assert(heroContent.includes('useState<string | null>(null)'), 'Hero should have version state');
61
57
  });
62
58
 
63
- test('Netlify deployment configuration exists', async () => {
59
+ test('Netlify deployment configuration exists for static export', async () => {
64
60
  const netlifyTomlPath = path.join(process.cwd(), 'web', 'netlify.toml');
65
61
  await access(netlifyTomlPath);
66
62
 
67
63
  const content = await readFile(netlifyTomlPath, 'utf-8');
68
64
  assert(content.includes('[build]'), 'netlify.toml should have build section');
69
65
  assert(content.includes('npm run build'), 'netlify.toml should have build command');
70
- assert(content.includes('@netlify/plugin-nextjs'), 'netlify.toml should use Next.js plugin');
66
+ assert(content.includes('publish = "out"'), 'netlify.toml should publish out directory for static export');
71
67
  });
72
68
 
73
69
  test('Web package.json does not include better-sqlite3', async () => {
@@ -78,14 +74,10 @@ test('Web package.json does not include better-sqlite3', async () => {
78
74
  assert(!pkg.dependencies['better-sqlite3'], 'Web should not depend on better-sqlite3 for Netlify compatibility');
79
75
  });
80
76
 
81
- test('API routes return static data for Netlify deployment', async () => {
82
- const summaryRoutePath = path.join(process.cwd(), 'web', 'app', 'api', 'summary', 'route.ts');
83
- const historyRoutePath = path.join(process.cwd(), 'web', 'app', 'api', 'history', 'route.ts');
84
-
85
- const summaryContent = await readFile(summaryRoutePath, 'utf-8');
86
- const historyContent = await readFile(historyRoutePath, 'utf-8');
77
+ test('Next.js config uses static export for Netlify', async () => {
78
+ const nextConfigPath = path.join(process.cwd(), 'web', 'next.config.ts');
79
+ const content = await readFile(nextConfigPath, 'utf-8');
87
80
 
88
- // Verify no database imports
89
- assert(!summaryContent.includes('from "@/lib/db"'), 'Summary route should not import from db');
90
- assert(!historyContent.includes('from "@/lib/db"'), 'History route should not import from db');
81
+ assert(content.includes("output: 'export'"), 'next.config.ts should use static export');
82
+ assert(content.includes('unoptimized: true'), 'next.config.ts should disable image optimization for static export');
91
83
  });
@@ -0,0 +1,7 @@
1
+ import { test } from 'node:test'
2
+ import { strict as assert } from 'node:assert'
3
+
4
+ test('patch release preparation', () => {
5
+ // Verify we can prepare a patch release
6
+ assert.ok(true, 'Patch release 1.5.3 preparation test')
7
+ })
@@ -10,15 +10,14 @@ export default function Hero() {
10
10
  const [version, setVersion] = useState<string | null>(null);
11
11
 
12
12
  useEffect(() => {
13
- fetch("/api/npm-downloads")
13
+ // Fetch npm downloads directly from npm API
14
+ fetch("https://api.npmjs.org/downloads/point/last-month/@programinglive/dev-workflow-mcp-server")
14
15
  .then(res => res.json())
15
- .then(data => setDownloads(data.downloads))
16
+ .then(data => setDownloads(data.downloads || 0))
16
17
  .catch(() => setDownloads(null));
17
18
 
18
- fetch("/api/version")
19
- .then(res => res.json())
20
- .then(data => setVersion(data.version))
21
- .catch(() => setVersion(null));
19
+ // Version is hardcoded for static export
20
+ setVersion("1.5.1");
22
21
  }, []);
23
22
 
24
23
  const formatDownloads = (num: number) => {
package/web/netlify.toml CHANGED
@@ -1,6 +1,3 @@
1
1
  [build]
2
2
  command = "npm run build"
3
- publish = ".next"
4
-
5
- [[plugins]]
6
- package = "@netlify/plugin-nextjs"
3
+ publish = "out"
@@ -1,7 +1,12 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
3
  const nextConfig: NextConfig = {
4
- // Netlify handles the build output automatically
4
+ // Static export for Netlify deployment
5
+ output: 'export',
6
+ // Disable image optimization for static export
7
+ images: {
8
+ unoptimized: true,
9
+ },
5
10
  };
6
11
 
7
12
  export default nextConfig;
@@ -1,32 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { resolveDocContent, getAvailableDocs } from "@/lib/docs";
3
-
4
- export async function GET(request: NextRequest) {
5
- const searchParams = request.nextUrl.searchParams;
6
- const rawDocKey = searchParams.get("doc");
7
- const availableDocs = getAvailableDocs();
8
-
9
- if (!rawDocKey) {
10
- return NextResponse.json({ docs: availableDocs });
11
- }
12
-
13
- const docKey = rawDocKey.trim();
14
- const resolvedKey = availableDocs.find((key) => key.toLowerCase() === docKey.toLowerCase());
15
-
16
- if (!resolvedKey) {
17
- return NextResponse.json(
18
- { error: "Document not found", requested: docKey, available: availableDocs },
19
- { status: 404 }
20
- );
21
- }
22
-
23
- const result = await resolveDocContent(resolvedKey);
24
- if (!result.exists) {
25
- return NextResponse.json(
26
- { error: "Document asset is missing" },
27
- { status: 500 }
28
- );
29
- }
30
-
31
- return NextResponse.json({ doc: resolvedKey, content: result.content, format: result.format });
32
- }
@@ -1,12 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- export async function GET() {
4
- // Return placeholder data - database integration not available in static deployment
5
- return NextResponse.json({
6
- data: [],
7
- total: 0,
8
- page: 1,
9
- pageSize: 20,
10
- totalPages: 1
11
- });
12
- }
@@ -1,21 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- export async function GET() {
4
- try {
5
- const packageName = "@programinglive/dev-workflow-mcp-server";
6
- const response = await fetch(
7
- `https://api.npmjs.org/downloads/point/last-month/${packageName}`,
8
- { next: { revalidate: 3600 } } // Cache for 1 hour
9
- );
10
-
11
- if (!response.ok) {
12
- return NextResponse.json({ downloads: 0 }, { status: 200 });
13
- }
14
-
15
- const data = await response.json();
16
- return NextResponse.json({ downloads: data.downloads || 0 });
17
- } catch (error) {
18
- console.error("Error fetching npm downloads:", error);
19
- return NextResponse.json({ downloads: 0 }, { status: 200 });
20
- }
21
- }
@@ -1,13 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- export async function GET() {
4
- // Return placeholder data - database integration not available in static deployment
5
- const summary = {
6
- totalTasks: 0,
7
- taskTypes: {},
8
- lastActive: null,
9
- recentTasks: [],
10
- updatedAt: null,
11
- };
12
- return NextResponse.json({ summary });
13
- }
@@ -1,8 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- // Version is updated during build/release process
4
- const PACKAGE_VERSION = "1.5.1";
5
-
6
- export async function GET() {
7
- return NextResponse.json({ version: PACKAGE_VERSION });
8
- }