@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 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 drive the git commands once verification passes
15
- 7. **Release** - Record the release details before closing out the task
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
@@ -0,0 +1,4 @@
1
+ import { exec as execCallback } from "child_process";
2
+ import { promisify } from "util";
3
+
4
+ export const exec = promisify(execCallback);
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
+ }