@mandujs/mcp 0.18.9 → 0.19.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/README.md +0 -1
- package/package.json +2 -2
- package/src/activity-adapter.ts +23 -0
- package/src/activity-monitor.ts +9 -8
- package/src/adapters/tool-adapter.ts +2 -0
- package/src/executor/error-handler.ts +268 -250
- package/src/index.ts +8 -0
- package/src/new-resources.ts +119 -0
- package/src/profiles.ts +34 -0
- package/src/prompts.ts +104 -0
- package/src/resources/handlers.ts +0 -23
- package/src/server.ts +78 -5
- package/src/tools/ate.ts +28 -0
- package/src/tools/brain.ts +56 -24
- package/src/tools/component.ts +194 -185
- package/src/tools/composite.ts +440 -0
- package/src/tools/contract.ts +58 -58
- package/src/tools/decisions.ts +270 -0
- package/src/tools/generate.ts +23 -21
- package/src/tools/guard.ts +32 -708
- package/src/tools/history.ts +24 -7
- package/src/tools/hydration.ts +40 -13
- package/src/tools/index.ts +28 -2
- package/src/tools/kitchen.ts +107 -0
- package/src/tools/negotiate.ts +263 -0
- package/src/tools/project.ts +464 -382
- package/src/tools/resource.ts +19 -2
- package/src/tools/runtime.ts +533 -508
- package/src/tools/seo.ts +446 -417
- package/src/tools/slot-validation.ts +200 -0
- package/src/tools/slot.ts +20 -25
- package/src/tools/spec.ts +45 -43
- package/src/tools/transaction.ts +55 -13
- package/src/tx-lock.ts +73 -0
- package/src/utils/project.ts +48 -9
- package/src/utils/runtime-control.ts +52 -0
- package/src/utils/withWarnings.ts +2 -1
package/src/tools/project.ts
CHANGED
|
@@ -1,382 +1,464 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu MCP - Project Tools
|
|
3
|
-
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
-
import type { ActivityMonitor } from "../activity-monitor.js";
|
|
12
|
-
import { spawn, type Subprocess } from "bun";
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
type: "
|
|
142
|
-
description: "
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
type: "
|
|
146
|
-
description: "
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
type: "
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
type: "
|
|
164
|
-
description: "
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Mandu MCP - Project Tools
|
|
3
|
+
*
|
|
4
|
+
* - mandu.project.init: Create a new Mandu project (init + optional install)
|
|
5
|
+
* - mandu.dev.start: Start dev server
|
|
6
|
+
* - mandu.dev.stop: Stop dev server
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import type { ActivityMonitor } from "../activity-monitor.js";
|
|
12
|
+
import { spawn, type Subprocess } from "bun";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import fs from "fs/promises";
|
|
16
|
+
|
|
17
|
+
type DevServerState = {
|
|
18
|
+
process: Subprocess;
|
|
19
|
+
cwd: string;
|
|
20
|
+
startedAt: Date;
|
|
21
|
+
output: string[];
|
|
22
|
+
maxLines: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let devServerState: DevServerState | null = null;
|
|
26
|
+
let devServerStarting = false;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the current dev server state.
|
|
30
|
+
* Used by other tools (e.g. kitchen) to discover the running server's port/output.
|
|
31
|
+
*/
|
|
32
|
+
export function getDevServerState(): { cwd: string; output: string[]; startedAt: Date } | null {
|
|
33
|
+
if (!devServerState) return null;
|
|
34
|
+
return {
|
|
35
|
+
cwd: devServerState.cwd,
|
|
36
|
+
output: [...devServerState.output],
|
|
37
|
+
startedAt: devServerState.startedAt,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trimOutput(text: string, maxChars: number = 4000): string {
|
|
42
|
+
if (text.length <= maxChars) return text;
|
|
43
|
+
return text.slice(-maxChars);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const COMMAND_TIMEOUT_MS = 120_000; // 2 minutes
|
|
47
|
+
|
|
48
|
+
async function runCommand(cmd: string[], cwd: string, timeoutMs: number = COMMAND_TIMEOUT_MS) {
|
|
49
|
+
const proc = spawn(cmd, {
|
|
50
|
+
cwd,
|
|
51
|
+
stdout: "pipe",
|
|
52
|
+
stderr: "pipe",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
proc.kill();
|
|
58
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms: ${cmd.join(" ")}`));
|
|
59
|
+
}, timeoutMs)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const [stdout, stderr, exitCode] = await Promise.race([
|
|
63
|
+
Promise.all([
|
|
64
|
+
new Response(proc.stdout).text(),
|
|
65
|
+
new Response(proc.stderr).text(),
|
|
66
|
+
proc.exited,
|
|
67
|
+
]),
|
|
68
|
+
timeoutPromise,
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
exitCode,
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function consumeStream(
|
|
79
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
80
|
+
state: DevServerState,
|
|
81
|
+
label: "stdout" | "stderr",
|
|
82
|
+
server?: Server
|
|
83
|
+
) {
|
|
84
|
+
if (!stream) return;
|
|
85
|
+
const reader = stream.getReader();
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
let buffer = "";
|
|
88
|
+
|
|
89
|
+
while (true) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done) break;
|
|
92
|
+
buffer += decoder.decode(value, { stream: true });
|
|
93
|
+
const lines = buffer.split(/\r?\n/);
|
|
94
|
+
buffer = lines.pop() ?? "";
|
|
95
|
+
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const text = line.trim();
|
|
98
|
+
if (!text) continue;
|
|
99
|
+
const entry = `[${label}] ${text}`;
|
|
100
|
+
state.output.push(entry);
|
|
101
|
+
if (state.output.length > state.maxLines) {
|
|
102
|
+
state.output.shift();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (server) {
|
|
106
|
+
server.sendLoggingMessage({
|
|
107
|
+
level: label === "stderr" ? "warning" : "info",
|
|
108
|
+
logger: "mandu-dev",
|
|
109
|
+
data: {
|
|
110
|
+
type: "dev_log",
|
|
111
|
+
stream: label,
|
|
112
|
+
message: text,
|
|
113
|
+
},
|
|
114
|
+
}).catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (buffer.trim()) {
|
|
120
|
+
const entry = `[${label}] ${buffer.trim()}`;
|
|
121
|
+
state.output.push(entry);
|
|
122
|
+
if (state.output.length > state.maxLines) {
|
|
123
|
+
state.output.shift();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const projectToolDefinitions: Tool[] = [
|
|
129
|
+
{
|
|
130
|
+
name: "mandu.project.init",
|
|
131
|
+
description:
|
|
132
|
+
"Initialize a new Mandu project (runs `mandu init` and optionally `bun install`).",
|
|
133
|
+
annotations: {
|
|
134
|
+
destructiveHint: true,
|
|
135
|
+
readOnlyHint: false,
|
|
136
|
+
},
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
name: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Project name (directory name)",
|
|
143
|
+
},
|
|
144
|
+
parentDir: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "Parent directory to create the project in (default: cwd)",
|
|
147
|
+
},
|
|
148
|
+
css: {
|
|
149
|
+
type: "string",
|
|
150
|
+
enum: ["tailwind", "panda", "none"],
|
|
151
|
+
description: "CSS framework (default: tailwind)",
|
|
152
|
+
},
|
|
153
|
+
ui: {
|
|
154
|
+
type: "string",
|
|
155
|
+
enum: ["shadcn", "ark", "none"],
|
|
156
|
+
description: "UI library (default: shadcn)",
|
|
157
|
+
},
|
|
158
|
+
theme: {
|
|
159
|
+
type: "boolean",
|
|
160
|
+
description: "Enable dark mode theme system",
|
|
161
|
+
},
|
|
162
|
+
minimal: {
|
|
163
|
+
type: "boolean",
|
|
164
|
+
description: "Create minimal template (no CSS/UI)",
|
|
165
|
+
},
|
|
166
|
+
install: {
|
|
167
|
+
type: "boolean",
|
|
168
|
+
description: "Run bun install after init (default: true)",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ["name"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "mandu.dev.start",
|
|
176
|
+
description: "Start Mandu dev server (bun run dev).",
|
|
177
|
+
annotations: {
|
|
178
|
+
readOnlyHint: false,
|
|
179
|
+
},
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
cwd: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description: "Project directory to run dev server in (default: current project)",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
required: [],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "mandu.dev.stop",
|
|
193
|
+
description: "Stop Mandu dev server if running.",
|
|
194
|
+
annotations: {
|
|
195
|
+
destructiveHint: true,
|
|
196
|
+
readOnlyHint: false,
|
|
197
|
+
},
|
|
198
|
+
inputSchema: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {},
|
|
201
|
+
required: [],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
export function projectTools(projectRoot: string, server?: Server, monitor?: ActivityMonitor) {
|
|
207
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
208
|
+
"mandu.project.init": async (args: Record<string, unknown>) => {
|
|
209
|
+
const {
|
|
210
|
+
name,
|
|
211
|
+
parentDir,
|
|
212
|
+
css,
|
|
213
|
+
ui,
|
|
214
|
+
theme,
|
|
215
|
+
minimal,
|
|
216
|
+
install = true,
|
|
217
|
+
} = args as {
|
|
218
|
+
name: string;
|
|
219
|
+
parentDir?: string;
|
|
220
|
+
css?: "tailwind" | "panda" | "none";
|
|
221
|
+
ui?: "shadcn" | "ark" | "none";
|
|
222
|
+
theme?: boolean;
|
|
223
|
+
minimal?: boolean;
|
|
224
|
+
install?: boolean;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const baseDir = parentDir
|
|
228
|
+
? path.resolve(projectRoot, parentDir)
|
|
229
|
+
: projectRoot;
|
|
230
|
+
|
|
231
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
232
|
+
|
|
233
|
+
// Runtime whitelist validation for spawn arguments
|
|
234
|
+
const VALID_CSS = ["tailwind", "panda", "none"];
|
|
235
|
+
const VALID_UI = ["shadcn", "ark", "none"];
|
|
236
|
+
if (css !== undefined && !VALID_CSS.includes(css)) {
|
|
237
|
+
return { success: false, error: `Invalid css value: ${css}. Must be one of: ${VALID_CSS.join(", ")}` };
|
|
238
|
+
}
|
|
239
|
+
if (ui !== undefined && !VALID_UI.includes(ui)) {
|
|
240
|
+
return { success: false, error: `Invalid ui value: ${ui}. Must be one of: ${VALID_UI.join(", ")}` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const initArgs = ["@mandujs/cli", "init", name];
|
|
244
|
+
if (minimal) {
|
|
245
|
+
initArgs.push("--minimal");
|
|
246
|
+
} else {
|
|
247
|
+
if (css) initArgs.push("--css", css);
|
|
248
|
+
if (ui) initArgs.push("--ui", ui);
|
|
249
|
+
if (theme) initArgs.push("--theme");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let initResult: { exitCode: number | null; stdout: string; stderr: string };
|
|
253
|
+
try {
|
|
254
|
+
initResult = await runCommand(["bunx", ...initArgs], baseDir);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
step: "init",
|
|
259
|
+
error: err instanceof Error ? err.message : String(err),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (initResult.exitCode !== 0) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
step: "init",
|
|
266
|
+
exitCode: initResult.exitCode,
|
|
267
|
+
stdout: trimOutput(initResult.stdout),
|
|
268
|
+
stderr: trimOutput(initResult.stderr),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const projectDir = path.join(baseDir, name);
|
|
273
|
+
|
|
274
|
+
let installResult: { exitCode: number | null; stdout: string; stderr: string } | null = null;
|
|
275
|
+
if (install !== false) {
|
|
276
|
+
try {
|
|
277
|
+
installResult = await runCommand(["bun", "install"], projectDir);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
step: "install",
|
|
282
|
+
projectDir,
|
|
283
|
+
error: err instanceof Error ? err.message : String(err),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (installResult.exitCode !== 0) {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
step: "install",
|
|
290
|
+
projectDir,
|
|
291
|
+
exitCode: installResult.exitCode,
|
|
292
|
+
stdout: trimOutput(installResult.stdout),
|
|
293
|
+
stderr: trimOutput(installResult.stderr),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
projectDir,
|
|
301
|
+
installed: install !== false,
|
|
302
|
+
init: {
|
|
303
|
+
exitCode: initResult.exitCode,
|
|
304
|
+
stdout: trimOutput(initResult.stdout),
|
|
305
|
+
stderr: trimOutput(initResult.stderr),
|
|
306
|
+
},
|
|
307
|
+
install: installResult
|
|
308
|
+
? {
|
|
309
|
+
exitCode: installResult.exitCode,
|
|
310
|
+
stdout: trimOutput(installResult.stdout),
|
|
311
|
+
stderr: trimOutput(installResult.stderr),
|
|
312
|
+
}
|
|
313
|
+
: null,
|
|
314
|
+
next: install !== false
|
|
315
|
+
? ["cd " + name, "bun run dev"]
|
|
316
|
+
: ["cd " + name, "bun install", "bun run dev"],
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
"mandu.dev.start": async (args: Record<string, unknown>) => {
|
|
321
|
+
const { cwd } = args as { cwd?: string };
|
|
322
|
+
if (devServerState || devServerStarting) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
message: devServerStarting
|
|
326
|
+
? "Dev server is starting up, please wait"
|
|
327
|
+
: "Dev server is already running",
|
|
328
|
+
pid: devServerState?.process.pid,
|
|
329
|
+
cwd: devServerState?.cwd,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
devServerStarting = true;
|
|
334
|
+
try {
|
|
335
|
+
const targetDir = cwd ? path.resolve(projectRoot, cwd) : projectRoot;
|
|
336
|
+
|
|
337
|
+
const proc = spawn(["bun", "run", "dev"], {
|
|
338
|
+
cwd: targetDir,
|
|
339
|
+
stdout: "pipe",
|
|
340
|
+
stderr: "pipe",
|
|
341
|
+
stdin: "ignore",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const state: DevServerState = {
|
|
345
|
+
process: proc,
|
|
346
|
+
cwd: targetDir,
|
|
347
|
+
startedAt: new Date(),
|
|
348
|
+
output: [],
|
|
349
|
+
maxLines: 50,
|
|
350
|
+
};
|
|
351
|
+
devServerState = state;
|
|
352
|
+
|
|
353
|
+
// Wait for the server to output its port before returning
|
|
354
|
+
const portPromise = new Promise<{ port: number; url: string } | null>((resolve) => {
|
|
355
|
+
const PORT_DETECT_TIMEOUT_MS = 15_000;
|
|
356
|
+
const timeout = setTimeout(() => resolve(null), PORT_DETECT_TIMEOUT_MS);
|
|
357
|
+
const portPattern = /https?:\/\/[^:\s]+:(\d+)/;
|
|
358
|
+
|
|
359
|
+
const originalPush = state.output.push.bind(state.output);
|
|
360
|
+
state.output.push = (...items: string[]) => {
|
|
361
|
+
const result = originalPush(...items);
|
|
362
|
+
for (const item of items) {
|
|
363
|
+
const match = item.match(portPattern);
|
|
364
|
+
if (match) {
|
|
365
|
+
const detectedPort = parseInt(match[1], 10);
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
state.output.push = originalPush;
|
|
368
|
+
resolve({
|
|
369
|
+
port: detectedPort,
|
|
370
|
+
url: match[0],
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
consumeStream(proc.stdout, state, "stdout", server).catch(() => {});
|
|
380
|
+
consumeStream(proc.stderr, state, "stderr", server).catch(() => {});
|
|
381
|
+
|
|
382
|
+
proc.exited.then(() => {
|
|
383
|
+
if (devServerState?.process === proc) {
|
|
384
|
+
devServerState = null;
|
|
385
|
+
}
|
|
386
|
+
}).catch(() => {});
|
|
387
|
+
|
|
388
|
+
if (monitor) {
|
|
389
|
+
monitor.logEvent("dev", `Dev server started (${targetDir})`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Wait for port detection (with timeout fallback)
|
|
393
|
+
const detected = await portPromise;
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
success: true,
|
|
397
|
+
pid: proc.pid,
|
|
398
|
+
port: detected?.port ?? null,
|
|
399
|
+
url: detected?.url ?? null,
|
|
400
|
+
cwd: targetDir,
|
|
401
|
+
startedAt: state.startedAt.toISOString(),
|
|
402
|
+
message: detected
|
|
403
|
+
? `Dev server started on port ${detected.port}`
|
|
404
|
+
: "Dev server started (port detection timed out)",
|
|
405
|
+
};
|
|
406
|
+
} finally {
|
|
407
|
+
devServerStarting = false;
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
"mandu.dev.stop": async () => {
|
|
412
|
+
if (!devServerState) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
message: "Dev server is not running",
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const { process: proc, cwd, output } = devServerState;
|
|
420
|
+
const pid = proc.pid;
|
|
421
|
+
devServerState = null;
|
|
422
|
+
|
|
423
|
+
// Kill the entire process tree to prevent zombie processes
|
|
424
|
+
try {
|
|
425
|
+
if (pid) {
|
|
426
|
+
if (process.platform === "win32") {
|
|
427
|
+
// Windows: taskkill /T kills the process tree, /F forces termination
|
|
428
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
|
429
|
+
} else {
|
|
430
|
+
// Unix: kill the entire process group (negative PID)
|
|
431
|
+
try {
|
|
432
|
+
process.kill(-pid, "SIGKILL");
|
|
433
|
+
} catch {
|
|
434
|
+
// Fallback: kill just the process if process group kill fails
|
|
435
|
+
proc.kill("SIGKILL");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
proc.kill();
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
// Process may have already exited
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (monitor) {
|
|
446
|
+
monitor.logEvent("dev", `Dev server stopped (${cwd})`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
success: true,
|
|
451
|
+
message: "Dev server stopped",
|
|
452
|
+
cwd,
|
|
453
|
+
tail: output.slice(-10),
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Backward-compatible aliases (deprecated)
|
|
459
|
+
handlers["mandu_init"] = handlers["mandu.project.init"];
|
|
460
|
+
handlers["mandu_dev_start"] = handlers["mandu.dev.start"];
|
|
461
|
+
handlers["mandu_dev_stop"] = handlers["mandu.dev.stop"];
|
|
462
|
+
|
|
463
|
+
return handlers;
|
|
464
|
+
}
|