@programinglive/dev-workflow-mcp-server 1.0.9 → 1.0.11
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 +24 -0
- 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 +188 -0
- package/tools/definitions.js +211 -0
- package/tools/handlers.js +620 -0
- package/utils.js +45 -0
- package/workflow-state.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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.0.11](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.10...v1.0.11) (2025-10-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ✨ Features
|
|
9
|
+
|
|
10
|
+
* add continue workflow guidance ([516b33e](https://github.com/programinglive/dev-workflow-mcp-server/commit/516b33ecfc7533df5868c9c26458088d6ea591a2))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### 🐛 Bug Fixes
|
|
14
|
+
|
|
15
|
+
* ensure commit and push happens before release ([08ee662](https://github.com/programinglive/dev-workflow-mcp-server/commit/08ee662f525431ba4a2d735ed253aad8d6720ac2))
|
|
16
|
+
|
|
17
|
+
### [1.0.10](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.9...v1.0.10) (2025-10-24)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ♻️ Refactors
|
|
21
|
+
|
|
22
|
+
* modularize workflow server ([f0c9401](https://github.com/programinglive/dev-workflow-mcp-server/commit/f0c940123a8e6f961555cc7ba4d6dec2deb854e7))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### ✅ Tests
|
|
26
|
+
|
|
27
|
+
* cover release push flow ([933ff11](https://github.com/programinglive/dev-workflow-mcp-server/commit/933ff1126af08a49d1ce38a4eedcc738b0064efb))
|
|
28
|
+
|
|
5
29
|
### [1.0.9](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.8...v1.0.9) (2025-10-23)
|
|
6
30
|
|
|
7
31
|
|
|
@@ -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
|
+
}
|