@programinglive/dev-workflow-mcp-server 1.13.13 → 1.13.14
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/git-facade.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getRemoteBranchSha,
|
|
17
17
|
getStagedChanges,
|
|
18
18
|
getStatusOutput,
|
|
19
|
+
getToplevel,
|
|
19
20
|
hasStagedChanges,
|
|
20
21
|
hasTestChanges,
|
|
21
22
|
hasWorkingChanges,
|
|
@@ -34,6 +35,7 @@ export function createGitFacade() {
|
|
|
34
35
|
getRemoteBranchSha,
|
|
35
36
|
getLastCommitMessage,
|
|
36
37
|
getStatusOutput,
|
|
38
|
+
getToplevel,
|
|
37
39
|
workingTreeSummary,
|
|
38
40
|
};
|
|
39
41
|
}
|
package/git-helpers.js
CHANGED
|
@@ -173,6 +173,18 @@ export async function getStatusOutput() {
|
|
|
173
173
|
return stdout;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Direktori teratas repo tempat git BENAR-BENAR berjalan.
|
|
178
|
+
*
|
|
179
|
+
* `git rev-parse --show-toplevel` menjawab dari repo yang aktif, bukan dari
|
|
180
|
+
* tebakan kita tentang direktori mana yang dimaksud. Dipakai untuk memastikan
|
|
181
|
+
* laporan `commit_and_push` menyebut repo yang sama dengan yang di-commit.
|
|
182
|
+
*/
|
|
183
|
+
export async function getToplevel() {
|
|
184
|
+
const { stdout } = await exec("git rev-parse --show-toplevel");
|
|
185
|
+
return stdout.trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
export async function getPrimaryBranch() {
|
|
177
189
|
try {
|
|
178
190
|
// Check if 'main' exists on origin
|
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.14",
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"start": "node index.js",
|
|
22
22
|
"local": "node index.js",
|
|
23
23
|
"setup": "node scripts/setup-stack.js",
|
|
24
|
-
"test": "node --no-deprecation --test",
|
|
24
|
+
"test": "node --no-deprecation --import ./tests/setup-env.js --test",
|
|
25
25
|
"test:security": "node --no-deprecation --test tests/security*.test.js",
|
|
26
26
|
"security:audit": "node index.js call security_audit",
|
|
27
27
|
"lint": "node scripts/lint.mjs",
|
|
@@ -2,6 +2,7 @@ import { textResponse, isReleaseSatisfied, formatChecklist } from "./shared.js";
|
|
|
2
2
|
import { handleReadyCheck } from "./status-handlers.js";
|
|
3
3
|
import { containsTestFilesInStatus } from "../../git-helpers.js";
|
|
4
4
|
import { resolveProjectDir, setActiveProjectDir, getActiveProjectDir } from "../../exec.js";
|
|
5
|
+
import path from "path";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Label repo yang benar-benar dipakai, untuk ditampilkan di setiap jawaban.
|
|
@@ -24,6 +25,18 @@ function repoLabel() {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Apakah dua path menunjuk direktori yang sama.
|
|
30
|
+
*
|
|
31
|
+
* Windows tidak membedakan huruf besar/kecil dan kadang memberi garis miring
|
|
32
|
+
* di akhir, jadi perbandingan string mentah akan salah.
|
|
33
|
+
*/
|
|
34
|
+
function samePath(a, b) {
|
|
35
|
+
if (!a || !b) return false;
|
|
36
|
+
const normal = (value) => path.resolve(value).replace(/[\\/]+$/, "").toLowerCase();
|
|
37
|
+
return normal(a) === normal(b);
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
/**
|
|
28
41
|
* Terapkan `projectDir` dari argumen tool bila ada.
|
|
29
42
|
* Mengembalikan pesan kesalahan, atau null bila tidak ada masalah.
|
|
@@ -127,10 +140,21 @@ export async function handleCommitAndPush(args, context) {
|
|
|
127
140
|
let primaryBranch;
|
|
128
141
|
let currentBranch;
|
|
129
142
|
let hasWorkingChanges;
|
|
143
|
+
let gitToplevel = "";
|
|
130
144
|
try {
|
|
131
145
|
primaryBranch = await git.getPrimaryBranch();
|
|
132
146
|
currentBranch = await git.getCurrentBranch();
|
|
133
147
|
hasWorkingChanges = await git.hasWorkingChanges();
|
|
148
|
+
|
|
149
|
+
// Tempat git BENAR-BENAR berjalan, menurut git sendiri.
|
|
150
|
+
//
|
|
151
|
+
// 31 Oktober 2026 laporan menyebut `portal-beautyworld` untuk commit yang
|
|
152
|
+
// masuk ke repo lain, karena labelnya dibaca dari variabel global yang bisa
|
|
153
|
+
// berubah - bukan dari repo tempat git berjalan. Laporan seperti itu membuat
|
|
154
|
+
// agen percaya commit-nya ada di tempat yang salah.
|
|
155
|
+
if (typeof git.getToplevel === "function") {
|
|
156
|
+
gitToplevel = String((await git.getToplevel()) || "").trim();
|
|
157
|
+
}
|
|
134
158
|
} catch (error) {
|
|
135
159
|
return textResponse(
|
|
136
160
|
`❌ Tidak bisa memeriksa repository Git, jadi tidak ada yang di-commit.\n\n` +
|
|
@@ -140,6 +164,22 @@ export async function handleCommitAndPush(args, context) {
|
|
|
140
164
|
);
|
|
141
165
|
}
|
|
142
166
|
|
|
167
|
+
// Kalau git berjalan di repo lain daripada yang diminta, BERHENTI. Commit ke
|
|
168
|
+
// repo yang salah jauh lebih mahal daripada satu pesan kesalahan.
|
|
169
|
+
const requestedRepo = getActiveProjectDir();
|
|
170
|
+
if (gitToplevel && requestedRepo && !samePath(gitToplevel, requestedRepo)) {
|
|
171
|
+
return textResponse(
|
|
172
|
+
`❌ Dibatalkan: git berjalan di repo yang BERBEDA dari yang diminta.\n\n` +
|
|
173
|
+
`Diminta : ${requestedRepo}\n` +
|
|
174
|
+
`Dipakai git: ${gitToplevel}\n\n` +
|
|
175
|
+
`Tidak ada yang di-commit. Sebutkan 'projectDir' yang benar pada 'commit_and_push', ` +
|
|
176
|
+
`atau jalankan ulang 'start_task' dengan 'projectDir' repo tersebut.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Label laporan memakai repo yang benar-benar dipakai git bila diketahui.
|
|
181
|
+
const repoName = gitToplevel || repoLabel();
|
|
182
|
+
|
|
143
183
|
const branchForPush = pickPushBranch({ requestedBranch, currentBranch, primaryBranch });
|
|
144
184
|
|
|
145
185
|
if (!hasWorkingChanges) {
|
|
@@ -153,7 +193,7 @@ export async function handleCommitAndPush(args, context) {
|
|
|
153
193
|
if (!pushed.ok) {
|
|
154
194
|
return textResponse(
|
|
155
195
|
`⚠️ Tidak ada perubahan untuk di-commit, tetapi HEAD BELUM ada di remote.\n` +
|
|
156
|
-
`Repo: ${
|
|
196
|
+
`Repo: ${repoName}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
|
|
157
197
|
`Sebab: ${pushed.detail}\n\n` +
|
|
158
198
|
`Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
|
|
159
199
|
);
|
|
@@ -166,7 +206,7 @@ export async function handleCommitAndPush(args, context) {
|
|
|
166
206
|
await workflowState.save();
|
|
167
207
|
|
|
168
208
|
return textResponse(
|
|
169
|
-
`ℹ️ No changes detected. Assuming commit and push already completed.\nRepo: ${
|
|
209
|
+
`ℹ️ No changes detected. Assuming commit and push already completed.\nRepo: ${repoName}\nBranch: ${branchForPush} (verified on remote)\nLast commit: ${effectiveCommitMessage || "(unavailable)"
|
|
170
210
|
}\n\nNext: Run 'perform_release' to handle the release and push tags.`
|
|
171
211
|
);
|
|
172
212
|
}
|
|
@@ -228,7 +268,7 @@ export async function handleCommitAndPush(args, context) {
|
|
|
228
268
|
if (!pushed.ok) {
|
|
229
269
|
return textResponse(
|
|
230
270
|
`❌ Commit dibuat, tetapi TIDAK ter-push.\n` +
|
|
231
|
-
`Repo: ${
|
|
271
|
+
`Repo: ${repoName}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
|
|
232
272
|
`Sebab: ${pushed.detail}\n\n` +
|
|
233
273
|
`Commit lokal: ${generatedSummary}\n\n` +
|
|
234
274
|
`Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
|
|
@@ -242,7 +282,7 @@ export async function handleCommitAndPush(args, context) {
|
|
|
242
282
|
await workflowState.save();
|
|
243
283
|
|
|
244
284
|
return textResponse(
|
|
245
|
-
`✅ Commit and push completed!\nRepo: ${
|
|
285
|
+
`✅ Commit and push completed!\nRepo: ${repoName}\n\nCommit message: ${generatedSummary}\nPushed to: ${branchForPush} (verified at ${pushed.head.slice(0, 8)})\n\nNext: Run 'perform_release' to handle the release and push tags.`
|
|
246
286
|
);
|
|
247
287
|
}
|
|
248
288
|
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
import { textResponse, isReleaseSatisfied, resetToCommitIfWorkingChanges, summarizeHistory } from "./shared.js";
|
|
2
2
|
import { resolveProjectDir, setActiveProjectDir, getActiveProjectDir } from "../../exec.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Label repo yang dipakai, untuk ditampilkan di laporan.
|
|
6
|
+
*
|
|
7
|
+
* Sama seperti di commit-handlers: laporan harus menyebut repo yang benar-benar
|
|
8
|
+
* dipakai, bukan tebakan.
|
|
9
|
+
*/
|
|
10
|
+
function repoLabel() {
|
|
11
|
+
try {
|
|
12
|
+
const resolved = resolveProjectDir({
|
|
13
|
+
explicit: getActiveProjectDir(),
|
|
14
|
+
env: process.env,
|
|
15
|
+
candidates: [process.cwd(), process.env.INIT_CWD],
|
|
16
|
+
});
|
|
17
|
+
const origin = resolved.source === "env" ? " — dari WORKFLOW_PROJECT_DIR (global)" : "";
|
|
18
|
+
return resolved.dir + origin;
|
|
19
|
+
} catch {
|
|
20
|
+
return "(tidak bisa ditentukan)";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Menjalankan perintah git yang hasilnya hanya dipakai untuk laporan.
|
|
26
|
+
*
|
|
27
|
+
* Kegagalan di sini tidak boleh menggagalkan rilis: kalau `git describe`
|
|
28
|
+
* tidak menemukan tag, itu bukan alasan untuk berhenti. Yang penting adalah
|
|
29
|
+
* rilis tetap berjalan dan laporannya jujur tentang apa yang tidak diketahui.
|
|
30
|
+
*/
|
|
31
|
+
async function safeExecStdout(exec, command) {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await exec(command);
|
|
34
|
+
return String(stdout || "").trim();
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
4
40
|
export async function handleSkipRelease(args, workflowState) {
|
|
5
41
|
if (workflowState.state.currentPhase === "idle") {
|
|
6
42
|
return textResponse("⚠️ Please start a task first using 'start_task' before skipping release.");
|
|
@@ -185,11 +221,45 @@ export async function handlePerformRelease(args, context) {
|
|
|
185
221
|
);
|
|
186
222
|
}
|
|
187
223
|
|
|
224
|
+
// Tipe rilis WAJIB sah sebelum perintah disusun.
|
|
225
|
+
//
|
|
226
|
+
// 31 Oktober 2026: `determineReleaseTypeFromCommit` mengembalikan string kosong,
|
|
227
|
+
// lalu perintahnya menjadi `npm run release -- --release-as ` + "null" ->
|
|
228
|
+
// standard-version menulis `"version": null` dan membuat tag `vnull`, lalu
|
|
229
|
+
// keduanya di-push. Yang salah bukan hanya hasilnya: perintah yang mustahil
|
|
230
|
+
// itu tetap dijalankan tanpa perlawanan.
|
|
231
|
+
if (!recognizedTypes.has(releaseType)) {
|
|
232
|
+
return textResponse(
|
|
233
|
+
`⚠️ Tipe rilis tidak bisa ditentukan dari commit terakhir, jadi rilis TIDAK dijalankan.\n\n` +
|
|
234
|
+
`Commit terakhir: ${lastCommitMessage || "(kosong)"}\n\n` +
|
|
235
|
+
`Sebutkan tipe rilisnya secara eksplisit:\n` +
|
|
236
|
+
` • 'perform_release' dengan releaseType: "patch" | "minor" | "major"\n` +
|
|
237
|
+
` • atau command: "npm run release:patch"\n\n` +
|
|
238
|
+
`Tanpa itu perintahnya akan menjadi "--release-as null", yang menghasilkan tag vnull ` +
|
|
239
|
+
`dan package.json berversi null.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
const finalCommand =
|
|
189
244
|
releaseCommand.includes("--release-as") || /release:(major|minor|patch)/i.test(releaseCommand)
|
|
190
245
|
? releaseCommand
|
|
191
246
|
: `${releaseCommand} -- --release-as ${releaseType}`;
|
|
192
247
|
|
|
248
|
+
// Jaring pengaman terakhir: apa pun yang terjadi di atas, jangan pernah
|
|
249
|
+
// menjalankan perintah yang membawa tipe rilis kosong.
|
|
250
|
+
if (/--release-as\s+(?:null|undefined|)\s*$/i.test(finalCommand) || /\snull\b/i.test(finalCommand)) {
|
|
251
|
+
return textResponse(
|
|
252
|
+
`⚠️ Perintah rilis akan berisi tipe rilis yang tidak sah, jadi TIDAK dijalankan:\n\n` +
|
|
253
|
+
` ${finalCommand}\n\n` +
|
|
254
|
+
`Sebutkan tipe rilisnya: releaseType "patch" | "minor" | "major".`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Catat keadaan SEBELUM rilis supaya, kalau push gagal, kita bisa menyebut
|
|
259
|
+
// persis apa yang tertinggal di mesin.
|
|
260
|
+
const headBefore = await safeExecStdout(exec, "git rev-parse HEAD");
|
|
261
|
+
const tagBefore = await safeExecStdout(exec, "git describe --tags --abbrev=0");
|
|
262
|
+
|
|
193
263
|
try {
|
|
194
264
|
await exec(finalCommand);
|
|
195
265
|
} catch (error) {
|
|
@@ -198,6 +268,9 @@ export async function handlePerformRelease(args, context) {
|
|
|
198
268
|
);
|
|
199
269
|
}
|
|
200
270
|
|
|
271
|
+
const headAfter = await safeExecStdout(exec, "git rev-parse HEAD");
|
|
272
|
+
const tagAfter = await safeExecStdout(exec, "git describe --tags --abbrev=0");
|
|
273
|
+
|
|
201
274
|
const branchForPush = workflowState.state.lastPushBranch || (await git.getCurrentBranch()) || "";
|
|
202
275
|
const pushCommand = branchForPush
|
|
203
276
|
? `git push --follow-tags origin ${utils.shellEscape(branchForPush)}`
|
|
@@ -206,8 +279,29 @@ export async function handlePerformRelease(args, context) {
|
|
|
206
279
|
try {
|
|
207
280
|
await exec(pushCommand);
|
|
208
281
|
} catch (error) {
|
|
282
|
+
// Rilis SUDAH membuat commit dan tag di mesin; hanya push yang gagal.
|
|
283
|
+
// Tanpa menyebut yang tertinggal, tidak jelas apa yang harus dibereskan -
|
|
284
|
+
// dan pada 31 Oktober 2026 hal itu membuat rilis tampak selesai.
|
|
285
|
+
const leftBehind = [];
|
|
286
|
+
if (headAfter && headAfter !== headBefore) {
|
|
287
|
+
leftBehind.push(` • commit rilis ${headAfter.slice(0, 8)}`);
|
|
288
|
+
}
|
|
289
|
+
if (tagAfter && tagAfter !== tagBefore) {
|
|
290
|
+
leftBehind.push(` • tag ${tagAfter}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
209
293
|
return textResponse(
|
|
210
|
-
`❌
|
|
294
|
+
`❌ Push rilis GAGAL. Rilis TIDAK tercatat sebagai selesai.\n\n` +
|
|
295
|
+
`Repo: ${repoLabel()}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
|
|
296
|
+
`Penyebab: ${error.stderr || error.stdout || error.message}\n\n` +
|
|
297
|
+
(leftBehind.length > 0
|
|
298
|
+
? `Tertinggal di mesin (belum sampai ke remote):\n${leftBehind.join("\n")}\n\n`
|
|
299
|
+
: "") +
|
|
300
|
+
`Pemulihan:\n` +
|
|
301
|
+
` 1. periksa: git log --oneline -3 && git tag --points-at HEAD\n` +
|
|
302
|
+
` 2. kirim ulang: git push --follow-tags origin ${branchForPush || "<branch>"}\n` +
|
|
303
|
+
` 3. atau batalkan rilis lokal: git reset --hard HEAD~1 && git tag -d ${tagAfter || "<tag>"}\n\n` +
|
|
304
|
+
`Jangan tandai task selesai sebelum tag benar-benar ada di remote.`
|
|
211
305
|
);
|
|
212
306
|
}
|
|
213
307
|
|
package/workflow-state.js
CHANGED
|
@@ -400,6 +400,19 @@ async function pathExists(filePath) {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
async function ensureCompatibilityLinks(primaryPath) {
|
|
403
|
+
// Cermin kompatibilitas ditulis ke `<paket>/.state/workflow-state.json`,
|
|
404
|
+
// BUKAN ke lokasi state yang sebenarnya. Itu benar untuk pemakaian nyata
|
|
405
|
+
// (CLI dan dasbor web membaca jalur lama), tetapi merusak saat tes: setiap
|
|
406
|
+
// WorkflowState yang dibuat tes - meski di direktori sementara - menimpa
|
|
407
|
+
// state repo, dan 31 Oktober 2026 itu menghapus task yang sedang berjalan di
|
|
408
|
+
// tengah `npm test`.
|
|
409
|
+
//
|
|
410
|
+
// Tes mematikan cermin ini lewat DEV_WORKFLOW_NO_COMPAT_LINKS=1 (lihat
|
|
411
|
+
// tests/setup-env.js). Perilaku produksi tidak berubah.
|
|
412
|
+
if (process.env.DEV_WORKFLOW_NO_COMPAT_LINKS === "1") {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
403
416
|
const resolvedPrimary = path.resolve(primaryPath);
|
|
404
417
|
|
|
405
418
|
if (!(await pathExists(resolvedPrimary))) {
|