@programinglive/dev-workflow-mcp-server 1.5.0 → 1.5.1
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/.github/workflows/npm-publish.yml +3 -0
- package/CHANGELOG.md +252 -3
- package/README.md +24 -1
- package/docs/NETLIFY_DEPLOYMENT.md +125 -0
- package/docs/PLESK_DEPLOYMENT.md +170 -0
- package/docs/WORKFLOW-BEST-PRACTICES.md +58 -0
- package/docs/product/PRD.md +5 -2
- package/docs/release-notes/RELEASE_NOTES.md +94 -0
- package/docs/web-dashboard.md +12 -0
- package/package.json +8 -6
- package/release-wrapper.js +68 -33
- package/scripts/update-release-notes.cjs +14 -18
- package/server.json +23 -0
- package/tailwind.config.js +2 -4
- package/tests/build.test.js +46 -3
- package/tests/handlers.test.js +78 -0
- package/tests/mcpRegistry.test.js +65 -0
- package/tests/release-wrapper.test.js +2 -0
- package/tests/workflowState.test.js +7 -4
- package/tools/definitions.js +2 -2
- package/tools/handlers.js +2 -2
- package/vite.web.config.js +23 -0
- package/web/README.md +36 -0
- package/web/app/api/docs/route.ts +32 -0
- package/web/app/api/history/route.ts +12 -0
- package/web/app/api/npm-downloads/route.ts +21 -0
- package/web/app/api/summary/route.ts +13 -0
- package/web/app/api/version/route.ts +8 -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 +97 -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 +6 -0
- package/web/next.config.ts +7 -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 +30 -158
- package/web/tailwind.config.ts +19 -0
- package/web/tsconfig.json +42 -0
- package/workflow-state.js +11 -25
- 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
package/docs/product/PRD.md
CHANGED
|
@@ -35,6 +35,7 @@ Developers need structured guidance to maintain disciplined workflows across pro
|
|
|
35
35
|
- Offer aggregated project summaries (JSON artifacts, SQLite sync, optional web dashboard).
|
|
36
36
|
- Guard release commands to ensure prerequisites are satisfied.
|
|
37
37
|
- Support `run_full_workflow` for scripted execution of all steps with validation.
|
|
38
|
+
- Web dashboard displays version dynamically from `package.json` via `/api/version` endpoint to stay in sync after releases.
|
|
38
39
|
|
|
39
40
|
## 8. Technical Considerations
|
|
40
41
|
- Node.js (ESM) codebase leveraging `@modelcontextprotocol/sdk`.
|
|
@@ -42,6 +43,7 @@ Developers need structured guidance to maintain disciplined workflows across pro
|
|
|
42
43
|
- Optional SQLite integration for history and summary caching.
|
|
43
44
|
- Vite build pipeline for distribution bundle; source usage recommended for MCP.
|
|
44
45
|
- Windows compatibility requirements (Python, Build Tools for native modules).
|
|
46
|
+
- **Deployment options**: Local development, Netlify (static), Plesk (full-featured).
|
|
45
47
|
|
|
46
48
|
## 9. Success Metrics
|
|
47
49
|
- ≥90% of workflow operations executed via MCP tools (vs. manual overrides).
|
|
@@ -50,8 +52,9 @@ Developers need structured guidance to maintain disciplined workflows across pro
|
|
|
50
52
|
|
|
51
53
|
## 10. Milestones
|
|
52
54
|
1. **MVP**: Core workflow tools, state persistence, basic history (complete).
|
|
53
|
-
2. **Enhancements**: Improved project root detection, compatibility mirrors, `run_full_workflow` automation (
|
|
54
|
-
3. **
|
|
55
|
+
2. **Enhancements**: Improved project root detection, compatibility mirrors, `run_full_workflow` automation (complete).
|
|
56
|
+
3. **v1.5.0**: Netlify deployment support for static web dashboard (complete).
|
|
57
|
+
4. **Future**: Expanded dashboard analytics, customizable workflows, richer integrations.
|
|
55
58
|
|
|
56
59
|
## 11. Risks & Mitigations
|
|
57
60
|
- **Risk**: Workflow feels overly prescriptive. **Mitigation**: Provide escape hatches (`force_complete_task`, `drop_task`) with audit trails.
|
|
@@ -4,6 +4,100 @@ Authoritative history of user-visible changes for the Development Workflow MCP S
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.5.0 — 2025-12-02
|
|
8
|
+
- See CHANGELOG for details.
|
|
9
|
+
|
|
10
|
+
## 1.4.11 — 2025-12-01 — 🐛 Bug Fixes
|
|
11
|
+
- move vite to dependencies to ensure build works on production (62fc4c0)
|
|
12
|
+
- resolve sentry permission issues (c739f8e)
|
|
13
|
+
|
|
14
|
+
## 1.4.10 — 2025-12-01 — ✨ Features
|
|
15
|
+
- **dashboard:** implement random data generation for analytics chart (17e2073)
|
|
16
|
+
- move dompdf to require and resolve debugbar production error (e5d1cb6)
|
|
17
|
+
|
|
18
|
+
## 1.4.9 — 2025-11-30 — ♻️ Refactors
|
|
19
|
+
- **dashboard:** combine analytics charts and use local Chart.js (86ed3f4)
|
|
20
|
+
|
|
21
|
+
## 1.4.8 — 2025-11-30 — 🐛 Bug Fixes
|
|
22
|
+
- **dashboard:** improve responsive layout for admin stats cards (33d5b39)
|
|
23
|
+
- **dashboard:** improve responsive layout for admin stats cards (530400d)
|
|
24
|
+
|
|
25
|
+
## 1.4.7 — 2025-11-29 — ✨ Features
|
|
26
|
+
- add core doc types (PRD/README/RELEASE_NOTES) and fix CI build (8ed0567)
|
|
27
|
+
- update hero text and meta description to emphasize Indonesian programmer community (681547b)
|
|
28
|
+
- display dynamic version in web dashboard from /api/version (69f561d)
|
|
29
|
+
- add 1.4.3 release notes (8b165f7)
|
|
30
|
+
- add MCP registry metadata (mcpName & server.json) (66caea1)
|
|
31
|
+
- align MCP registry schema and docs (e4e732d)
|
|
32
|
+
- **release:** 1.4.4 🚀 (05a968f)
|
|
33
|
+
- **release:** 1.4.5 🚀 (7e823b9)
|
|
34
|
+
- **release:** 1.4.6 🚀 (7e0b672)
|
|
35
|
+
|
|
36
|
+
## 1.4.6 — 2025-11-27 — 🧹 Chores
|
|
37
|
+
- add MCP registry metadata (mcpName & server.json) (66caea1)
|
|
38
|
+
|
|
39
|
+
## 1.4.5 — 2025-11-27 — 🐛 Bug Fixes
|
|
40
|
+
- display dynamic version in web dashboard from /api/version (69f561d)
|
|
41
|
+
|
|
42
|
+
## 1.4.4 — 2025-11-27 — ♻️ Refactors
|
|
43
|
+
- consolidate workflow state to root .state folder only (3d69a14)
|
|
44
|
+
- ensure release guard uses freshest workflow state (e07eb42)
|
|
45
|
+
- remove invalid turbopack experimental config from next.config.ts (af0a7ea)
|
|
46
|
+
- **web:** configure next.js root to silence warnings (22ed002)
|
|
47
|
+
- **web:** improve mobile layout for hero section (53b26bf)
|
|
48
|
+
- add Plesk deployment guide (de35675)
|
|
49
|
+
- add 1.4.3 release notes (8b165f7)
|
|
50
|
+
- fix release state detection (ed5dfd1)
|
|
51
|
+
- prepare for release (a6ea2d0)
|
|
52
|
+
- **release:** 1.4.2 (fac8999)
|
|
53
|
+
- **release:** 1.4.2 🚀 (01b0b3c)
|
|
54
|
+
- **release:** 1.4.3 🚀 (04d8c3b)
|
|
55
|
+
- update task list (35663dd)
|
|
56
|
+
- **web:** fix next.js lockfile warning (05ffce0)
|
|
57
|
+
- add core doc types (PRD/README/RELEASE_NOTES) and fix CI build (8ed0567)
|
|
58
|
+
- update hero text and meta description to emphasize Indonesian programmer community (681547b)
|
|
59
|
+
- update program tracks and fix fast refresh warning (aa5bc9c)
|
|
60
|
+
|
|
61
|
+
## 1.4.3 — 2025-11-27 — 🧹 Chores
|
|
62
|
+
- **release:** 1.4.2 (fac8999)
|
|
63
|
+
- remove invalid turbopack experimental config from next.config.ts (af0a7ea)
|
|
64
|
+
- add Plesk deployment guide (de35675)
|
|
65
|
+
- update program tracks and fix fast refresh warning (aa5bc9c)
|
|
66
|
+
|
|
67
|
+
## 1.4.2 — 2025-11-27 — 🧹 Chores
|
|
68
|
+
- fix release state detection (ed5dfd1)
|
|
69
|
+
- prepare for release (a6ea2d0)
|
|
70
|
+
- update task list (35663dd)
|
|
71
|
+
- **web:** fix next.js lockfile warning (05ffce0)
|
|
72
|
+
- ensure release guard uses freshest workflow state (e07eb42)
|
|
73
|
+
- **web:** configure next.js root to silence warnings (22ed002)
|
|
74
|
+
- **web:** improve mobile layout for hero section (53b26bf)
|
|
75
|
+
- consolidate workflow state to root .state folder only (3d69a14)
|
|
76
|
+
|
|
77
|
+
## 1.4.1 — 2025-11-27 — 🐛 Bug Fixes
|
|
78
|
+
- update build test to include postbuild (b1e2f48)
|
|
79
|
+
- add custom server.js for Plesk (b3912cd)
|
|
80
|
+
- migrate dashboard to Next.js with TypeScript and Tailwind CSS (bce1fec)
|
|
81
|
+
- fix package.json scripts (fb25e98)
|
|
82
|
+
- fix tsconfig and remove dist artifact (282759f)
|
|
83
|
+
|
|
84
|
+
## 1.4.0 — 2025-11-26
|
|
85
|
+
- See CHANGELOG for details.
|
|
86
|
+
|
|
87
|
+
## 1.3.11 — 2025-11-23
|
|
88
|
+
- Re-applied Plesk security fix (moved static files to `web/public/`).
|
|
89
|
+
- Updated build configuration and server paths.
|
|
90
|
+
|
|
91
|
+
## 1.3.10 — 2025-11-23
|
|
92
|
+
- Fixed workflow state synchronization issue that caused forced releases.
|
|
93
|
+
- Added `docs/WORKFLOW-BEST-PRACTICES.md`.
|
|
94
|
+
|
|
95
|
+
## 1.3.9 — 2025-11-23
|
|
96
|
+
- Improved documentation modal layout and readability.
|
|
97
|
+
|
|
98
|
+
## 1.3.8 — 2025-11-23
|
|
99
|
+
- Fixed documentation loading in production by tracking `dist/docs` in git.
|
|
100
|
+
|
|
7
101
|
## 1.3.0 — 2025-11-22
|
|
8
102
|
- Added comprehensive Antigravity integration documentation and examples.
|
|
9
103
|
- Fixed Windows shell escaping for commit messages.
|
package/docs/web-dashboard.md
CHANGED
|
@@ -156,8 +156,20 @@ Returns an array of task counts grouped by period.
|
|
|
156
156
|
| Dashboard shows empty data | Confirm the MCP server has generated `.state/users/<user>/workflow-state.json` and that the chosen `user` matches. |
|
|
157
157
|
| Port already in use | Set `DEV_WORKFLOW_WEB_PORT` to a free port before running `npm run web`. |
|
|
158
158
|
|
|
159
|
+
## Deployment Options
|
|
160
|
+
|
|
161
|
+
The web dashboard can be deployed in multiple ways:
|
|
162
|
+
|
|
163
|
+
| Platform | Database Support | Best For |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| Local (`npm run web`) | Full SQLite | Development and personal use |
|
|
166
|
+
| [Netlify](./NETLIFY_DEPLOYMENT.md) | Static only | Public documentation site |
|
|
167
|
+
| [Plesk](./PLESK_DEPLOYMENT.md) | Full SQLite | Self-hosted production |
|
|
168
|
+
|
|
159
169
|
## Related Topics
|
|
160
170
|
|
|
161
171
|
- [Main README](../README.md)
|
|
172
|
+
- [Netlify Deployment](./NETLIFY_DEPLOYMENT.md)
|
|
173
|
+
- [Plesk Deployment](./PLESK_DEPLOYMENT.md)
|
|
162
174
|
- [Workflow State Internals](./state.md) *(coming soon)*
|
|
163
175
|
- [CLI Tools](../README.md#🛠️-available-tools)
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"homepage": "https://devworkflow.programinglive.com/",
|
|
6
|
+
"mcpName": "io.github.programinglive/dev-workflow-mcp-server",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "index.js",
|
|
8
9
|
"bin": {
|
|
9
10
|
"dev-workflow-mcp": "index.js"
|
|
10
11
|
},
|
|
11
12
|
"scripts": {
|
|
12
|
-
"build": "
|
|
13
|
-
"build:
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"build": "npm run build:server && npm run build:web",
|
|
14
|
+
"build:server": "vite build",
|
|
15
|
+
"build:web": "npm run build --prefix web",
|
|
16
|
+
"watch:web": "npm run dev --prefix web",
|
|
17
|
+
"postbuild": "node scripts/build-docs.js",
|
|
16
18
|
"dev": "node --watch index.js",
|
|
17
19
|
"start": "node index.js",
|
|
18
20
|
"local": "node index.js",
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
"release:major": "node release-wrapper.js major",
|
|
25
27
|
"release:minor": "node release-wrapper.js minor",
|
|
26
28
|
"release:patch": "node release-wrapper.js patch",
|
|
27
|
-
"web": "
|
|
29
|
+
"web": "npm start --prefix web"
|
|
28
30
|
},
|
|
29
31
|
"keywords": [
|
|
30
32
|
"mcp",
|
package/release-wrapper.js
CHANGED
|
@@ -3,9 +3,71 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { resolveStateFile } from './workflow-state.js';
|
|
6
7
|
|
|
7
8
|
const RELEASE_TYPES = new Set(['major', 'minor', 'patch']);
|
|
8
9
|
|
|
10
|
+
async function readStateCandidate(filePath) {
|
|
11
|
+
if (!filePath) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const [raw, stats] = await Promise.all([fs.readFile(filePath, 'utf8'), fs.stat(filePath)]);
|
|
17
|
+
return {
|
|
18
|
+
state: JSON.parse(raw),
|
|
19
|
+
path: filePath,
|
|
20
|
+
mtimeMs: stats.mtimeMs,
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadWorkflowStateCandidates(projectRoot) {
|
|
28
|
+
const candidates = [];
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
candidates.push(resolveStateFile());
|
|
32
|
+
} catch {
|
|
33
|
+
// resolver may throw before state exists; fall through to other candidates
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const legacyStatePath = path.join(projectRoot, '.state', 'workflow-state.json');
|
|
37
|
+
candidates.push(legacyStatePath);
|
|
38
|
+
|
|
39
|
+
// Add explicit DEV_WORKFLOW_STATE_FILE if provided (resolver would have used it, but keep explicit just in case)
|
|
40
|
+
if (process.env.DEV_WORKFLOW_STATE_FILE) {
|
|
41
|
+
candidates.push(path.resolve(process.env.DEV_WORKFLOW_STATE_FILE));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
let freshest = null;
|
|
46
|
+
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (!candidate) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resolved = path.resolve(candidate);
|
|
53
|
+
if (seen.has(resolved)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
seen.add(resolved);
|
|
57
|
+
|
|
58
|
+
const result = await readStateCandidate(resolved);
|
|
59
|
+
if (!result) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!freshest || result.mtimeMs > freshest.mtimeMs) {
|
|
64
|
+
freshest = result;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return freshest;
|
|
69
|
+
}
|
|
70
|
+
|
|
9
71
|
async function main() {
|
|
10
72
|
const releaseTypeArg = (process.argv[2] || '').toLowerCase();
|
|
11
73
|
const forceFlag = process.argv.includes('--force') || process.env.DEV_WORKFLOW_FORCE_RELEASE === '1';
|
|
@@ -26,44 +88,17 @@ async function main() {
|
|
|
26
88
|
}
|
|
27
89
|
const projectRoot = projectRootCandidate;
|
|
28
90
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const baseStateDir = path.join(projectRoot, '.state');
|
|
32
|
-
const legacyStatePath = path.join(baseStateDir, 'workflow-state.json');
|
|
33
|
-
let resolvedUserId = (process.env.DEV_WORKFLOW_USER_ID || '').trim();
|
|
34
|
-
if (!resolvedUserId) {
|
|
35
|
-
try {
|
|
36
|
-
const fileUserId = (await fs.readFile(path.join(baseStateDir, 'user-id'), 'utf8')).trim();
|
|
37
|
-
if (fileUserId) {
|
|
38
|
-
resolvedUserId = fileUserId;
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// no user-id file; legacy layout likely
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const userStatePath = resolvedUserId ? path.join(baseStateDir, 'users', resolvedUserId, 'workflow-state.json') : null;
|
|
45
|
-
|
|
46
|
-
let statePath = legacyStatePath;
|
|
47
|
-
if (userStatePath) {
|
|
48
|
-
try {
|
|
49
|
-
await fs.access(userStatePath);
|
|
50
|
-
statePath = userStatePath;
|
|
51
|
-
} catch {
|
|
52
|
-
// fall back to legacy path
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
let state;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const raw = await fs.readFile(statePath, 'utf8');
|
|
59
|
-
state = JSON.parse(raw);
|
|
60
|
-
} catch (error) {
|
|
91
|
+
const result = await loadWorkflowStateCandidates(projectRoot);
|
|
92
|
+
if (!result) {
|
|
61
93
|
console.error(
|
|
62
|
-
`❌ Release guard: cannot locate workflow state
|
|
94
|
+
`❌ Release guard: cannot locate workflow state under ${path.join(projectRoot, '.state')}.
|
|
95
|
+
Make sure the dev-workflow MCP is installed and you have started a task.`
|
|
63
96
|
);
|
|
64
97
|
process.exit(1);
|
|
65
98
|
}
|
|
66
99
|
|
|
100
|
+
const { state, path: statePath } = result;
|
|
101
|
+
|
|
67
102
|
const errors = [];
|
|
68
103
|
if (state.currentPhase !== 'release') {
|
|
69
104
|
errors.push(
|
|
@@ -67,33 +67,29 @@ function updateReleaseNotes({
|
|
|
67
67
|
|
|
68
68
|
function insertReleaseNotesEntry({ content, version, releaseDate, highlight, sectionHeading, detailBullets }) {
|
|
69
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
70
|
|
|
75
|
-
|
|
76
|
-
lines.
|
|
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
|
+
}
|
|
77
76
|
|
|
78
77
|
const sectionLines = [];
|
|
79
78
|
sectionLines.push('');
|
|
80
79
|
const trimmedHeading = typeof sectionHeading === 'string' ? sectionHeading.trim() : '';
|
|
81
|
-
const headingSuffix = trimmedHeading ? `
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
sectionLines.push(
|
|
85
|
-
|
|
80
|
+
const headingSuffix = trimmedHeading ? ` — ${trimmedHeading}` : '';
|
|
81
|
+
|
|
82
|
+
// Format: ## [Version] — [Date]
|
|
83
|
+
sectionLines.push(`## ${version} — ${releaseDate}${headingSuffix}`);
|
|
84
|
+
|
|
86
85
|
for (const bullet of detailBullets) {
|
|
87
86
|
sectionLines.push(`- ${bullet}`);
|
|
88
87
|
}
|
|
89
|
-
sectionLines.push('');
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
lines.splice(firstSectionIndex, 0, ...sectionLines);
|
|
96
|
-
}
|
|
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);
|
|
97
93
|
|
|
98
94
|
return lines.join('\n');
|
|
99
95
|
}
|
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
package/tests/build.test.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
const exec = promisify(execCallback);
|
|
9
9
|
|
|
10
10
|
test('build generates dist/index.mjs file', async () => {
|
|
11
|
-
const { stderr } = await exec('npm run build');
|
|
11
|
+
const { stderr } = await exec('npm run build:server && npm run postbuild');
|
|
12
12
|
// Allow the known vite warning about node:process
|
|
13
13
|
const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
|
|
14
14
|
const tailwindOutput = '≈ tailwindcss';
|
|
@@ -43,6 +43,49 @@ test('build script exists in package.json', async () => {
|
|
|
43
43
|
const content = await readFile(packagePath, 'utf-8');
|
|
44
44
|
const pkg = JSON.parse(content);
|
|
45
45
|
|
|
46
|
-
assert.strictEqual(typeof pkg.scripts
|
|
47
|
-
assert(pkg.scripts
|
|
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');
|
|
48
91
|
});
|
package/tests/handlers.test.js
CHANGED
|
@@ -40,6 +40,84 @@ function createRequest(name, args) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
test("run_tests guidance mentions core documentation types", async () => {
|
|
44
|
+
await withWorkflowState(async (workflowState) => {
|
|
45
|
+
workflowState.state.bugFixed = true;
|
|
46
|
+
workflowState.state.testsCreated = true;
|
|
47
|
+
await workflowState.save();
|
|
48
|
+
|
|
49
|
+
const response = await handleToolCall({
|
|
50
|
+
request: createRequest("run_tests", {
|
|
51
|
+
passed: true,
|
|
52
|
+
testCommand: "npm test",
|
|
53
|
+
}),
|
|
54
|
+
normalizeRequestArgs,
|
|
55
|
+
workflowState,
|
|
56
|
+
exec: async () => ({ stdout: "" }),
|
|
57
|
+
git: {
|
|
58
|
+
hasWorkingChanges: async () => false,
|
|
59
|
+
hasStagedChanges: async () => false,
|
|
60
|
+
hasTestChanges: async () => true,
|
|
61
|
+
getStagedChanges: async () => [],
|
|
62
|
+
getCurrentBranch: async () => "main",
|
|
63
|
+
getPrimaryBranch: async () => "main",
|
|
64
|
+
getLastCommitMessage: async () => "",
|
|
65
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
66
|
+
},
|
|
67
|
+
utils,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const text = response.content[0].text;
|
|
71
|
+
assert.ok(
|
|
72
|
+
text.includes("create or update documentation using 'create_documentation'"),
|
|
73
|
+
"run_tests guidance should mention create or update documentation",
|
|
74
|
+
);
|
|
75
|
+
assert.ok(
|
|
76
|
+
text.includes('"PRD", "README", "RELEASE_NOTES"'),
|
|
77
|
+
"run_tests guidance should list core documentation types",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("create_documentation accepts PRD and records created/updated message", async () => {
|
|
83
|
+
await withWorkflowState(async (workflowState) => {
|
|
84
|
+
workflowState.state.testsPassed = true;
|
|
85
|
+
await workflowState.save();
|
|
86
|
+
|
|
87
|
+
const response = await handleToolCall({
|
|
88
|
+
request: createRequest("create_documentation", {
|
|
89
|
+
documentationType: "PRD",
|
|
90
|
+
summary: "Updated PRD for documentation workflow expectations",
|
|
91
|
+
}),
|
|
92
|
+
normalizeRequestArgs,
|
|
93
|
+
workflowState,
|
|
94
|
+
exec: async () => ({ stdout: "" }),
|
|
95
|
+
git: {
|
|
96
|
+
hasWorkingChanges: async () => false,
|
|
97
|
+
hasStagedChanges: async () => false,
|
|
98
|
+
hasTestChanges: async () => true,
|
|
99
|
+
getStagedChanges: async () => [],
|
|
100
|
+
getCurrentBranch: async () => "main",
|
|
101
|
+
getPrimaryBranch: async () => "main",
|
|
102
|
+
getLastCommitMessage: async () => "",
|
|
103
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
104
|
+
},
|
|
105
|
+
utils,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(workflowState.state.documentationCreated, true);
|
|
109
|
+
assert.equal(workflowState.state.documentationType, "PRD");
|
|
110
|
+
assert.equal(
|
|
111
|
+
workflowState.state.documentationSummary,
|
|
112
|
+
"Updated PRD for documentation workflow expectations",
|
|
113
|
+
);
|
|
114
|
+
assert.ok(
|
|
115
|
+
response.content[0].text.includes("Documentation created/updated!"),
|
|
116
|
+
"create_documentation response should mention created/updated",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
43
121
|
test("commit_and_push commits changes and pushes", async () => {
|
|
44
122
|
await withWorkflowState(async (workflowState) => {
|
|
45
123
|
workflowState.state.readyToCommit = true;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
async function readJson(relativePath) {
|
|
9
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
10
|
+
const content = await readFile(fullPath, "utf-8");
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("package.json includes mcpName for MCP registry", async () => {
|
|
15
|
+
const pkg = await readJson("package.json");
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(
|
|
18
|
+
pkg.mcpName,
|
|
19
|
+
"io.github.programinglive/dev-workflow-mcp-server",
|
|
20
|
+
"package.json should declare mcpName matching the MCP server namespace",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("server.json is configured for npm package deployment", async () => {
|
|
25
|
+
const server = await readJson("server.json");
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(
|
|
28
|
+
server.name,
|
|
29
|
+
"io.github.programinglive/dev-workflow-mcp-server",
|
|
30
|
+
"server.json name should match the MCP server namespace",
|
|
31
|
+
);
|
|
32
|
+
assert.ok(Array.isArray(server.packages) && server.packages.length > 0,
|
|
33
|
+
"server.json should declare at least one package entry",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const npmPkg = server.packages[0];
|
|
37
|
+
|
|
38
|
+
assert.strictEqual(
|
|
39
|
+
npmPkg.registryType,
|
|
40
|
+
"npm",
|
|
41
|
+
"First server.json package entry should use npm registryType",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
assert.strictEqual(
|
|
45
|
+
npmPkg.registryBaseUrl,
|
|
46
|
+
"https://registry.npmjs.org",
|
|
47
|
+
"server.json npm registryBaseUrl should point at the public npm registry",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.strictEqual(
|
|
51
|
+
npmPkg.identifier,
|
|
52
|
+
"@programinglive/dev-workflow-mcp-server",
|
|
53
|
+
"server.json npm identifier should match the published npm package name",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
assert.ok(
|
|
57
|
+
typeof npmPkg.version === "string" && npmPkg.version.length > 0,
|
|
58
|
+
"server.json npm package version should be a non-empty string",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
assert.ok(
|
|
62
|
+
npmPkg.transport && npmPkg.transport.type === "stdio",
|
|
63
|
+
"server.json npm package transport should be stdio",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
@@ -18,6 +18,8 @@ async function runWrapper({ state, args = ["patch"], env = {} }) {
|
|
|
18
18
|
const statePath = path.join(stateDir, "workflow-state.json");
|
|
19
19
|
await mkdir(stateDir, { recursive: true });
|
|
20
20
|
await writeFile(statePath, JSON.stringify(state, null, 2));
|
|
21
|
+
// Create package.json so resolveStateFile recognizes this as a project root
|
|
22
|
+
await writeFile(path.join(dir, "package.json"), JSON.stringify({ name: "test-project" }));
|
|
21
23
|
|
|
22
24
|
const childEnv = {
|
|
23
25
|
...process.env,
|
|
@@ -67,14 +67,16 @@ test("resolveStateFile prefers current working directory when multiple project m
|
|
|
67
67
|
|
|
68
68
|
const state = new TempWorkflowState();
|
|
69
69
|
const resolvedStateFile = state.stateFile;
|
|
70
|
+
const resolvedProjectA = await realpath(projectA);
|
|
71
|
+
const resolvedProjectB = await realpath(projectB);
|
|
70
72
|
|
|
71
73
|
assert.ok(
|
|
72
|
-
resolvedStateFile.startsWith(path.join(
|
|
74
|
+
resolvedStateFile.startsWith(path.join(resolvedProjectB, ".state", "users")),
|
|
73
75
|
"State file should resolve under the current working directory's project"
|
|
74
76
|
);
|
|
75
77
|
|
|
76
78
|
assert.ok(
|
|
77
|
-
!resolvedStateFile.startsWith(path.join(
|
|
79
|
+
!resolvedStateFile.startsWith(path.join(resolvedProjectA, ".state")),
|
|
78
80
|
"State file should not use INIT_CWD project when cwd has its own markers"
|
|
79
81
|
);
|
|
80
82
|
} finally {
|
|
@@ -303,9 +305,11 @@ test("WorkflowState resolves state file relative to project root", async () => {
|
|
|
303
305
|
if (devStateWasSet) {
|
|
304
306
|
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
305
307
|
}
|
|
308
|
+
const resolvedProjectRoot = await realpath(projectRoot);
|
|
309
|
+
|
|
306
310
|
const state = new WorkflowState();
|
|
307
311
|
assert.ok(
|
|
308
|
-
state.stateFile.startsWith(path.join(
|
|
312
|
+
state.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
|
|
309
313
|
"state file should resolve under project .state/users directory"
|
|
310
314
|
);
|
|
311
315
|
|
|
@@ -314,7 +318,6 @@ test("WorkflowState resolves state file relative to project root", async () => {
|
|
|
314
318
|
delete process.env.DEV_WORKFLOW_STATE_FILE;
|
|
315
319
|
|
|
316
320
|
const state2 = new WorkflowState();
|
|
317
|
-
const resolvedProjectRoot = await realpath(projectRoot);
|
|
318
321
|
assert.ok(
|
|
319
322
|
state2.stateFile.startsWith(path.join(resolvedProjectRoot, ".state", "users")),
|
|
320
323
|
"state file should resolve to project root"
|