@mandujs/mcp 0.18.3 → 0.18.5
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/package.json +2 -2
- package/src/activity-monitor.ts +11 -14
- package/src/server.ts +14 -5
- package/src/tools/brain.ts +1 -1
- package/src/tools/component.ts +185 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/project.ts +92 -44
- package/src/utils/project.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mandujs/core": "^0.18.
|
|
35
|
+
"@mandujs/core": "^0.18.8",
|
|
36
36
|
"@mandujs/ate": "^0.17.0",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
38
38
|
},
|
package/src/activity-monitor.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
-
import {
|
|
10
|
+
import type { Subprocess } from "bun";
|
|
11
11
|
|
|
12
12
|
const TOOL_ICONS: Record<string, string> = {
|
|
13
13
|
// Spec
|
|
@@ -266,7 +266,7 @@ function writeDefaultConfig(projectRoot: string, config: Required<MonitorConfig>
|
|
|
266
266
|
export class ActivityMonitor {
|
|
267
267
|
private logFile = "";
|
|
268
268
|
private logStream: fs.WriteStream | null = null;
|
|
269
|
-
private tailProcess:
|
|
269
|
+
private tailProcess: Subprocess | null = null;
|
|
270
270
|
private projectRoot: string;
|
|
271
271
|
private config: Required<MonitorConfig>;
|
|
272
272
|
private outputFormat: MonitorOutputFormat;
|
|
@@ -813,9 +813,9 @@ export class ActivityMonitor {
|
|
|
813
813
|
private openTerminal(): void {
|
|
814
814
|
try {
|
|
815
815
|
if (process.platform === "win32") {
|
|
816
|
-
this.tailProcess = spawn(
|
|
817
|
-
"cmd",
|
|
816
|
+
this.tailProcess = Bun.spawn(
|
|
818
817
|
[
|
|
818
|
+
"cmd",
|
|
819
819
|
"/c",
|
|
820
820
|
"start",
|
|
821
821
|
"Mandu Activity Monitor",
|
|
@@ -824,22 +824,19 @@ export class ActivityMonitor {
|
|
|
824
824
|
"-Command",
|
|
825
825
|
`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; chcp 65001 | Out-Null; Get-Content '${this.logFile}' -Wait -Encoding UTF8`,
|
|
826
826
|
],
|
|
827
|
-
{ cwd: this.projectRoot,
|
|
827
|
+
{ cwd: this.projectRoot, stdio: ["ignore", "ignore", "ignore"] }
|
|
828
828
|
);
|
|
829
829
|
} else if (process.platform === "darwin") {
|
|
830
|
-
this.tailProcess = spawn(
|
|
831
|
-
"osascript",
|
|
832
|
-
["
|
|
833
|
-
{ detached: true, stdio: "ignore" }
|
|
830
|
+
this.tailProcess = Bun.spawn(
|
|
831
|
+
["osascript", "-e", `tell application "Terminal" to do script "tail -f '${this.logFile}'"`],
|
|
832
|
+
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
834
833
|
);
|
|
835
834
|
} else {
|
|
836
|
-
this.tailProcess = spawn(
|
|
837
|
-
"x-terminal-emulator",
|
|
838
|
-
["
|
|
839
|
-
{ cwd: this.projectRoot, detached: true, stdio: "ignore" }
|
|
835
|
+
this.tailProcess = Bun.spawn(
|
|
836
|
+
["x-terminal-emulator", "-e", `tail -f '${this.logFile}'`],
|
|
837
|
+
{ cwd: this.projectRoot, stdio: ["ignore", "ignore", "ignore"] }
|
|
840
838
|
);
|
|
841
839
|
}
|
|
842
|
-
this.tailProcess?.unref();
|
|
843
840
|
} catch {
|
|
844
841
|
// Terminal auto-open failed silently
|
|
845
842
|
}
|
package/src/server.ts
CHANGED
|
@@ -322,7 +322,10 @@ export class ManduMcpServer {
|
|
|
322
322
|
* 리소스 패턴 매칭
|
|
323
323
|
*/
|
|
324
324
|
function matchResourcePattern(pattern: string, uri: string): boolean {
|
|
325
|
-
const regexPattern = pattern
|
|
325
|
+
const regexPattern = pattern
|
|
326
|
+
.split(/\{[^}]+\}/)
|
|
327
|
+
.map(part => part.replace(/[.+*?^${}()|[\]\\]/g, "\\$&"))
|
|
328
|
+
.join("([^/]+)");
|
|
326
329
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
327
330
|
return regex.test(uri);
|
|
328
331
|
}
|
|
@@ -332,10 +335,16 @@ function matchResourcePattern(pattern: string, uri: string): boolean {
|
|
|
332
335
|
*/
|
|
333
336
|
function extractResourceParams(pattern: string, uri: string): Record<string, string> {
|
|
334
337
|
const paramNames: string[] = [];
|
|
335
|
-
const regexPattern = pattern
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
const regexPattern = pattern
|
|
339
|
+
.split(/\{([^}]+)\}/)
|
|
340
|
+
.map((part, index) => {
|
|
341
|
+
if (index % 2 === 1) {
|
|
342
|
+
paramNames.push(part);
|
|
343
|
+
return "([^/]+)";
|
|
344
|
+
}
|
|
345
|
+
return part.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
|
346
|
+
})
|
|
347
|
+
.join("");
|
|
339
348
|
|
|
340
349
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
341
350
|
const match = uri.match(regex);
|
package/src/tools/brain.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
generateJsonStatus,
|
|
27
27
|
initializeArchitectureAnalyzer,
|
|
28
28
|
getArchitectureAnalyzer,
|
|
29
|
-
} from "
|
|
29
|
+
} from "@mandujs/core";
|
|
30
30
|
import { getProjectPaths } from "../utils/project.js";
|
|
31
31
|
|
|
32
32
|
export const brainToolDefinitions: Tool[] = [
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
|
|
5
|
+
export const componentToolDefinitions: Tool[] = [
|
|
6
|
+
{
|
|
7
|
+
name: "mandu_add_component",
|
|
8
|
+
description:
|
|
9
|
+
"Scaffold a new client-side component in the correct FSD (Feature-Sliced Design) layer. " +
|
|
10
|
+
"Mandu projects organize client components under src/client/ following FSD layers: " +
|
|
11
|
+
"shared (reusable primitives), entities (domain objects), features (user interactions), " +
|
|
12
|
+
"widgets (composite blocks), pages (page-level controllers). " +
|
|
13
|
+
"Creates the component file and updates the layer's public API index.ts. " +
|
|
14
|
+
"Use this instead of manually creating files in app/ to maintain FSD architecture.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
name: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Component name in PascalCase (e.g., 'ReactionBar', 'UserAvatar')",
|
|
21
|
+
},
|
|
22
|
+
layer: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: ["shared", "entities", "features", "widgets", "pages"],
|
|
25
|
+
description:
|
|
26
|
+
"FSD layer: " +
|
|
27
|
+
"'shared' (reusable UI primitives, utils — no business logic), " +
|
|
28
|
+
"'entities' (domain models and their UI — User, Message, Post), " +
|
|
29
|
+
"'features' (user interactions that change state — like, comment, follow), " +
|
|
30
|
+
"'widgets' (composite sections combining entities+features), " +
|
|
31
|
+
"'pages' (page-level client components — use sparingly, prefer features/entities)",
|
|
32
|
+
},
|
|
33
|
+
slice: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description:
|
|
36
|
+
"Feature slice name in kebab-case (required for features/entities/widgets). " +
|
|
37
|
+
"Examples: 'chat-reaction', 'user-profile', 'post-feed'. " +
|
|
38
|
+
"For 'shared' layer, use segment name like 'ui', 'lib', 'api'.",
|
|
39
|
+
},
|
|
40
|
+
segment: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: ["ui", "model", "api", "lib", "config"],
|
|
43
|
+
description: "Segment within the slice (default: 'ui'). 'ui' for React components, 'model' for hooks/store, 'api' for data fetching.",
|
|
44
|
+
},
|
|
45
|
+
description: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Brief description of what this component does (added as a comment)",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ["name", "layer"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function toKebabCase(name: string): string {
|
|
56
|
+
return name
|
|
57
|
+
.replace(/([A-Z])/g, "-$1")
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/^-/, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function componentTools(projectRoot: string) {
|
|
63
|
+
return {
|
|
64
|
+
mandu_add_component: async (args: Record<string, unknown>) => {
|
|
65
|
+
const {
|
|
66
|
+
name,
|
|
67
|
+
layer,
|
|
68
|
+
slice,
|
|
69
|
+
segment = "ui",
|
|
70
|
+
description = "",
|
|
71
|
+
} = args as {
|
|
72
|
+
name: string;
|
|
73
|
+
layer: "shared" | "entities" | "features" | "widgets" | "pages";
|
|
74
|
+
slice?: string;
|
|
75
|
+
segment?: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Validate: features/entities/widgets require a slice
|
|
80
|
+
if (["features", "entities", "widgets"].includes(layer) && !slice) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: `The '${layer}' layer requires a 'slice' name (e.g., 'chat-reaction', 'user-profile').`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build the file path
|
|
88
|
+
const clientBase = path.join(projectRoot, "src", "client");
|
|
89
|
+
let componentDir: string;
|
|
90
|
+
let indexPath: string;
|
|
91
|
+
|
|
92
|
+
if (layer === "shared") {
|
|
93
|
+
const sliceName = slice || "ui";
|
|
94
|
+
componentDir = path.join(clientBase, "shared", sliceName);
|
|
95
|
+
indexPath = path.join(clientBase, "shared", sliceName, "index.ts");
|
|
96
|
+
} else if (layer === "pages") {
|
|
97
|
+
componentDir = path.join(clientBase, "pages");
|
|
98
|
+
indexPath = path.join(clientBase, "pages", "index.ts");
|
|
99
|
+
} else {
|
|
100
|
+
const sliceName = slice!;
|
|
101
|
+
componentDir = path.join(clientBase, layer, sliceName, segment);
|
|
102
|
+
indexPath = path.join(clientBase, layer, sliceName, "index.ts");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const kebabName = toKebabCase(name);
|
|
106
|
+
const componentFile = path.join(componentDir, `${kebabName}.tsx`);
|
|
107
|
+
const relativePath = path.relative(projectRoot, componentFile).replace(/\\/g, "/");
|
|
108
|
+
|
|
109
|
+
// Check if file already exists
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(componentFile);
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: `Component file already exists: ${relativePath}`,
|
|
115
|
+
};
|
|
116
|
+
} catch {
|
|
117
|
+
// Good - file doesn't exist
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create directory
|
|
121
|
+
await fs.mkdir(componentDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
// Generate component template
|
|
124
|
+
const descComment = description ? `\n * ${description}` : "";
|
|
125
|
+
const template = `/**
|
|
126
|
+
* ${name} Component${descComment}
|
|
127
|
+
* Layer: ${layer}${slice ? ` / ${slice}` : ""}
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
import { useState } from "react";
|
|
131
|
+
|
|
132
|
+
interface ${name}Props {
|
|
133
|
+
// TODO: Define props
|
|
134
|
+
className?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function ${name}({ className }: ${name}Props) {
|
|
138
|
+
return (
|
|
139
|
+
<div className={className}>
|
|
140
|
+
{/* TODO: Implement ${name} */}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
await fs.writeFile(componentFile, template, "utf-8");
|
|
147
|
+
|
|
148
|
+
// Update index.ts (create or append export)
|
|
149
|
+
const exportLine = `export { ${name} } from "./${segment}/${kebabName}.js";\n`;
|
|
150
|
+
const simpleExportLine = `export { ${name} } from "./${kebabName}.js";\n`;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
let indexContent = "";
|
|
154
|
+
try {
|
|
155
|
+
indexContent = await fs.readFile(indexPath, "utf-8");
|
|
156
|
+
} catch {
|
|
157
|
+
// index.ts doesn't exist yet
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const exportToAdd = layer === "shared" || layer === "pages" ? simpleExportLine : exportLine;
|
|
161
|
+
|
|
162
|
+
if (!indexContent.includes(`{ ${name} }`)) {
|
|
163
|
+
await fs.writeFile(indexPath, indexContent + exportToAdd, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// index update failed - not critical
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
component: name,
|
|
172
|
+
layer,
|
|
173
|
+
slice: slice || null,
|
|
174
|
+
segment,
|
|
175
|
+
createdFiles: [relativePath],
|
|
176
|
+
updatedFiles: [path.relative(projectRoot, indexPath).replace(/\\/g, "/")],
|
|
177
|
+
message: `Created ${name} in ${layer}${slice ? `/${slice}` : ""}/${segment}`,
|
|
178
|
+
nextSteps: [
|
|
179
|
+
`Edit ${relativePath} to implement the component`,
|
|
180
|
+
`Import with: import { ${name} } from "@/client/${layer}${slice ? `/${slice}` : ""}"`,
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { seoTools, seoToolDefinitions } from "./seo.js";
|
|
|
24
24
|
export { projectTools, projectToolDefinitions } from "./project.js";
|
|
25
25
|
export { ateTools, ateToolDefinitions } from "./ate.js";
|
|
26
26
|
export { resourceTools, resourceToolDefinitions } from "./resource.js";
|
|
27
|
+
export { componentTools, componentToolDefinitions } from "./component.js";
|
|
27
28
|
|
|
28
29
|
// 도구 모듈 import (등록용)
|
|
29
30
|
import { specTools, specToolDefinitions } from "./spec.js";
|
|
@@ -40,6 +41,7 @@ import { seoTools, seoToolDefinitions } from "./seo.js";
|
|
|
40
41
|
import { projectTools, projectToolDefinitions } from "./project.js";
|
|
41
42
|
import { ateTools, ateToolDefinitions } from "./ate.js";
|
|
42
43
|
import { resourceTools, resourceToolDefinitions } from "./resource.js";
|
|
44
|
+
import { componentTools, componentToolDefinitions } from "./component.js";
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* 도구 모듈 정보
|
|
@@ -73,6 +75,7 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
73
75
|
{ category: "project", definitions: projectToolDefinitions, handlers: projectTools as ToolModule["handlers"], requiresServer: true },
|
|
74
76
|
{ category: "ate", definitions: ateToolDefinitions as any, handlers: ateTools as any },
|
|
75
77
|
{ category: "resource", definitions: resourceToolDefinitions, handlers: resourceTools },
|
|
78
|
+
{ category: "component", definitions: componentToolDefinitions, handlers: componentTools },
|
|
76
79
|
];
|
|
77
80
|
|
|
78
81
|
/**
|
package/src/tools/project.ts
CHANGED
|
@@ -22,23 +22,36 @@ type DevServerState = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
let devServerState: DevServerState | null = null;
|
|
25
|
+
let devServerStarting = false;
|
|
25
26
|
|
|
26
27
|
function trimOutput(text: string, maxChars: number = 4000): string {
|
|
27
28
|
if (text.length <= maxChars) return text;
|
|
28
29
|
return text.slice(-maxChars);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
const COMMAND_TIMEOUT_MS = 120_000; // 2 minutes
|
|
33
|
+
|
|
34
|
+
async function runCommand(cmd: string[], cwd: string, timeoutMs: number = COMMAND_TIMEOUT_MS) {
|
|
32
35
|
const proc = spawn(cmd, {
|
|
33
36
|
cwd,
|
|
34
37
|
stdout: "pipe",
|
|
35
38
|
stderr: "pipe",
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
proc.kill();
|
|
44
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms: ${cmd.join(" ")}`));
|
|
45
|
+
}, timeoutMs)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const [stdout, stderr, exitCode] = await Promise.race([
|
|
49
|
+
Promise.all([
|
|
50
|
+
new Response(proc.stdout).text(),
|
|
51
|
+
new Response(proc.stderr).text(),
|
|
52
|
+
proc.exited,
|
|
53
|
+
]),
|
|
54
|
+
timeoutPromise,
|
|
42
55
|
]);
|
|
43
56
|
|
|
44
57
|
return {
|
|
@@ -192,6 +205,16 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
192
205
|
|
|
193
206
|
await fs.mkdir(baseDir, { recursive: true });
|
|
194
207
|
|
|
208
|
+
// Runtime whitelist validation for spawn arguments
|
|
209
|
+
const VALID_CSS = ["tailwind", "panda", "none"];
|
|
210
|
+
const VALID_UI = ["shadcn", "ark", "none"];
|
|
211
|
+
if (css !== undefined && !VALID_CSS.includes(css)) {
|
|
212
|
+
return { success: false, error: `Invalid css value: ${css}. Must be one of: ${VALID_CSS.join(", ")}` };
|
|
213
|
+
}
|
|
214
|
+
if (ui !== undefined && !VALID_UI.includes(ui)) {
|
|
215
|
+
return { success: false, error: `Invalid ui value: ${ui}. Must be one of: ${VALID_UI.join(", ")}` };
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
const initArgs = ["@mandujs/cli", "init", name];
|
|
196
219
|
if (minimal) {
|
|
197
220
|
initArgs.push("--minimal");
|
|
@@ -201,7 +224,16 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
201
224
|
if (theme) initArgs.push("--theme");
|
|
202
225
|
}
|
|
203
226
|
|
|
204
|
-
|
|
227
|
+
let initResult: { exitCode: number | null; stdout: string; stderr: string };
|
|
228
|
+
try {
|
|
229
|
+
initResult = await runCommand(["bunx", ...initArgs], baseDir);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
step: "init",
|
|
234
|
+
error: err instanceof Error ? err.message : String(err),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
205
237
|
if (initResult.exitCode !== 0) {
|
|
206
238
|
return {
|
|
207
239
|
success: false,
|
|
@@ -216,7 +248,16 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
216
248
|
|
|
217
249
|
let installResult: { exitCode: number | null; stdout: string; stderr: string } | null = null;
|
|
218
250
|
if (install !== false) {
|
|
219
|
-
|
|
251
|
+
try {
|
|
252
|
+
installResult = await runCommand(["bun", "install"], projectDir);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
step: "install",
|
|
257
|
+
projectDir,
|
|
258
|
+
error: err instanceof Error ? err.message : String(err),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
220
261
|
if (installResult.exitCode !== 0) {
|
|
221
262
|
return {
|
|
222
263
|
success: false,
|
|
@@ -253,53 +294,60 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
253
294
|
|
|
254
295
|
mandu_dev_start: async (args: Record<string, unknown>) => {
|
|
255
296
|
const { cwd } = args as { cwd?: string };
|
|
256
|
-
if (devServerState) {
|
|
297
|
+
if (devServerState || devServerStarting) {
|
|
257
298
|
return {
|
|
258
299
|
success: false,
|
|
259
|
-
message:
|
|
260
|
-
|
|
261
|
-
|
|
300
|
+
message: devServerStarting
|
|
301
|
+
? "Dev server is starting up, please wait"
|
|
302
|
+
: "Dev server is already running",
|
|
303
|
+
pid: devServerState?.process.pid,
|
|
304
|
+
cwd: devServerState?.cwd,
|
|
262
305
|
};
|
|
263
306
|
}
|
|
264
307
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
308
|
+
devServerStarting = true;
|
|
309
|
+
try {
|
|
310
|
+
const targetDir = cwd ? path.resolve(projectRoot, cwd) : projectRoot;
|
|
311
|
+
|
|
312
|
+
const proc = spawn(["bun", "run", "dev"], {
|
|
313
|
+
cwd: targetDir,
|
|
314
|
+
stdout: "pipe",
|
|
315
|
+
stderr: "pipe",
|
|
316
|
+
stdin: "ignore",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const state: DevServerState = {
|
|
320
|
+
process: proc,
|
|
321
|
+
cwd: targetDir,
|
|
322
|
+
startedAt: new Date(),
|
|
323
|
+
output: [],
|
|
324
|
+
maxLines: 50,
|
|
325
|
+
};
|
|
326
|
+
devServerState = state;
|
|
327
|
+
|
|
328
|
+
consumeStream(proc.stdout, state, "stdout", server).catch(() => {});
|
|
329
|
+
consumeStream(proc.stderr, state, "stderr", server).catch(() => {});
|
|
282
330
|
|
|
283
|
-
|
|
284
|
-
|
|
331
|
+
proc.exited.then(() => {
|
|
332
|
+
if (devServerState?.process === proc) {
|
|
333
|
+
devServerState = null;
|
|
334
|
+
}
|
|
335
|
+
}).catch(() => {});
|
|
285
336
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
devServerState = null;
|
|
337
|
+
if (monitor) {
|
|
338
|
+
monitor.logEvent("dev", `Dev server started (${targetDir})`);
|
|
289
339
|
}
|
|
290
|
-
}).catch(() => {});
|
|
291
340
|
|
|
292
|
-
|
|
293
|
-
|
|
341
|
+
return {
|
|
342
|
+
success: true,
|
|
343
|
+
pid: proc.pid,
|
|
344
|
+
cwd: targetDir,
|
|
345
|
+
startedAt: state.startedAt.toISOString(),
|
|
346
|
+
message: "Dev server started",
|
|
347
|
+
};
|
|
348
|
+
} finally {
|
|
349
|
+
devServerStarting = false;
|
|
294
350
|
}
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
success: true,
|
|
298
|
-
pid: proc.pid,
|
|
299
|
-
cwd: targetDir,
|
|
300
|
-
startedAt: state.startedAt.toISOString(),
|
|
301
|
-
message: "Dev server started",
|
|
302
|
-
};
|
|
303
351
|
},
|
|
304
352
|
|
|
305
353
|
mandu_dev_stop: async () => {
|
package/src/utils/project.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function getProjectPaths(rootDir: string) {
|
|
|
54
54
|
export function isInsideProject(filePath: string, rootDir: string): boolean {
|
|
55
55
|
const resolved = path.resolve(filePath);
|
|
56
56
|
const root = path.resolve(rootDir);
|
|
57
|
-
return resolved.startsWith(root);
|
|
57
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -73,7 +73,9 @@ export async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Write JSON file safely
|
|
76
|
+
* Write JSON file safely.
|
|
77
|
+
* Note: Callers are responsible for ensuring filePath is within the project root.
|
|
78
|
+
* All internal callers use paths derived from getProjectPaths() which are scoped to projectRoot.
|
|
77
79
|
*/
|
|
78
80
|
export async function writeJsonFile(filePath: string, data: unknown): Promise<void> {
|
|
79
81
|
const dir = path.dirname(filePath);
|