@programinglive/dev-workflow-mcp-server 1.5.0 → 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.
- package/.agent/workflows/dev-workflow-bugfix.md +250 -250
- package/.agent/workflows/dev-workflow-feature.md +201 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
- package/.agent/workflows/dev-workflow-refactor.md +238 -238
- package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
- package/.github/pull_request_template.md +22 -22
- package/.github/workflows/npm-publish.yml +50 -47
- package/.husky/commit-msg +0 -0
- package/CHANGELOG.md +750 -491
- package/CODE_OF_CONDUCT.md +79 -79
- package/CONTRIBUTING.md +60 -60
- package/LICENSE +21 -21
- package/README.md +632 -609
- package/SECURITY.md +20 -20
- package/db/index.js +204 -204
- package/docs/NETLIFY_DEPLOYMENT.md +125 -0
- package/docs/PLESK_DEPLOYMENT.md +170 -0
- package/docs/WORKFLOW-BEST-PRACTICES.md +58 -0
- package/docs/antigravity/EXAMPLES.md +535 -535
- package/docs/antigravity/GETTING-STARTED.md +292 -292
- package/docs/antigravity/TROUBLESHOOTING.md +742 -742
- package/docs/antigravity/WALKTHROUGH.md +25 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
- package/docs/product/PRD.md +67 -64
- package/docs/release-notes/RELEASE_NOTES.md +226 -132
- package/docs/web-dashboard.md +175 -163
- package/exec.js +49 -49
- package/index.js +279 -279
- package/install.js +73 -73
- package/package.json +11 -9
- package/release-wrapper.js +192 -157
- package/scripts/build-docs.js +63 -63
- package/scripts/hello-world.js +1 -1
- package/scripts/release.cjs +252 -252
- package/scripts/update-release-notes.cjs +178 -182
- package/server.json +23 -0
- package/tailwind.config.js +8 -10
- package/tests/build.test.js +83 -48
- package/tests/exec.test.js +35 -35
- package/tests/handlers.test.js +1058 -980
- package/tests/hello-world.test.js +13 -13
- package/tests/mcpRegistry.test.js +65 -0
- package/tests/release-wrapper.test.js +107 -105
- package/tests/workflowState.test.js +505 -502
- package/tools/definitions.js +332 -332
- package/tools/handlers.js +1019 -1019
- package/vite.config.js +43 -43
- package/vite.web.config.js +23 -0
- package/web/README.md +36 -0
- package/web/app/docs/page.tsx +202 -0
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +91 -0
- package/web/app/layout.tsx +36 -0
- package/web/app/page.tsx +15 -0
- package/web/components/ApiReference.tsx +20 -0
- package/web/components/Features.tsx +61 -0
- package/web/components/Footer.tsx +9 -0
- package/web/components/Hero.tsx +96 -0
- package/web/components/History.tsx +143 -0
- package/web/components/Navbar.tsx +64 -0
- package/web/components/Terminal.tsx +114 -0
- package/web/eslint.config.mjs +18 -0
- package/web/lib/db.js +131 -0
- package/web/lib/docs.js +73 -0
- package/web/netlify.toml +3 -0
- package/web/next.config.ts +12 -0
- package/web/package-lock.json +6560 -0
- package/web/package.json +28 -0
- package/web/postcss.config.mjs +9 -0
- package/web/public/assets/android-chrome-192x192.png +0 -0
- package/web/public/assets/android-chrome-512x512.png +0 -0
- package/web/public/assets/apple-touch-icon.png +0 -0
- package/web/public/assets/favicon-16x16.png +0 -0
- package/web/public/assets/favicon-32x32.png +0 -0
- package/web/public/assets/favicon.ico +0 -0
- package/web/public/assets/favicon.png +0 -0
- package/web/public/assets/logo.png +0 -0
- package/web/public/assets/og-preview.png +0 -0
- package/web/public/assets/site.webmanifest +1 -0
- package/web/public/file.svg +1 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/server.js +31 -159
- package/web/tailwind.config.ts +19 -0
- package/web/tsconfig.json +42 -0
- package/workflow-state.js +612 -626
- package/dist/docs/EXAMPLES.html +0 -411
- package/dist/docs/GETTING-STARTED.html +0 -233
- package/dist/docs/README.html +0 -535
- package/dist/docs/RELEASE_NOTES.html +0 -167
- package/dist/docs/TROUBLESHOOTING.html +0 -542
- package/dist/docs/WORKFLOW-TEMPLATES.html +0 -591
- package/dist/docs/web-dashboard.html +0 -158
- package/web/public/index.html +0 -1046
- package/web/public/input.css +0 -3
- package/web/public/output.css +0 -1
|
@@ -1,182 +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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sectionLines.push(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
console.error(`❌ Failed to update release notes: ${error.message}`);
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
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
|
+
|
|
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
ADDED
|
@@ -0,0 +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
|
+
}
|
package/tailwind.config.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
/** @type {import('tailwindcss').Config} */
|
|
2
|
-
|
|
3
|
-
content: [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
plugins: [],
|
|
10
|
-
}
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: ["./web/public/**/*.{html,js}"],
|
|
4
|
+
theme: {
|
|
5
|
+
extend: {},
|
|
6
|
+
},
|
|
7
|
+
plugins: [],
|
|
8
|
+
}
|
package/tests/build.test.js
CHANGED
|
@@ -1,48 +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');
|
|
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
|
|
47
|
-
assert(pkg.scripts
|
|
48
|
-
});
|
|
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
|
+
});
|
package/tests/exec.test.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import { exec } from '../exec.js';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { mkdtemp, rm } from 'fs/promises';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
|
|
8
|
-
test('exec runs commands successfully', async () => {
|
|
9
|
-
const echoCommand = process.platform === 'win32' ? 'echo hello world' : 'echo "hello world"';
|
|
10
|
-
const { stdout } = await exec(echoCommand);
|
|
11
|
-
const normalized = stdout.trim().replace(/^"(.*)"$/, '$1');
|
|
12
|
-
assert.strictEqual(normalized, 'hello world');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('exec respects explicit cwd option', async () => {
|
|
16
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const dirCommand = process.platform === 'win32' ? 'cd' : 'pwd';
|
|
20
|
-
const { stdout } = await exec(dirCommand, { cwd: tempDir });
|
|
21
|
-
// Just verify it runs in the specified directory (path may vary by platform)
|
|
22
|
-
assert(stdout.trim().toLowerCase().includes('mcp-test-'));
|
|
23
|
-
} finally {
|
|
24
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('exec handles command errors', async () => {
|
|
29
|
-
try {
|
|
30
|
-
await exec('command-that-does-not-exist');
|
|
31
|
-
assert.fail('Should have thrown an error');
|
|
32
|
-
} catch (error) {
|
|
33
|
-
assert(error.code !== undefined || error.message.includes('command-not-found'));
|
|
34
|
-
}
|
|
35
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { exec } from '../exec.js';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
test('exec runs commands successfully', async () => {
|
|
9
|
+
const echoCommand = process.platform === 'win32' ? 'echo hello world' : 'echo "hello world"';
|
|
10
|
+
const { stdout } = await exec(echoCommand);
|
|
11
|
+
const normalized = stdout.trim().replace(/^"(.*)"$/, '$1');
|
|
12
|
+
assert.strictEqual(normalized, 'hello world');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('exec respects explicit cwd option', async () => {
|
|
16
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const dirCommand = process.platform === 'win32' ? 'cd' : 'pwd';
|
|
20
|
+
const { stdout } = await exec(dirCommand, { cwd: tempDir });
|
|
21
|
+
// Just verify it runs in the specified directory (path may vary by platform)
|
|
22
|
+
assert(stdout.trim().toLowerCase().includes('mcp-test-'));
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('exec handles command errors', async () => {
|
|
29
|
+
try {
|
|
30
|
+
await exec('command-that-does-not-exist');
|
|
31
|
+
assert.fail('Should have thrown an error');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
assert(error.code !== undefined || error.message.includes('command-not-found'));
|
|
34
|
+
}
|
|
35
|
+
});
|