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

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/npm-publish.yml +50 -50
  9. package/.husky/commit-msg +0 -0
  10. package/CHANGELOG.md +749 -739
  11. package/CODE_OF_CONDUCT.md +79 -79
  12. package/CONTRIBUTING.md +60 -60
  13. package/LICENSE +21 -21
  14. package/README.md +632 -632
  15. package/SECURITY.md +20 -20
  16. package/db/index.js +204 -204
  17. package/docs/NETLIFY_DEPLOYMENT.md +125 -125
  18. package/docs/PLESK_DEPLOYMENT.md +170 -170
  19. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
  20. package/docs/antigravity/EXAMPLES.md +535 -535
  21. package/docs/antigravity/GETTING-STARTED.md +292 -292
  22. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  23. package/docs/antigravity/WALKTHROUGH.md +25 -25
  24. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  25. package/docs/product/PRD.md +67 -67
  26. package/docs/release-notes/RELEASE_NOTES.md +226 -226
  27. package/docs/web-dashboard.md +175 -175
  28. package/exec.js +49 -49
  29. package/index.js +279 -279
  30. package/install.js +73 -73
  31. package/package.json +4 -4
  32. package/release-wrapper.js +192 -192
  33. package/scripts/build-docs.js +63 -63
  34. package/scripts/hello-world.js +1 -1
  35. package/scripts/release.cjs +252 -252
  36. package/scripts/update-release-notes.cjs +178 -178
  37. package/server.json +23 -23
  38. package/tailwind.config.js +8 -8
  39. package/tests/build.test.js +83 -91
  40. package/tests/exec.test.js +35 -35
  41. package/tests/handlers.test.js +1058 -1058
  42. package/tests/hello-world.test.js +13 -13
  43. package/tests/mcpRegistry.test.js +65 -65
  44. package/tests/release-wrapper.test.js +107 -107
  45. package/tests/workflowState.test.js +505 -505
  46. package/tools/definitions.js +332 -332
  47. package/tools/handlers.js +1019 -1019
  48. package/vite.config.js +43 -43
  49. package/vite.web.config.js +23 -23
  50. package/web/README.md +36 -36
  51. package/web/app/docs/page.tsx +202 -202
  52. package/web/app/globals.css +90 -90
  53. package/web/app/layout.tsx +36 -36
  54. package/web/app/page.tsx +15 -15
  55. package/web/components/ApiReference.tsx +20 -20
  56. package/web/components/Features.tsx +61 -61
  57. package/web/components/Footer.tsx +9 -9
  58. package/web/components/Hero.tsx +96 -97
  59. package/web/components/History.tsx +143 -143
  60. package/web/components/Navbar.tsx +64 -64
  61. package/web/components/Terminal.tsx +114 -114
  62. package/web/eslint.config.mjs +18 -18
  63. package/web/lib/db.js +131 -131
  64. package/web/lib/docs.js +73 -73
  65. package/web/netlify.toml +3 -6
  66. package/web/next.config.ts +12 -7
  67. package/web/package-lock.json +6560 -6560
  68. package/web/package.json +28 -28
  69. package/web/postcss.config.mjs +9 -9
  70. package/web/server.js +31 -31
  71. package/web/tailwind.config.ts +19 -19
  72. package/web/tsconfig.json +42 -42
  73. package/workflow-state.js +612 -612
  74. package/web/app/api/docs/route.ts +0 -32
  75. package/web/app/api/history/route.ts +0 -12
  76. package/web/app/api/npm-downloads/route.ts +0 -21
  77. package/web/app/api/summary/route.ts +0 -13
  78. package/web/app/api/version/route.ts +0 -8
@@ -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
+ }
package/server.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
3
- "name": "io.github.programinglive/dev-workflow-mcp-server",
4
- "description": "MCP server to enforce development workflow discipline",
5
- "title": "Dev Workflow MCP Server",
6
- "websiteUrl": "https://devworkflow.programinglive.com/",
7
- "repository": {
8
- "url": "https://github.com/programinglive/dev-workflow-mcp-server",
9
- "source": "github"
10
- },
11
- "version": "1.4.6",
12
- "packages": [
13
- {
14
- "registryType": "npm",
15
- "registryBaseUrl": "https://registry.npmjs.org",
16
- "identifier": "@programinglive/dev-workflow-mcp-server",
17
- "version": "1.4.6",
18
- "transport": {
19
- "type": "stdio"
20
- }
21
- }
22
- ]
23
- }
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
3
+ "name": "io.github.programinglive/dev-workflow-mcp-server",
4
+ "description": "MCP server to enforce development workflow discipline",
5
+ "title": "Dev Workflow MCP Server",
6
+ "websiteUrl": "https://devworkflow.programinglive.com/",
7
+ "repository": {
8
+ "url": "https://github.com/programinglive/dev-workflow-mcp-server",
9
+ "source": "github"
10
+ },
11
+ "version": "1.4.6",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "@programinglive/dev-workflow-mcp-server",
17
+ "version": "1.4.6",
18
+ "transport": {
19
+ "type": "stdio"
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -1,8 +1,8 @@
1
- /** @type {import('tailwindcss').Config} */
2
- module.exports = {
3
- content: ["./web/public/**/*.{html,js}"],
4
- theme: {
5
- extend: {},
6
- },
7
- plugins: [],
8
- }
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ["./web/public/**/*.{html,js}"],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ }
@@ -1,91 +1,83 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert';
3
- import { readFile, access } from 'fs/promises';
4
- import { exec as execCallback } from 'child_process';
5
- import { promisify } from 'util';
6
- import path from 'path';
7
-
8
- const exec = promisify(execCallback);
9
-
10
- test('build generates dist/index.mjs file', async () => {
11
- const { stderr } = await exec('npm run build:server && npm run postbuild');
12
- // Allow the known vite warning about node:process
13
- const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
- const tailwindOutput = '≈ tailwindcss';
15
- const browserslistWarning = 'Browserslist: caniuse-lite is outdated';
16
- const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
17
- assert(
18
- actualStderr === '' || actualStderr.includes(knownWarning) || actualStderr.includes(tailwindOutput) || actualStderr.includes(browserslistWarning),
19
- `Build should not have unexpected errors: ${actualStderr}`
20
- );
21
-
22
- // Verify the file exists
23
- const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
24
- await access(distPath);
25
-
26
- // Verify it contains expected content
27
- const content = await readFile(distPath, 'utf-8');
28
- assert(content.includes('main'), 'Bundle should contain main function');
29
- assert(content.length > 1000, 'Bundle should be substantial');
30
-
31
- const docDir = path.join(process.cwd(), 'dist', 'docs');
32
- const docFiles = ['README.html', 'web-dashboard.html'];
33
- for (const file of docFiles) {
34
- const fullPath = path.join(docDir, file);
35
- await access(fullPath);
36
- const docContent = await readFile(fullPath, 'utf-8');
37
- assert(docContent.includes('<h1'), `${file} should contain HTML output`);
38
- }
39
- });
40
-
41
- test('build script exists in package.json', async () => {
42
- const packagePath = path.join(process.cwd(), 'package.json');
43
- const content = await readFile(packagePath, 'utf-8');
44
- const pkg = JSON.parse(content);
45
-
46
- assert.strictEqual(typeof pkg.scripts['build:server'], 'string');
47
- assert(pkg.scripts['build:server'].includes('vite build'));
48
- });
49
-
50
- test('Hero component fetches version dynamically', async () => {
51
- const heroPath = path.join(process.cwd(), 'web', 'components', 'Hero.tsx');
52
- const heroContent = await readFile(heroPath, 'utf-8');
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');
60
- assert(heroContent.includes('useState<string | null>(null)'), 'Hero should have version state');
61
- });
62
-
63
- test('Netlify deployment configuration exists', async () => {
64
- const netlifyTomlPath = path.join(process.cwd(), 'web', 'netlify.toml');
65
- await access(netlifyTomlPath);
66
-
67
- const content = await readFile(netlifyTomlPath, 'utf-8');
68
- assert(content.includes('[build]'), 'netlify.toml should have build section');
69
- 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');
71
- });
72
-
73
- test('Web package.json does not include better-sqlite3', async () => {
74
- const webPackagePath = path.join(process.cwd(), 'web', 'package.json');
75
- const content = await readFile(webPackagePath, 'utf-8');
76
- const pkg = JSON.parse(content);
77
-
78
- assert(!pkg.dependencies['better-sqlite3'], 'Web should not depend on better-sqlite3 for Netlify compatibility');
79
- });
80
-
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');
87
-
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');
91
- });
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { readFile, access } from 'fs/promises';
4
+ import { exec as execCallback } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import path from 'path';
7
+
8
+ const exec = promisify(execCallback);
9
+
10
+ test('build generates dist/index.mjs file', async () => {
11
+ const { stderr } = await exec('npm run build:server && npm run postbuild');
12
+ // Allow the known vite warning about node:process
13
+ const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
+ const tailwindOutput = '≈ tailwindcss';
15
+ const browserslistWarning = 'Browserslist: caniuse-lite is outdated';
16
+ const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
17
+ assert(
18
+ actualStderr === '' || actualStderr.includes(knownWarning) || actualStderr.includes(tailwindOutput) || actualStderr.includes(browserslistWarning),
19
+ `Build should not have unexpected errors: ${actualStderr}`
20
+ );
21
+
22
+ // Verify the file exists
23
+ const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
24
+ await access(distPath);
25
+
26
+ // Verify it contains expected content
27
+ const content = await readFile(distPath, 'utf-8');
28
+ assert(content.includes('main'), 'Bundle should contain main function');
29
+ assert(content.length > 1000, 'Bundle should be substantial');
30
+
31
+ const docDir = path.join(process.cwd(), 'dist', 'docs');
32
+ const docFiles = ['README.html', 'web-dashboard.html'];
33
+ for (const file of docFiles) {
34
+ const fullPath = path.join(docDir, file);
35
+ await access(fullPath);
36
+ const docContent = await readFile(fullPath, 'utf-8');
37
+ assert(docContent.includes('<h1'), `${file} should contain HTML output`);
38
+ }
39
+ });
40
+
41
+ test('build script exists in package.json', async () => {
42
+ const packagePath = path.join(process.cwd(), 'package.json');
43
+ const content = await readFile(packagePath, 'utf-8');
44
+ const pkg = JSON.parse(content);
45
+
46
+ assert.strictEqual(typeof pkg.scripts['build:server'], 'string');
47
+ assert(pkg.scripts['build:server'].includes('vite build'));
48
+ });
49
+
50
+ test('Hero component fetches npm downloads directly', async () => {
51
+ const heroPath = path.join(process.cwd(), 'web', 'components', 'Hero.tsx');
52
+ const heroContent = await readFile(heroPath, 'utf-8');
53
+
54
+ // Verify it fetches npm downloads directly from npm API
55
+ assert(heroContent.includes('api.npmjs.org'), 'Hero should fetch downloads from npm API');
56
+ assert(heroContent.includes('useState<string | null>(null)'), 'Hero should have version state');
57
+ });
58
+
59
+ test('Netlify deployment configuration exists for static export', async () => {
60
+ const netlifyTomlPath = path.join(process.cwd(), 'web', 'netlify.toml');
61
+ await access(netlifyTomlPath);
62
+
63
+ const content = await readFile(netlifyTomlPath, 'utf-8');
64
+ assert(content.includes('[build]'), 'netlify.toml should have build section');
65
+ assert(content.includes('npm run build'), 'netlify.toml should have build command');
66
+ assert(content.includes('publish = "out"'), 'netlify.toml should publish out directory for static export');
67
+ });
68
+
69
+ test('Web package.json does not include better-sqlite3', async () => {
70
+ const webPackagePath = path.join(process.cwd(), 'web', 'package.json');
71
+ const content = await readFile(webPackagePath, 'utf-8');
72
+ const pkg = JSON.parse(content);
73
+
74
+ assert(!pkg.dependencies['better-sqlite3'], 'Web should not depend on better-sqlite3 for Netlify compatibility');
75
+ });
76
+
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');
80
+
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');
83
+ });