@programinglive/dev-workflow-mcp-server 1.1.7 → 1.1.16
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/.github/workflows/npm-publish.yml +47 -0
- package/CHANGELOG.md +81 -0
- package/README.md +144 -5
- package/db/index.js +204 -0
- package/docs/product/PRD.md +64 -0
- package/docs/web-dashboard.md +163 -0
- package/exec.js +46 -1
- package/index.js +81 -6
- package/install.js +5 -1
- package/package.json +19 -8
- package/release-wrapper.js +26 -1
- package/tests/build.test.js +37 -0
- package/tests/exec.test.js +35 -0
- package/tests/handlers.test.js +288 -0
- package/tests/release-wrapper.test.js +15 -5
- package/tests/workflowState.test.js +272 -1
- package/tools/definitions.js +103 -0
- package/tools/handlers.js +261 -8
- package/vite.config.js +43 -0
- package/web/index.html +747 -0
- package/web/server.js +90 -0
- package/workflow-state.js +502 -15
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Dev Workflow Web Dashboard
|
|
2
|
+
|
|
3
|
+
Learn how to start and use the Dev Workflow dashboard included with the MCP server. The dashboard exposes project history, summaries, and aggregates through a small Node.js service.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Before launching the dashboard, confirm the following:
|
|
8
|
+
|
|
9
|
+
- Node.js 18+ installed (same requirement as the MCP server)
|
|
10
|
+
- Project dependencies installed with `npm install`
|
|
11
|
+
- Workflow history stored under `.state/` (created automatically when using the MCP server)
|
|
12
|
+
|
|
13
|
+
> **Plesk users:** follow the [Plesk installation guide](../README.md#option-3-install-on-plesk-hosting) first and ensure SSH access is enabled.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Task history browser** – Paginated list of completed workflow entries.
|
|
18
|
+
- **Summary stats** – Counts of tasks by type and recent activity timeline.
|
|
19
|
+
- **Aggregated trends** – Daily, monthly, or yearly breakdowns of workflow activity.
|
|
20
|
+
- **Multi-user support** – Switch between user identifiers recorded in the `.state` directory.
|
|
21
|
+
|
|
22
|
+
## Getting Started
|
|
23
|
+
|
|
24
|
+
Launch the dashboard from your project directory:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run web
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When the server is ready you will see:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
🌐 Dev Workflow Dashboard running at http://localhost:3111
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open the URL in your browser. If port 3111 is in use, the script automatically selects a nearby free port.
|
|
37
|
+
|
|
38
|
+
### Choosing a Port
|
|
39
|
+
|
|
40
|
+
Set `DEV_WORKFLOW_WEB_PORT` to force a specific port:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
DEV_WORKFLOW_WEB_PORT=5000 npm run web
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Selecting a User
|
|
47
|
+
|
|
48
|
+
The dashboard defaults to the `default` user. Append `?user=<USER_ID>` to view another user’s data. User IDs map to the subdirectories under `.state/users/`.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
http://localhost:3111/?user=my-teammate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Navigation
|
|
55
|
+
|
|
56
|
+
The HTML frontend lives in `web/index.html`. The default UI includes:
|
|
57
|
+
|
|
58
|
+
- **Summary cards** – Total tasks completed and last active date.
|
|
59
|
+
- **Recent tasks table** – Paginated entries with task descriptions and timestamps.
|
|
60
|
+
- **Charts** (if script extensions are enabled) – Visual trends per frequency selection.
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
Interact with the dashboard programmatically using the JSON endpoints served by `web/server.js`.
|
|
65
|
+
|
|
66
|
+
### `GET /api/summary`
|
|
67
|
+
|
|
68
|
+
Returns aggregate statistics for a user.
|
|
69
|
+
|
|
70
|
+
**Query parameters**
|
|
71
|
+
|
|
72
|
+
- `user` *(optional, string)* – User identifier (default `default`).
|
|
73
|
+
|
|
74
|
+
**Response**
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"summary": {
|
|
79
|
+
"totalTasks": 12,
|
|
80
|
+
"taskTypes": { "feature": 8, "bugfix": 3, "refactor": 1 },
|
|
81
|
+
"lastActive": "2025-10-01T09:25:41.123Z",
|
|
82
|
+
"recentTasks": [
|
|
83
|
+
{ "description": "feat: add billing webhooks", "type": "feature", "timestamp": "2025-10-01T09:25:41.123Z" }
|
|
84
|
+
],
|
|
85
|
+
"updatedAt": "2025-10-01T09:30:00.000Z"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `GET /api/history`
|
|
91
|
+
|
|
92
|
+
Returns paginated task history.
|
|
93
|
+
|
|
94
|
+
**Query parameters**
|
|
95
|
+
|
|
96
|
+
- `user` *(optional, string)* – User identifier (default `default`).
|
|
97
|
+
- `page` *(optional, number)* – Page number (default `1`).
|
|
98
|
+
- `pageSize` *(optional, number)* – Items per page (default `20`, max `100`).
|
|
99
|
+
- `startDate` *(optional, string)* – Filter by ISO date (inclusive).
|
|
100
|
+
- `endDate` *(optional, string)* – Filter by ISO date (inclusive).
|
|
101
|
+
|
|
102
|
+
**Response**
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"entries": [
|
|
107
|
+
{
|
|
108
|
+
"id": 42,
|
|
109
|
+
"user_id": "default",
|
|
110
|
+
"task_description": "feat: add reporting endpoint",
|
|
111
|
+
"task_type": "feature",
|
|
112
|
+
"commit_message": "feat: add reporting endpoint",
|
|
113
|
+
"timestamp": "2025-09-30T09:18:00.000Z"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"total": 42,
|
|
117
|
+
"page": 1,
|
|
118
|
+
"pageSize": 20
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `GET /api/history-summary`
|
|
123
|
+
|
|
124
|
+
Returns an array of task counts grouped by period.
|
|
125
|
+
|
|
126
|
+
**Query parameters**
|
|
127
|
+
|
|
128
|
+
- `user` *(optional, string)* – User identifier (default `default`).
|
|
129
|
+
- `frequency` *(optional, string)* – `daily`, `monthly`, or `yearly` (default `daily`).
|
|
130
|
+
- `startDate`, `endDate` *(optional, string)* – Limit range by ISO dates.
|
|
131
|
+
|
|
132
|
+
**Response**
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"summary": [
|
|
137
|
+
{ "period": "2025-09-30", "count": 3 },
|
|
138
|
+
{ "period": "2025-09-29", "count": 1 }
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
| Variable | Description | Default |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `DEV_WORKFLOW_WEB_PORT` | Port for the dashboard | `3111` or next available |
|
|
148
|
+
| `DEV_WORKFLOW_USER_ID` | User identifier when launching via MCP | auto-generated |
|
|
149
|
+
| `DEV_WORKFLOW_STATE_FILE` | Overrides path to workflow state JSON | auto-detected |
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
| Symptom | Fix |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| Server fails with `better-sqlite3` build error | Install Python 3 and build tools (Windows) or ensure SQLite build dependencies exist (Linux/macOS). |
|
|
156
|
+
| Dashboard shows empty data | Confirm the MCP server has generated `.state/users/<user>/workflow-state.json` and that the chosen `user` matches. |
|
|
157
|
+
| Port already in use | Set `DEV_WORKFLOW_WEB_PORT` to a free port before running `npm run web`. |
|
|
158
|
+
|
|
159
|
+
## Related Topics
|
|
160
|
+
|
|
161
|
+
- [Main README](../README.md)
|
|
162
|
+
- [Workflow State Internals](./state.md) *(coming soon)*
|
|
163
|
+
- [CLI Tools](../README.md#🛠️-available-tools)
|
package/exec.js
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
import { exec as execCallback } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
2
5
|
import { promisify } from "util";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
const execPromise = promisify(execCallback);
|
|
8
|
+
|
|
9
|
+
function findRepoRoot(startDir) {
|
|
10
|
+
if (!startDir) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let current = path.resolve(startDir);
|
|
15
|
+
const { root } = path.parse(current);
|
|
16
|
+
|
|
17
|
+
while (true) {
|
|
18
|
+
if (existsSync(path.join(current, ".git"))) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (current === root) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parent = path.dirname(current);
|
|
27
|
+
if (parent === current) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
current = parent;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getDefaultCwd() {
|
|
36
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const candidateDirs = [process.cwd(), process.env.INIT_CWD, moduleDir];
|
|
38
|
+
return candidateDirs.reduce((resolved, candidate) => {
|
|
39
|
+
if (resolved) {
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
return findRepoRoot(candidate) || null;
|
|
43
|
+
}, null) || process.cwd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function exec(command, options = {}) {
|
|
47
|
+
const execOptions = options.cwd ? options : { ...options, cwd: getDefaultCwd() };
|
|
48
|
+
return execPromise(command, execOptions);
|
|
49
|
+
}
|
package/index.js
CHANGED
|
@@ -32,8 +32,7 @@ export { createCommitMessageParts, determineReleaseTypeFromCommit } from "./comm
|
|
|
32
32
|
export { containsTestFilesInStatus, workingTreeSummary } from "./git-helpers.js";
|
|
33
33
|
export { WorkflowState } from "./workflow-state.js";
|
|
34
34
|
|
|
35
|
-
export
|
|
36
|
-
await workflowState.load();
|
|
35
|
+
export let workflowState = null;
|
|
37
36
|
|
|
38
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
39
38
|
|
|
@@ -184,14 +183,90 @@ Use 'check_ready_to_commit' to verify workflow completion.
|
|
|
184
183
|
|
|
185
184
|
// Start server
|
|
186
185
|
export async function main() {
|
|
186
|
+
try {
|
|
187
|
+
// Initialize workflow state
|
|
188
|
+
workflowState = new WorkflowState();
|
|
189
|
+
await workflowState.load();
|
|
190
|
+
await workflowState.ensurePrimaryFile();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Warning: Failed to initialize workflow state:", error.message);
|
|
193
|
+
// Continue anyway - workflow state is optional for MCP to function
|
|
194
|
+
workflowState = new WorkflowState();
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
const transport = new StdioServerTransport();
|
|
188
198
|
await server.connect(transport);
|
|
189
199
|
console.error("Dev Workflow MCP Server running on stdio");
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
// --- Lightweight CLI: dev-workflow-mcp call <toolName> [--args '<json>'] ---
|
|
203
|
+
async function cliMain(argv) {
|
|
204
|
+
const [, , subcommand, toolName, ...rest] = argv;
|
|
205
|
+
if (subcommand !== "call") return false;
|
|
206
|
+
|
|
207
|
+
if (!toolName || typeof toolName !== "string" || toolName.trim() === "") {
|
|
208
|
+
console.error("Usage: dev-workflow-mcp call <toolName> [--args '<json>']");
|
|
209
|
+
process.exit(2);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Parse --args '<json>' if provided
|
|
213
|
+
let rawArgs = undefined;
|
|
214
|
+
for (let i = 0; i < rest.length; i++) {
|
|
215
|
+
if (rest[i] === "--args") {
|
|
216
|
+
rawArgs = rest[i + 1];
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Initialize workflow state similar to the server path
|
|
223
|
+
workflowState = new WorkflowState();
|
|
224
|
+
await workflowState.load();
|
|
225
|
+
await workflowState.ensurePrimaryFile();
|
|
226
|
+
|
|
227
|
+
const request = {
|
|
228
|
+
params: {
|
|
229
|
+
name: toolName,
|
|
230
|
+
arguments: rawArgs ?? {},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const response = await handleToolCall({
|
|
235
|
+
request,
|
|
236
|
+
normalizeRequestArgs,
|
|
237
|
+
workflowState,
|
|
238
|
+
exec,
|
|
239
|
+
git: {
|
|
240
|
+
hasWorkingChanges,
|
|
241
|
+
hasTestChanges,
|
|
242
|
+
hasStagedChanges,
|
|
243
|
+
getStagedChanges,
|
|
244
|
+
getCurrentBranch,
|
|
245
|
+
getLastCommitMessage,
|
|
246
|
+
workingTreeSummary,
|
|
247
|
+
},
|
|
248
|
+
utils: { shellEscape, determineReleaseTypeFromCommit, createCommitMessageParts },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const text = response?.content?.[0]?.text || "";
|
|
252
|
+
console.log(text);
|
|
253
|
+
const isError = /^\s*[⚠️❌]/u.test(text);
|
|
254
|
+
process.exit(isError ? 1 : 0);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error("CLI error:", error.message || String(error));
|
|
195
257
|
process.exit(1);
|
|
196
|
-
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const isDirectRun = process.argv[1] === __filename;
|
|
262
|
+
if (isDirectRun) {
|
|
263
|
+
// If invoked with a CLI subcommand, run it; otherwise start MCP server
|
|
264
|
+
if (process.argv[2] === "call") {
|
|
265
|
+
cliMain(process.argv);
|
|
266
|
+
} else {
|
|
267
|
+
main().catch((error) => {
|
|
268
|
+
console.error("Server error:", error);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
197
272
|
}
|
package/install.js
CHANGED
|
@@ -25,7 +25,11 @@ async function install() {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const stateDir = path.join(projectRoot, '.state');
|
|
29
|
+
const stateFile = path.join(stateDir, 'workflow-state.json');
|
|
30
|
+
|
|
31
|
+
// Create .state directory if it doesn't exist
|
|
32
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
29
33
|
|
|
30
34
|
// Check if state file already exists
|
|
31
35
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,16 +8,19 @@
|
|
|
8
8
|
"dev-workflow-mcp": "index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
+
"build": "vite build",
|
|
12
|
+
"dev": "node --watch index.js",
|
|
13
|
+
"start": "node index.js",
|
|
14
|
+
"local": "node index.js",
|
|
11
15
|
"test": "node --test",
|
|
12
16
|
"check": "node --check index.js",
|
|
13
|
-
"start": "node index.js",
|
|
14
|
-
"dev": "node --watch index.js",
|
|
15
17
|
"prepare": "husky",
|
|
16
18
|
"postinstall": "node install.js",
|
|
17
19
|
"release": "standard-version",
|
|
18
20
|
"release:major": "node release-wrapper.js major",
|
|
19
21
|
"release:minor": "node release-wrapper.js minor",
|
|
20
|
-
"release:patch": "node release-wrapper.js patch"
|
|
22
|
+
"release:patch": "node release-wrapper.js patch",
|
|
23
|
+
"web": "node web/server.js"
|
|
21
24
|
},
|
|
22
25
|
"keywords": [
|
|
23
26
|
"mcp",
|
|
@@ -27,8 +30,18 @@
|
|
|
27
30
|
],
|
|
28
31
|
"author": "",
|
|
29
32
|
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/programinglive/dev-workflow-mcp-server.git"
|
|
36
|
+
},
|
|
30
37
|
"dependencies": {
|
|
31
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
39
|
+
"better-sqlite3": "^11.8.0",
|
|
40
|
+
"esbuild": "^0.25.0",
|
|
41
|
+
"get-port": "^7.1.0",
|
|
42
|
+
"husky": "^9.1.7",
|
|
43
|
+
"standard-version": "^9.5.0",
|
|
44
|
+
"vite": "^7.1.12"
|
|
32
45
|
},
|
|
33
46
|
"publishConfig": {
|
|
34
47
|
"access": "public"
|
|
@@ -36,9 +49,7 @@
|
|
|
36
49
|
"devDependencies": {
|
|
37
50
|
"@commitlint/cli": "^20.1.0",
|
|
38
51
|
"@commitlint/config-conventional": "^20.0.0",
|
|
39
|
-
"@programinglive/commiter": "^1.
|
|
40
|
-
"husky": "^9.1.7",
|
|
41
|
-
"standard-version": "^9.5.0"
|
|
52
|
+
"@programinglive/commiter": "^1.1.0"
|
|
42
53
|
},
|
|
43
54
|
"standard-version": {
|
|
44
55
|
"releaseCommitMessageFormat": "chore(release): {{currentTag}} 🚀",
|
package/release-wrapper.js
CHANGED
|
@@ -22,7 +22,32 @@ async function main() {
|
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Prefer state under INIT_CWD to support per-project runs and tests.
|
|
26
|
+
// If DEV_WORKFLOW_USER_ID is set and a user-scoped file exists, use it; otherwise fall back to legacy path.
|
|
27
|
+
const baseStateDir = path.join(projectRoot, '.state');
|
|
28
|
+
const legacyStatePath = path.join(baseStateDir, 'workflow-state.json');
|
|
29
|
+
let resolvedUserId = (process.env.DEV_WORKFLOW_USER_ID || '').trim();
|
|
30
|
+
if (!resolvedUserId) {
|
|
31
|
+
try {
|
|
32
|
+
const fileUserId = (await fs.readFile(path.join(baseStateDir, 'user-id'), 'utf8')).trim();
|
|
33
|
+
if (fileUserId) {
|
|
34
|
+
resolvedUserId = fileUserId;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// no user-id file; legacy layout likely
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const userStatePath = resolvedUserId ? path.join(baseStateDir, 'users', resolvedUserId, 'workflow-state.json') : null;
|
|
41
|
+
|
|
42
|
+
let statePath = legacyStatePath;
|
|
43
|
+
if (userStatePath) {
|
|
44
|
+
try {
|
|
45
|
+
await fs.access(userStatePath);
|
|
46
|
+
statePath = userStatePath;
|
|
47
|
+
} catch {
|
|
48
|
+
// fall back to legacy path
|
|
49
|
+
}
|
|
50
|
+
}
|
|
26
51
|
let state;
|
|
27
52
|
|
|
28
53
|
try {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { readFile, access } from 'fs/promises';
|
|
4
|
+
import { exec as execCallback } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
const exec = promisify(execCallback);
|
|
9
|
+
|
|
10
|
+
test('build generates dist/index.mjs file', async () => {
|
|
11
|
+
const { stderr } = await exec('npm run build');
|
|
12
|
+
// Allow the known vite warning about node:process
|
|
13
|
+
const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
|
|
14
|
+
const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
|
|
15
|
+
assert(
|
|
16
|
+
actualStderr === '' || actualStderr.includes(knownWarning),
|
|
17
|
+
`Build should not have unexpected errors: ${actualStderr}`
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Verify the file exists
|
|
21
|
+
const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
|
|
22
|
+
await access(distPath);
|
|
23
|
+
|
|
24
|
+
// Verify it contains expected content
|
|
25
|
+
const content = await readFile(distPath, 'utf-8');
|
|
26
|
+
assert(content.includes('main'), 'Bundle should contain main function');
|
|
27
|
+
assert(content.length > 1000, 'Bundle should be substantial');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('build script exists in package.json', async () => {
|
|
31
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
32
|
+
const content = await readFile(packagePath, 'utf-8');
|
|
33
|
+
const pkg = JSON.parse(content);
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(typeof pkg.scripts.build, 'string');
|
|
36
|
+
assert(pkg.scripts.build.includes('vite build'));
|
|
37
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { exec } from '../exec.js';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
test('exec runs commands successfully', async () => {
|
|
9
|
+
const echoCommand = process.platform === 'win32' ? 'echo hello world' : 'echo "hello world"';
|
|
10
|
+
const { stdout } = await exec(echoCommand);
|
|
11
|
+
const normalized = stdout.trim().replace(/^"(.*)"$/, '$1');
|
|
12
|
+
assert.strictEqual(normalized, 'hello world');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('exec respects explicit cwd option', async () => {
|
|
16
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const dirCommand = process.platform === 'win32' ? 'cd' : 'pwd';
|
|
20
|
+
const { stdout } = await exec(dirCommand, { cwd: tempDir });
|
|
21
|
+
// Just verify it runs in the specified directory (path may vary by platform)
|
|
22
|
+
assert(stdout.trim().toLowerCase().includes('mcp-test-'));
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('exec handles command errors', async () => {
|
|
29
|
+
try {
|
|
30
|
+
await exec('command-that-does-not-exist');
|
|
31
|
+
assert.fail('Should have thrown an error');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
assert(error.code !== undefined || error.message.includes('command-not-found'));
|
|
34
|
+
}
|
|
35
|
+
});
|