@programinglive/dev-workflow-mcp-server 1.13.1 → 1.13.3
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/exec.js +23 -7
- package/git-helpers.js +41 -51
- package/package.json +1 -1
- package/scripts/release.cjs +67 -7
- package/tools/handler-modules/commit-handlers.js +19 -4
- package/tools/handler-modules/release-handlers.js +22 -2
package/exec.js
CHANGED
|
@@ -52,8 +52,16 @@ function samePath(a, b) {
|
|
|
52
52
|
* Menentukan direktori repo proyek tempat git boleh dijalankan.
|
|
53
53
|
*
|
|
54
54
|
* Urutan: eksplisit -> WORKFLOW_PROJECT_DIR/PROJECT_DIR -> kandidat.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
*
|
|
56
|
+
* Repo paket MCP sengaja tidak dipilih sebagai cadangan: dulu server pernah
|
|
57
|
+
* menjalankan git di repo-nya sendiri saat cwd proyek gagal diresolusi, lalu
|
|
58
|
+
* melaporkan "No changes detected" sementara perubahan proyek tidak pernah
|
|
59
|
+
* ter-commit.
|
|
60
|
+
*
|
|
61
|
+
* Tetapi kandidat yang MEMANG berada di dalam repo paket berarti pemanggil
|
|
62
|
+
* sengaja bekerja di sana - misalnya saat mengembangkan server MCP ini sendiri.
|
|
63
|
+
* Membuangnya tanpa syarat membuat `commit_and_push` mustahil dipakai di repo
|
|
64
|
+
* paket, jadi kandidat seperti itu diterima. Repo proyek lain tetap menang.
|
|
57
65
|
*/
|
|
58
66
|
export function resolveWorkingDirectory({ explicit = "", env = process.env, candidates = [] } = {}) {
|
|
59
67
|
if (explicit) {
|
|
@@ -74,14 +82,22 @@ export function resolveWorkingDirectory({ explicit = "", env = process.env, cand
|
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
const found = candidates.map((candidate) => findRepoRoot(candidate)).filter(Boolean);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
|
|
86
|
+
const projectRepo = found.find((dir) => !samePath(dir, PACKAGE_DIR));
|
|
87
|
+
if (projectRepo) {
|
|
88
|
+
return projectRepo;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Hanya repo paket yang ditemukan. Itu hanya mungkin bila salah satu kandidat
|
|
92
|
+
// benar-benar berada di dalamnya, jadi pemanggil memilih repo ini dengan sadar.
|
|
93
|
+
if (found.some((dir) => samePath(dir, PACKAGE_DIR))) {
|
|
94
|
+
return path.resolve(PACKAGE_DIR);
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
throw new Error(
|
|
83
|
-
"Tidak bisa menentukan repo proyek:
|
|
84
|
-
|
|
98
|
+
"Tidak bisa menentukan repo proyek: tidak ada repository git pada " +
|
|
99
|
+
`${candidates.filter(Boolean).map((candidate) => `"${candidate}"`).join(", ") || "direktori kerja"}. ` +
|
|
100
|
+
"Set WORKFLOW_PROJECT_DIR ke direktori proyek, atau jalankan server MCP dari dalam direktori proyek.",
|
|
85
101
|
);
|
|
86
102
|
}
|
|
87
103
|
|
package/git-helpers.js
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
import { exec } from "./exec.js";
|
|
2
2
|
|
|
3
3
|
export async function hasStagedChanges() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
} catch (error) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
4
|
+
const { stdout } = await exec("git status --porcelain");
|
|
5
|
+
return stdout
|
|
6
|
+
.split("\n")
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.some((line) => {
|
|
9
|
+
const indexStatus = line[0];
|
|
10
|
+
return indexStatus && indexStatus !== " " && indexStatus !== "?";
|
|
11
|
+
});
|
|
16
12
|
}
|
|
17
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Apakah ada perubahan yang belum di-commit?
|
|
16
|
+
*
|
|
17
|
+
* MENGAPA TIDAK `catch { return false }`
|
|
18
|
+
* --------------------------------------
|
|
19
|
+
* Dulu fungsi ini menelan setiap error git dan menjawab `false`. Pemanggil
|
|
20
|
+
* membacanya sebagai "tidak ada perubahan", lalu `commit_and_push` menjawab
|
|
21
|
+
* "No changes detected. Assuming commit and push already completed" - sukses
|
|
22
|
+
* palsu, padahal tidak ada commit sama sekali.
|
|
23
|
+
*
|
|
24
|
+
* Yang terjadi 30 Oktober 2026: server MCP dijalankan dengan cwd direktori sesi
|
|
25
|
+
* OpenCode (`Documents\Default Project`) yang bukan repo git. `git status`
|
|
26
|
+
* gagal, error ditelan, delapan berkas tidak pernah ter-commit, dan tool tetap
|
|
27
|
+
* melaporkan berhasil.
|
|
28
|
+
*
|
|
29
|
+
* "Tidak tahu" bukan "tidak ada perubahan". Sekarang error diteruskan supaya
|
|
30
|
+
* pemanggil bisa melaporkan masalah sebenarnya.
|
|
31
|
+
*/
|
|
18
32
|
export async function hasWorkingChanges() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return stdout.split("\n").some((line) => line && line.trim().length > 0);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
33
|
+
const { stdout } = await exec("git status --porcelain");
|
|
34
|
+
return stdout.split("\n").some((line) => line && line.trim().length > 0);
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export function workingTreeSummary(statusOutput) {
|
|
@@ -113,51 +123,31 @@ function parseNameStatusLine(line) {
|
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
export async function getStagedChanges() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
.filter(Boolean);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
126
|
+
const { stdout } = await exec("git diff --cached --name-status");
|
|
127
|
+
return stdout
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((line) => parseNameStatusLine(line))
|
|
130
|
+
.filter(Boolean);
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
export async function hasTestChanges() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return containsTestFilesInStatus(stdout);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
+
const { stdout } = await exec("git status --porcelain");
|
|
135
|
+
return containsTestFilesInStatus(stdout);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
export async function getLastCommitMessage() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return stdout.trim();
|
|
140
|
-
} catch (error) {
|
|
141
|
-
return "";
|
|
142
|
-
}
|
|
139
|
+
const { stdout } = await exec("git log -1 --pretty=%B");
|
|
140
|
+
return stdout.trim();
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
export async function getCurrentBranch() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return stdout.trim();
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return "";
|
|
151
|
-
}
|
|
144
|
+
const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
|
|
145
|
+
return stdout.trim();
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
export async function getStatusOutput() {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return stdout;
|
|
158
|
-
} catch (error) {
|
|
159
|
-
return "";
|
|
160
|
-
}
|
|
149
|
+
const { stdout } = await exec("git status --porcelain");
|
|
150
|
+
return stdout;
|
|
161
151
|
}
|
|
162
152
|
|
|
163
153
|
export async function getPrimaryBranch() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.3",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"homepage": "https://devworkflow.programinglive.com/",
|
|
6
6
|
"mcpName": "io.github.programinglive/dev-workflow-mcp-server",
|
package/scripts/release.cjs
CHANGED
|
@@ -40,6 +40,40 @@ function getCliArguments(argv = process.argv) {
|
|
|
40
40
|
return { releaseType: 'patch', extraArgs: [] };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// `--release-as` selalu diambil sebagai tipe rilis dan DIBUANG dari extraArgs.
|
|
44
|
+
//
|
|
45
|
+
// Kalau dibiarkan, argumen itu sampai ke standard-version dua kali: sekali
|
|
46
|
+
// dari releaseType, sekali dari extraArgs. yargs lalu mengubahnya menjadi
|
|
47
|
+
// array, dan `semver.inc("1.13.2", ["patch","patch"])` mengembalikan null -
|
|
48
|
+
// rilis menulis `"version": null` dan tag `vnull`.
|
|
49
|
+
const releaseAsIndex = rawArgs.indexOf('--release-as');
|
|
50
|
+
if (releaseAsIndex !== -1) {
|
|
51
|
+
const value = rawArgs[releaseAsIndex + 1];
|
|
52
|
+
if (!value || value.startsWith('-')) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'--release-as memerlukan nilai tipe rilis (major, minor, patch, atau versi semver).'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
releaseType: value,
|
|
59
|
+
extraArgs: [...rawArgs.slice(0, releaseAsIndex), ...rawArgs.slice(releaseAsIndex + 2)]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const inline = rawArgs.find((arg) => arg.startsWith('--release-as='));
|
|
64
|
+
if (inline) {
|
|
65
|
+
const value = inline.slice('--release-as='.length);
|
|
66
|
+
if (!value) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'--release-as memerlukan nilai tipe rilis (major, minor, patch, atau versi semver).'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
releaseType: value,
|
|
73
|
+
extraArgs: rawArgs.filter((arg) => arg !== inline)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
if (rawArgs[0].startsWith('-')) {
|
|
44
78
|
return { releaseType: 'patch', extraArgs: rawArgs };
|
|
45
79
|
}
|
|
@@ -62,17 +96,43 @@ function getNpmRunArgument(env = process.env) {
|
|
|
62
96
|
return undefined;
|
|
63
97
|
}
|
|
64
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Membuang setiap `--release-as` dari argumen tambahan.
|
|
101
|
+
*
|
|
102
|
+
* Pertahanan lapis kedua: tipe rilis sudah ditentukan lewat `releaseType`, jadi
|
|
103
|
+
* salinan apa pun di `extraArgs` hanya akan menggandakan opsi dan membuat yargs
|
|
104
|
+
* memberinya array - yang berakhir pada `semver.inc(...) === null`.
|
|
105
|
+
*/
|
|
106
|
+
function stripReleaseAs(extraArgs) {
|
|
107
|
+
if (!Array.isArray(extraArgs)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const kept = [];
|
|
112
|
+
for (let index = 0; index < extraArgs.length; index += 1) {
|
|
113
|
+
const arg = extraArgs[index];
|
|
114
|
+
if (arg === '--release-as') {
|
|
115
|
+
index += 1; // lewati nilainya juga
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (typeof arg === 'string' && arg.startsWith('--release-as=')) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
kept.push(arg);
|
|
122
|
+
}
|
|
123
|
+
return kept;
|
|
124
|
+
}
|
|
125
|
+
|
|
65
126
|
function buildStandardVersionArgs({ releaseType, extraArgs }) {
|
|
66
127
|
const args = [];
|
|
67
|
-
|
|
128
|
+
const remainingArgs = stripReleaseAs(extraArgs);
|
|
129
|
+
|
|
68
130
|
// Handle --first-release flag
|
|
69
|
-
const isFirstRelease =
|
|
131
|
+
const isFirstRelease = remainingArgs.includes('--first-release');
|
|
70
132
|
if (isFirstRelease) {
|
|
71
133
|
args.push('--release-as', '0.0.1');
|
|
72
|
-
// Remove --first-release from extraArgs to avoid passing it to standard-version
|
|
73
|
-
extraArgs = extraArgs.filter(arg => arg !== '--first-release');
|
|
74
134
|
} else if (releaseType) {
|
|
75
|
-
const normalized = releaseType.trim();
|
|
135
|
+
const normalized = String(releaseType).trim();
|
|
76
136
|
const isValid = VALID_RELEASE_TYPES.has(normalized) || SEMVER_REGEX.test(normalized);
|
|
77
137
|
if (!isValid) {
|
|
78
138
|
const allowed = Array.from(VALID_RELEASE_TYPES).join(', ');
|
|
@@ -81,8 +141,8 @@ function buildStandardVersionArgs({ releaseType, extraArgs }) {
|
|
|
81
141
|
args.push('--release-as', normalized);
|
|
82
142
|
}
|
|
83
143
|
|
|
84
|
-
if (
|
|
85
|
-
args.push(...
|
|
144
|
+
if (remainingArgs.length > 0) {
|
|
145
|
+
args.push(...remainingArgs.filter((arg) => arg !== '--first-release'));
|
|
86
146
|
}
|
|
87
147
|
|
|
88
148
|
return args;
|
|
@@ -15,11 +15,26 @@ export async function handleCommitAndPush(args, context) {
|
|
|
15
15
|
typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
|
|
16
16
|
|
|
17
17
|
const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
|
|
18
|
-
const primaryBranch = await git.getPrimaryBranch();
|
|
19
|
-
const currentBranch = await git.getCurrentBranch();
|
|
20
|
-
const branchForPush = requestedBranch || primaryBranch || currentBranch || "";
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
// Preflight: kalau git tidak bisa dijalankan, jangan menebak. Dulu rantai ini
|
|
20
|
+
// gagal, error ditelan helper, lalu tool menjawab "No changes detected" dan
|
|
21
|
+
// menandai commit selesai padahal tidak ada commit sama sekali.
|
|
22
|
+
let primaryBranch;
|
|
23
|
+
let currentBranch;
|
|
24
|
+
let hasWorkingChanges;
|
|
25
|
+
try {
|
|
26
|
+
primaryBranch = await git.getPrimaryBranch();
|
|
27
|
+
currentBranch = await git.getCurrentBranch();
|
|
28
|
+
hasWorkingChanges = await git.hasWorkingChanges();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return textResponse(
|
|
31
|
+
`❌ Tidak bisa memeriksa repository Git, jadi tidak ada yang di-commit.\n\n` +
|
|
32
|
+
`Penyebab: ${error.message}\n\n` +
|
|
33
|
+
`Set WORKFLOW_PROJECT_DIR ke direktori proyek, atau jalankan server MCP dari dalam direktori proyek.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const branchForPush = requestedBranch || primaryBranch || currentBranch || "";
|
|
23
38
|
|
|
24
39
|
if (!hasWorkingChanges) {
|
|
25
40
|
const lastCommitMessage = await git.getLastCommitMessage();
|
|
@@ -50,7 +50,18 @@ export async function handlePerformRelease(args, context) {
|
|
|
50
50
|
return textResponse("⚠️ Please start a task first using 'start_task'!");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
let hasNewChanges;
|
|
54
|
+
try {
|
|
55
|
+
hasNewChanges = await resetToCommitIfWorkingChanges(workflowState, git);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return textResponse(
|
|
58
|
+
`❌ Tidak bisa memeriksa repository Git, jadi status rilis tidak dapat diverifikasi.\n\n` +
|
|
59
|
+
`Penyebab: ${error.message}\n\n` +
|
|
60
|
+
`Set WORKFLOW_PROJECT_DIR ke direktori proyek, atau jalankan server MCP dari dalam direktori proyek.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (hasNewChanges) {
|
|
54
65
|
return textResponse(
|
|
55
66
|
"⚠️ Detected new changes after the last commit. Please run 'commit_and_push' again before recording a release."
|
|
56
67
|
);
|
|
@@ -63,7 +74,16 @@ export async function handlePerformRelease(args, context) {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
if (!workflowState.state.commitAndPushCompleted) {
|
|
66
|
-
|
|
77
|
+
let hasWorkingChanges;
|
|
78
|
+
try {
|
|
79
|
+
hasWorkingChanges = await git.hasWorkingChanges();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return textResponse(
|
|
82
|
+
`❌ Tidak bisa memeriksa repository Git, jadi status rilis tidak dapat diverifikasi.\n\n` +
|
|
83
|
+
`Penyebab: ${error.message}\n\n` +
|
|
84
|
+
`Set WORKFLOW_PROJECT_DIR ke direktori proyek, atau jalankan server MCP dari dalam direktori proyek.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
67
87
|
|
|
68
88
|
if (hasWorkingChanges) {
|
|
69
89
|
return textResponse(
|