@programinglive/dev-workflow-mcp-server 1.0.9 → 1.1.0
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 +43 -0
- package/README.md +2 -2
- package/commit-helpers.js +166 -0
- package/exec.js +4 -0
- package/git-helpers.js +152 -0
- package/index.js +45 -1300
- package/package.json +1 -1
- package/tests/handlers.test.js +460 -0
- package/tools/definitions.js +211 -0
- package/tools/handlers.js +739 -0
- package/utils.js +45 -0
- package/workflow-state.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
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.1.0](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.11...v1.1.0) (2025-10-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### 📝 Documentation
|
|
9
|
+
|
|
10
|
+
* clarify commit and push step ([c741f61](https://github.com/programinglive/dev-workflow-mcp-server/commit/c741f618813479c5df2bcca94c3dffc8881e97c8))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### 🐛 Bug Fixes
|
|
14
|
+
|
|
15
|
+
* allow commit_and_push on clean tree ([a831974](https://github.com/programinglive/dev-workflow-mcp-server/commit/a831974f66313ebe8752d416752076df63902bea))
|
|
16
|
+
* auto-detect commit in perform_release when tree is clean ([0db7038](https://github.com/programinglive/dev-workflow-mcp-server/commit/0db7038148f68963f53e52a5f73623dea7ad3462))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### ✨ Features
|
|
20
|
+
|
|
21
|
+
* check staged changes when resetting workflow to commit phase ([05d02fd](https://github.com/programinglive/dev-workflow-mcp-server/commit/05d02fd7ec6177af3fb1fd86ec093e1e37b56cbd))
|
|
22
|
+
* continue_workflow auto-executes next step in commit phase ([a5b49ed](https://github.com/programinglive/dev-workflow-mcp-server/commit/a5b49edaae7cb51ea3aa49d9b1b64587843441ce))
|
|
23
|
+
|
|
24
|
+
### [1.0.11](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.10...v1.0.11) (2025-10-25)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### ✨ Features
|
|
28
|
+
|
|
29
|
+
* add continue workflow guidance ([516b33e](https://github.com/programinglive/dev-workflow-mcp-server/commit/516b33ecfc7533df5868c9c26458088d6ea591a2))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### 🐛 Bug Fixes
|
|
33
|
+
|
|
34
|
+
* ensure commit and push happens before release ([08ee662](https://github.com/programinglive/dev-workflow-mcp-server/commit/08ee662f525431ba4a2d735ed253aad8d6720ac2))
|
|
35
|
+
|
|
36
|
+
### [1.0.10](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.9...v1.0.10) (2025-10-24)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### ♻️ Refactors
|
|
40
|
+
|
|
41
|
+
* modularize workflow server ([f0c9401](https://github.com/programinglive/dev-workflow-mcp-server/commit/f0c940123a8e6f961555cc7ba4d6dec2deb854e7))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### ✅ Tests
|
|
45
|
+
|
|
46
|
+
* cover release push flow ([933ff11](https://github.com/programinglive/dev-workflow-mcp-server/commit/933ff1126af08a49d1ce38a4eedcc738b0064efb))
|
|
47
|
+
|
|
5
48
|
### [1.0.9](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.8...v1.0.9) (2025-10-23)
|
|
6
49
|
|
|
7
50
|
|
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ This MCP server guides you through a disciplined development workflow:
|
|
|
11
11
|
3. **Create Tests** - Always test your changes
|
|
12
12
|
4. **Run Tests** - Tests must pass (GREEN)
|
|
13
13
|
5. **Document** - Update documentation
|
|
14
|
-
6. **Commit & Push** - Let the server
|
|
15
|
-
7. **Release** -
|
|
14
|
+
6. **Commit & Push** - Let the server stage, commit, **and push** your changes once verification passes (if you make new edits afterward, the workflow moves back to this step automatically)
|
|
15
|
+
7. **Release** - After the push succeeds, record the release details before closing out the task
|
|
16
16
|
|
|
17
17
|
## 🚀 Installation
|
|
18
18
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
function formatFileList(paths) {
|
|
2
|
+
const uniquePaths = [...new Set(paths.filter(Boolean))];
|
|
3
|
+
|
|
4
|
+
if (uniquePaths.length === 0) {
|
|
5
|
+
return "updates";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (uniquePaths.length === 1) {
|
|
9
|
+
return uniquePaths[0];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (uniquePaths.length === 2) {
|
|
13
|
+
return `${uniquePaths[0]} and ${uniquePaths[1]}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (uniquePaths.length === 3) {
|
|
17
|
+
return `${uniquePaths[0]}, ${uniquePaths[1]}, and ${uniquePaths[2]}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const remaining = uniquePaths.length - 3;
|
|
21
|
+
return `${uniquePaths[0]}, ${uniquePaths[1]}, ${uniquePaths[2]} +${remaining} more`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function determineCommitType(changes) {
|
|
25
|
+
const hasTestChange = changes.some((change) =>
|
|
26
|
+
[change.path, change.originalPath].some((p) => isTestFilePath(p))
|
|
27
|
+
);
|
|
28
|
+
const hasDocsChange = changes.some((change) =>
|
|
29
|
+
[change.path, change.originalPath].some((p) => isDocumentationFilePath(p))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (hasTestChange) {
|
|
33
|
+
return "test";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (hasDocsChange) {
|
|
37
|
+
return "docs";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return "chore";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isTestFilePath(filePath) {
|
|
44
|
+
if (!filePath) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (/\.(test|spec)\.[^.]+$/i.test(filePath)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return /(\/|^)(__tests__|tests?|spec)\//i.test(filePath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isDocumentationFilePath(filePath) {
|
|
56
|
+
if (!filePath) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalized = filePath.toLowerCase();
|
|
61
|
+
return (
|
|
62
|
+
normalized.startsWith("docs/") ||
|
|
63
|
+
normalized.startsWith("documentation/") ||
|
|
64
|
+
normalized.endsWith(".md") ||
|
|
65
|
+
normalized.endsWith(".rst") ||
|
|
66
|
+
normalized.endsWith(".adoc") ||
|
|
67
|
+
normalized.includes("readme")
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function describeChange(change) {
|
|
72
|
+
const statusCode = change.status || "";
|
|
73
|
+
const path = change.path || change.originalPath || "";
|
|
74
|
+
|
|
75
|
+
if (statusCode.startsWith("R")) {
|
|
76
|
+
return `renamed ${change.originalPath} -> ${change.path}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (statusCode.startsWith("C")) {
|
|
80
|
+
return `copied ${change.originalPath} -> ${change.path}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (statusCode[0]) {
|
|
84
|
+
case "A":
|
|
85
|
+
return `added ${path}`;
|
|
86
|
+
case "M":
|
|
87
|
+
return `modified ${path}`;
|
|
88
|
+
case "D":
|
|
89
|
+
return `removed ${path}`;
|
|
90
|
+
case "U":
|
|
91
|
+
return `updated ${path}`;
|
|
92
|
+
default:
|
|
93
|
+
return `${statusCode} ${path}`.trim();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createCommitMessageParts(changes, providedSummary = "") {
|
|
98
|
+
const summaryInput = typeof providedSummary === "string" ? providedSummary.trim() : "";
|
|
99
|
+
|
|
100
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
summary: summaryInput,
|
|
103
|
+
body: "",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const commitType = determineCommitType(changes);
|
|
108
|
+
const summaryPaths = changes
|
|
109
|
+
.map((change) => change.path || change.originalPath)
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
const generatedSummary = `${commitType}: update ${formatFileList(summaryPaths)}`;
|
|
112
|
+
const summary = summaryInput || generatedSummary;
|
|
113
|
+
|
|
114
|
+
const detailLines = changes
|
|
115
|
+
.map((change) => describeChange(change))
|
|
116
|
+
.filter((line) => line && line.trim().length > 0)
|
|
117
|
+
.map((line) => `- ${line}`);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
summary,
|
|
121
|
+
body: detailLines.join("\n"),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeCommitHeader(message) {
|
|
126
|
+
if (typeof message !== "string") {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const header = message.split("\n")[0] || "";
|
|
131
|
+
return header.trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function containsBreakingChange(message) {
|
|
135
|
+
if (typeof message !== "string") {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return /breaking change/i.test(message);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function determineReleaseTypeFromCommit(message) {
|
|
143
|
+
const header = normalizeCommitHeader(message).toLowerCase();
|
|
144
|
+
const hasBreaking = containsBreakingChange(message) || /!/.test(header.split(":")[0] || "");
|
|
145
|
+
|
|
146
|
+
if (hasBreaking) {
|
|
147
|
+
return "major";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const typeMatch = header.match(/^([a-z]+)(\(|:)/i);
|
|
151
|
+
const commitType = typeMatch ? typeMatch[1] : "";
|
|
152
|
+
|
|
153
|
+
if (commitType === "feat") {
|
|
154
|
+
return "minor";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (commitType === "fix" || commitType === "perf") {
|
|
158
|
+
return "patch";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!commitType && containsBreakingChange(message)) {
|
|
162
|
+
return "major";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return "patch";
|
|
166
|
+
}
|
package/exec.js
ADDED
package/git-helpers.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { exec } from "./exec.js";
|
|
2
|
+
|
|
3
|
+
export async function hasStagedChanges() {
|
|
4
|
+
try {
|
|
5
|
+
const { stdout } = await exec("git status --porcelain");
|
|
6
|
+
return stdout
|
|
7
|
+
.split("\n")
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.some((line) => {
|
|
10
|
+
const indexStatus = line[0];
|
|
11
|
+
return indexStatus && indexStatus !== " " && indexStatus !== "?";
|
|
12
|
+
});
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function hasWorkingChanges() {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await exec("git status --porcelain");
|
|
21
|
+
return stdout.split("\n").some((line) => line && line.trim().length > 0);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function workingTreeSummary(statusOutput) {
|
|
28
|
+
if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
|
|
29
|
+
return {
|
|
30
|
+
hasChanges: false,
|
|
31
|
+
lines: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = statusOutput
|
|
36
|
+
.split("\n")
|
|
37
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
hasChanges: lines.length > 0,
|
|
42
|
+
lines,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isTestFilePath(filePath) {
|
|
47
|
+
if (!filePath) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (/\.(test|spec)\.[^.]+$/i.test(filePath)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return /(\/|^)(__tests__|tests?|spec)\//i.test(filePath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractPathFromStatusLine(line) {
|
|
59
|
+
if (!line || line.length < 4) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rawPath = line.slice(3).trim();
|
|
64
|
+
if (!rawPath) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (rawPath.includes(" -> ")) {
|
|
69
|
+
const parts = rawPath.split(" -> ");
|
|
70
|
+
return parts[parts.length - 1].trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return rawPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function containsTestFilesInStatus(statusOutput) {
|
|
77
|
+
if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return statusOutput
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => extractPathFromStatusLine(line))
|
|
84
|
+
.some((path) => isTestFilePath(path));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseNameStatusLine(line) {
|
|
88
|
+
if (!line) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parts = line.split("\t").map((part) => part.trim());
|
|
93
|
+
const status = parts[0];
|
|
94
|
+
if (!status) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (status.startsWith("R") || status.startsWith("C")) {
|
|
99
|
+
const originalPath = parts[1] || "";
|
|
100
|
+
const newPath = parts[2] || originalPath;
|
|
101
|
+
return {
|
|
102
|
+
status,
|
|
103
|
+
path: newPath,
|
|
104
|
+
originalPath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
status,
|
|
110
|
+
path: parts[1] || "",
|
|
111
|
+
originalPath: "",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function getStagedChanges() {
|
|
116
|
+
try {
|
|
117
|
+
const { stdout } = await exec("git diff --cached --name-status");
|
|
118
|
+
return stdout
|
|
119
|
+
.split("\n")
|
|
120
|
+
.map((line) => parseNameStatusLine(line))
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function hasTestChanges() {
|
|
128
|
+
try {
|
|
129
|
+
const { stdout } = await exec("git status --porcelain");
|
|
130
|
+
return containsTestFilesInStatus(stdout);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function getLastCommitMessage() {
|
|
137
|
+
try {
|
|
138
|
+
const { stdout } = await exec("git log -1 --pretty=%B");
|
|
139
|
+
return stdout.trim();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function getCurrentBranch() {
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
|
|
148
|
+
return stdout.trim();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
}
|