@nbakka/mcp-appium 1.0.28 → 2.0.1

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/lib/server.js ADDED
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMcpServer = void 0;
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const zod_1 = require("zod");
6
+ const logger_1 = require("./logger");
7
+ const android_1 = require("./android");
8
+ const robot_1 = require("./robot");
9
+ const iphone_simulator_1 = require("./iphone-simulator");
10
+ const ios_1 = require("./ios");
11
+ const png_1 = require("./png");
12
+ const image_utils_1 = require("./image-utils");
13
+ const getAgentVersion = () => {
14
+ const json = require("../package.json");
15
+ return json.version;
16
+ };
17
+
18
+ const getLatestAgentVersion = async () => {
19
+ const response = await fetch("https://api.github.com/repos/mobile-next/mobile-mcp/tags?per_page=1");
20
+ const json = await response.json();
21
+ return json[0].name;
22
+ };
23
+ const checkForLatestAgentVersion = async () => {
24
+ try {
25
+ const latestVersion = await getLatestAgentVersion();
26
+ const currentVersion = getAgentVersion();
27
+ if (latestVersion !== currentVersion) {
28
+ (0, logger_1.trace)(`You are running an older version of the agent. Please update to the latest version: ${latestVersion}.`);
29
+ }
30
+ }
31
+ catch (error) {
32
+ // ignore
33
+ }
34
+ };
35
+ const createMcpServer = () => {
36
+ const server = new mcp_js_1.McpServer({
37
+ name: "mobile-mcp",
38
+ version: getAgentVersion(),
39
+ capabilities: {
40
+ resources: {},
41
+ tools: {},
42
+ },
43
+ });
44
+ const tool = (name, description, paramsSchema, cb) => {
45
+ const wrappedCb = async (args) => {
46
+ try {
47
+ (0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
48
+ const response = await cb(args);
49
+ (0, logger_1.trace)(`=> ${response}`);
50
+ return {
51
+ content: [{ type: "text", text: response }],
52
+ };
53
+ }
54
+ catch (error) {
55
+ if (error instanceof robot_1.ActionableError) {
56
+ return {
57
+ content: [{ type: "text", text: `${error.message}. Please fix the issue and try again.` }],
58
+ };
59
+ }
60
+ else {
61
+ // a real exception
62
+ (0, logger_1.trace)(`Tool '${description}' failed: ${error.message} stack: ${error.stack}`);
63
+ return {
64
+ content: [{ type: "text", text: `Error: ${error.message}` }],
65
+ isError: true,
66
+ };
67
+ }
68
+ }
69
+ };
70
+ server.tool(name, description, paramsSchema, args => wrappedCb(args));
71
+ };
72
+ let robot;
73
+ const simulatorManager = new iphone_simulator_1.SimctlManager();
74
+ const requireRobot = () => {
75
+ if (!robot) {
76
+ throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
77
+ }
78
+ };
79
+ tool("mobile_list_available_devices", "List all available devices. This includes both physical devices and simulators. If there is more than one device returned, you need to let the user select one of them.", {}, async ({}) => {
80
+ const iosManager = new ios_1.IosManager();
81
+ const androidManager = new android_1.AndroidDeviceManager();
82
+ const devices = simulatorManager.listBootedSimulators();
83
+ const simulatorNames = devices.map(d => d.name);
84
+ const androidDevices = androidManager.getConnectedDevices();
85
+ const iosDevices = await iosManager.listDevices();
86
+ const iosDeviceNames = iosDevices.map(d => d.deviceId);
87
+ const androidTvDevices = androidDevices.filter(d => d.deviceType === "tv").map(d => d.deviceId);
88
+ const androidMobileDevices = androidDevices.filter(d => d.deviceType === "mobile").map(d => d.deviceId);
89
+ const resp = ["Found these devices:"];
90
+ if (simulatorNames.length > 0) {
91
+ resp.push(`iOS simulators: [${simulatorNames.join(".")}]`);
92
+ }
93
+ if (iosDevices.length > 0) {
94
+ resp.push(`iOS devices: [${iosDeviceNames.join(",")}]`);
95
+ }
96
+ if (androidMobileDevices.length > 0) {
97
+ resp.push(`Android devices: [${androidMobileDevices.join(",")}]`);
98
+ }
99
+ if (androidTvDevices.length > 0) {
100
+ resp.push(`Android TV devices: [${androidTvDevices.join(",")}]`);
101
+ }
102
+ return resp.join("\n");
103
+ });
104
+ tool("mobile_use_device", "Select a device to use. This can be a simulator or an Android device. Use the list_available_devices tool to get a list of available devices.", {
105
+ device: zod_1.z.string().describe("The name of the device to select"),
106
+ deviceType: zod_1.z.enum(["simulator", "ios", "android"]).describe("The type of device to select"),
107
+ }, async ({ device, deviceType }) => {
108
+ switch (deviceType) {
109
+ case "simulator":
110
+ robot = simulatorManager.getSimulator(device);
111
+ break;
112
+ case "ios":
113
+ robot = new ios_1.IosRobot(device);
114
+ break;
115
+ case "android":
116
+ robot = new android_1.AndroidRobot(device);
117
+ break;
118
+ }
119
+ return `Selected device: ${device}`;
120
+ });
121
+ tool("mobile_list_apps", "List all the installed apps on the device", {}, async ({}) => {
122
+ requireRobot();
123
+ const result = await robot.listApps();
124
+ return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
125
+ });
126
+ tool("mobile_launch_app", "Launch an app on mobile device. Use this to open a specific app. You can find the package name of the app by calling list_apps_on_device.", {
127
+ packageName: zod_1.z.string().describe("The package name of the app to launch"),
128
+ }, async ({ packageName }) => {
129
+ requireRobot();
130
+ await robot.launchApp(packageName);
131
+ return `Launched app ${packageName}`;
132
+ });
133
+ tool("mobile_terminate_app", "Stop and terminate an app on mobile device", {
134
+ packageName: zod_1.z.string().describe("The package name of the app to terminate"),
135
+ }, async ({ packageName }) => {
136
+ requireRobot();
137
+ await robot.terminateApp(packageName);
138
+ return `Terminated app ${packageName}`;
139
+ });
140
+ tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {}, async ({}) => {
141
+ requireRobot();
142
+ const screenSize = await robot.getScreenSize();
143
+ return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
144
+ });
145
+ tool("mobile_click_on_element_by_text", "Click on the screen element identified by its text", {
146
+ text: zod_1.z.string().describe("The visible text of the element to click"),
147
+ }, async ({ text }) => {
148
+ requireRobot();
149
+ const xpath = `//*[@text="${text}"]`;
150
+ const element = await robot.clickByXPath(xpath);
151
+ if (!element) {
152
+ throw new Error(`Element with text "${text}" not found`);
153
+ }
154
+ // Wait for 2 seconds after click
155
+ await new Promise(resolve => setTimeout(resolve, 2000));
156
+ return `Clicked on element with text: "${text}"`;
157
+ });
158
+
159
+ tool("mobile_list_elements_on_screen", "List elements on screen with display text or accessibility label. Do not cache this result.", {}, async ({}) => {
160
+ requireRobot();
161
+ const elements = await robot.getElementsOnScreen();
162
+ const result = elements.map(element => {
163
+ const out = {
164
+ type: element.type,
165
+ text: element.text,
166
+ label: element.label,
167
+ name: element.name,
168
+ value: element.value,
169
+ };
170
+ if (element.focused) {
171
+ out.focused = true;
172
+ }
173
+ return out;
174
+ });
175
+ return `Found these elements on screen: ${JSON.stringify(result)}`;
176
+ });
177
+ tool("mobile_press_button", "Press a button on device", {
178
+ button: zod_1.z.string().describe("The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER, DPAD_CENTER (android tv only), DPAD_UP (android tv only), DPAD_DOWN (android tv only), DPAD_LEFT (android tv only), DPAD_RIGHT (android tv only)"),
179
+ }, async ({ button }) => {
180
+ requireRobot();
181
+ await robot.pressButton(button);
182
+ return `Pressed the button: ${button}`;
183
+ });
184
+ tool("mobile_open_url", "Open a URL in browser on device", {
185
+ url: zod_1.z.string().describe("The URL to open"),
186
+ }, async ({ url }) => {
187
+ requireRobot();
188
+ await robot.openUrl(url);
189
+ await new Promise(resolve => setTimeout(resolve, 5000));
190
+ return `Opened URL: ${url}`;
191
+ });
192
+ tool("swipe_on_screen", "Swipe on the screen", {
193
+ direction: zod_1.z.enum(["up", "down"]).describe("The direction to swipe"),
194
+ }, async ({ direction }) => {
195
+ requireRobot();
196
+ await robot.swipe(direction);
197
+ return `Swiped ${direction} on screen`;
198
+ });
199
+ tool("mobile_type_keys", "Type text into the focused element", {
200
+ text: zod_1.z.string().describe("The text to type"),
201
+ submit: zod_1.z.boolean().describe("Whether to submit the text. If true, the text will be submitted as if the user pressed the enter key."),
202
+ }, async ({ text, submit }) => {
203
+ requireRobot();
204
+ await robot.sendKeys(text);
205
+ if (submit) {
206
+ await robot.pressButton("ENTER");
207
+ }
208
+ return `Typed text: ${text}`;
209
+ });
210
+ tool("mobile_set_orientation", "Change the screen orientation of the device", {
211
+ orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
212
+ }, async ({ orientation }) => {
213
+ requireRobot();
214
+ await robot.setOrientation(orientation);
215
+ return `Changed device orientation to ${orientation}`;
216
+ });
217
+ tool("mobile_get_orientation", "Get the current screen orientation of the device", {}, async () => {
218
+ requireRobot();
219
+ const orientation = await robot.getOrientation();
220
+ return `Current device orientation is ${orientation}`;
221
+ });
222
+ // async check for latest agent version
223
+ checkForLatestAgentVersion().then();
224
+ return server;
225
+ };
226
+
227
+ tool(
228
+ "mobile_create_session",
229
+ "create a mobile session once so that session id can be used in other tools where it is needed",
230
+ {},
231
+ async () => {
232
+ const capabilities = {
233
+ platformName: "Android",
234
+ "appium:udid": "emulator-5554",
235
+ "appium:automationName": "UiAutomator2",
236
+ "appium:noReset": true,
237
+ "appium:appPackage": "com.locon.housing",
238
+ "appium:appActivity": "com.locon.housing.presentation.MainActivity",
239
+ };
240
+
241
+ const payload = {
242
+ capabilities: {
243
+ firstMatch: [{}],
244
+ alwaysMatch: capabilities,
245
+ },
246
+ };
247
+
248
+ const response = await fetch("http://localhost:4723/session", {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify(payload),
252
+ });
253
+
254
+ if (!response.ok) {
255
+ throw new Error(`Failed to create session: ${response.statusText}`);
256
+ }
257
+
258
+ const json = await response.json();
259
+ return `Session created with sessionId: ${json.sessionId}`;
260
+ }
261
+ );
262
+
263
+
264
+ tool(
265
+ "mobile_click_using_xpath",
266
+ "Click an element identified by text using path",
267
+ {
268
+ sessionId: zod_1.z.string().describe("Appium session ID"),
269
+ text: zod_1.z.string().describe("Visible text of the element to click"),
270
+ },
271
+ async ({ sessionId, text }) => {
272
+ const xpath = `//*[@text="${text}"]`;
273
+ const clickUrl = `http://localhost:4723/session/${sessionId}/element`;
274
+
275
+ // Find element
276
+ const findResponse = await fetch(clickUrl, {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({ using: "xpath", value: xpath }),
280
+ });
281
+
282
+ if (!findResponse.ok) {
283
+ throw new Error(`Failed to find element: ${findResponse.statusText}`);
284
+ }
285
+
286
+ const findJson = await findResponse.json();
287
+ if (!findJson.value || !findJson.value.elementId) {
288
+ throw new Error(`Element with text "${text}" not found`);
289
+ }
290
+
291
+ const elementId = findJson.value.elementId;
292
+
293
+ // Click element
294
+ const clickElementUrl = `http://localhost:4723/session/${sessionId}/element/${elementId}/click`;
295
+ const clickResponse = await fetch(clickElementUrl, { method: "POST" });
296
+
297
+ if (!clickResponse.ok) {
298
+ throw new Error(`Failed to click element: ${clickResponse.statusText}`);
299
+ }
300
+
301
+ // Optional wait after click
302
+ await new Promise((resolve) => setTimeout(resolve, 2000));
303
+
304
+ return `Clicked on element with text: "${text}" in session: ${sessionId}`;
305
+ }
306
+ );
307
+ exports.createMcpServer = createMcpServer;
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebDriverAgent = void 0;
4
+ const robot_1 = require("./robot");
5
+ class WebDriverAgent {
6
+ host;
7
+ port;
8
+ constructor(host, port) {
9
+ this.host = host;
10
+ this.port = port;
11
+ }
12
+ async isRunning() {
13
+ const url = `http://${this.host}:${this.port}/status`;
14
+ try {
15
+ const response = await fetch(url);
16
+ return response.status === 200;
17
+ }
18
+ catch (error) {
19
+ console.error(`Failed to connect to WebDriverAgent: ${error}`);
20
+ return false;
21
+ }
22
+ }
23
+ async createSession() {
24
+ const url = `http://${this.host}:${this.port}/session`;
25
+ const response = await fetch(url, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ },
30
+ body: JSON.stringify({ capabilities: { alwaysMatch: { platformName: "iOS" } } }),
31
+ });
32
+ const json = await response.json();
33
+ return json.value.sessionId;
34
+ }
35
+ async deleteSession(sessionId) {
36
+ const url = `http://${this.host}:${this.port}/session/${sessionId}`;
37
+ const response = await fetch(url, { method: "DELETE" });
38
+ return response.json();
39
+ }
40
+ async withinSession(fn) {
41
+ const sessionId = await this.createSession();
42
+ const url = `http://${this.host}:${this.port}/session/${sessionId}`;
43
+ const result = await fn(url);
44
+ await this.deleteSession(sessionId);
45
+ return result;
46
+ }
47
+ async getScreenSize() {
48
+ return this.withinSession(async (sessionUrl) => {
49
+ const url = `${sessionUrl}/wda/screen`;
50
+ const response = await fetch(url);
51
+ const json = await response.json();
52
+ return {
53
+ width: json.value.screenSize.width,
54
+ height: json.value.screenSize.height,
55
+ scale: json.value.scale || 1,
56
+ };
57
+ });
58
+ }
59
+ async sendKeys(keys) {
60
+ await this.withinSession(async (sessionUrl) => {
61
+ const url = `${sessionUrl}/wda/keys`;
62
+ await fetch(url, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ },
67
+ body: JSON.stringify({ value: [keys] }),
68
+ });
69
+ });
70
+ }
71
+ async pressButton(button) {
72
+ const _map = {
73
+ "HOME": "home",
74
+ "VOLUME_UP": "volumeup",
75
+ "VOLUME_DOWN": "volumedown",
76
+ };
77
+ if (button === "ENTER") {
78
+ await this.sendKeys("\n");
79
+ return;
80
+ }
81
+ // Type assertion to check if button is a key of _map
82
+ if (!(button in _map)) {
83
+ throw new robot_1.ActionableError(`Button "${button}" is not supported`);
84
+ }
85
+ await this.withinSession(async (sessionUrl) => {
86
+ const url = `${sessionUrl}/wda/pressButton`;
87
+ const response = await fetch(url, {
88
+ method: "POST",
89
+ headers: {
90
+ "Content-Type": "application/json",
91
+ },
92
+ body: JSON.stringify({
93
+ name: button,
94
+ }),
95
+ });
96
+ return response.json();
97
+ });
98
+ }
99
+ async tap(x, y) {
100
+ await this.withinSession(async (sessionUrl) => {
101
+ const url = `${sessionUrl}/actions`;
102
+ await fetch(url, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ },
107
+ body: JSON.stringify({
108
+ actions: [
109
+ {
110
+ type: "pointer",
111
+ id: "finger1",
112
+ parameters: { pointerType: "touch" },
113
+ actions: [
114
+ { type: "pointerMove", duration: 0, x, y },
115
+ { type: "pointerDown", button: 0 },
116
+ { type: "pause", duration: 100 },
117
+ { type: "pointerUp", button: 0 }
118
+ ]
119
+ }
120
+ ]
121
+ }),
122
+ });
123
+ });
124
+ }
125
+ isVisible(rect) {
126
+ return rect.x >= 0 && rect.y >= 0;
127
+ }
128
+ filterSourceElements(source) {
129
+ const output = [];
130
+ const acceptedTypes = ["TextField", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image"];
131
+ if (acceptedTypes.includes(source.type)) {
132
+ if (source.isVisible === "1" && this.isVisible(source.rect)) {
133
+ if (source.label !== null || source.name !== null) {
134
+ output.push({
135
+ type: source.type,
136
+ label: source.label,
137
+ name: source.name,
138
+ value: source.value,
139
+ rect: {
140
+ x: source.rect.x,
141
+ y: source.rect.y,
142
+ width: source.rect.width,
143
+ height: source.rect.height,
144
+ },
145
+ });
146
+ }
147
+ }
148
+ }
149
+ if (source.children) {
150
+ for (const child of source.children) {
151
+ output.push(...this.filterSourceElements(child));
152
+ }
153
+ }
154
+ return output;
155
+ }
156
+ async getPageSource() {
157
+ const url = `http://${this.host}:${this.port}/source/?format=json`;
158
+ const response = await fetch(url);
159
+ const json = await response.json();
160
+ return json;
161
+ }
162
+ async getElementsOnScreen() {
163
+ const source = await this.getPageSource();
164
+ return this.filterSourceElements(source.value);
165
+ }
166
+ async openUrl(url) {
167
+ await this.withinSession(async (sessionUrl) => {
168
+ await fetch(`${sessionUrl}/url`, {
169
+ method: "POST",
170
+ body: JSON.stringify({ url }),
171
+ });
172
+ });
173
+ }
174
+ async swipe(direction) {
175
+ await this.withinSession(async (sessionUrl) => {
176
+ const x0 = 200;
177
+ let y0 = 600;
178
+ const x1 = 200;
179
+ let y1 = 200;
180
+ if (direction === "up") {
181
+ const tmp = y0;
182
+ y0 = y1;
183
+ y1 = tmp;
184
+ }
185
+ const url = `${sessionUrl}/actions`;
186
+ await fetch(url, {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ },
191
+ body: JSON.stringify({
192
+ actions: [
193
+ {
194
+ type: "pointer",
195
+ id: "finger1",
196
+ parameters: { pointerType: "touch" },
197
+ actions: [
198
+ { type: "pointerMove", duration: 0, x: x0, y: y0 },
199
+ { type: "pointerDown", button: 0 },
200
+ { type: "pointerMove", duration: 0, x: x1, y: y1 },
201
+ { type: "pause", duration: 1000 },
202
+ { type: "pointerUp", button: 0 }
203
+ ]
204
+ }
205
+ ]
206
+ }),
207
+ });
208
+ });
209
+ }
210
+ async setOrientation(orientation) {
211
+ await this.withinSession(async (sessionUrl) => {
212
+ const url = `${sessionUrl}/orientation`;
213
+ await fetch(url, {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({
217
+ orientation: orientation.toUpperCase()
218
+ })
219
+ });
220
+ });
221
+ }
222
+ async getOrientation() {
223
+ return this.withinSession(async (sessionUrl) => {
224
+ const url = `${sessionUrl}/orientation`;
225
+ const response = await fetch(url);
226
+ const json = await response.json();
227
+ return json.value.toLowerCase();
228
+ });
229
+ }
230
+ }
231
+ exports.WebDriverAgent = WebDriverAgent;
package/package.json CHANGED
@@ -1,24 +1,52 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "1.0.28",
4
- "description": "Selenium WebDriver MCP Server",
5
- "type": "module",
6
- "main": "src/lib/server.js",
7
- "bin": {
8
- "mcp-appium": "./src/lib/server.js"
3
+ "version": "2.0.1",
4
+ "description": "Appium MCP",
5
+ "engines": {
6
+ "node": ">=18"
9
7
  },
8
+ "license": "Apache-2.0",
10
9
  "scripts": {
11
- "start": "node src/lib/server.js",
12
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "build": "tsc && chmod +x lib/index.js",
11
+ "lint": "eslint .",
12
+ "test": "nyc mocha --require ts-node/register test/*.ts",
13
+ "watch": "tsc --watch",
14
+ "clean": "rm -rf lib",
15
+ "prepare": "husky"
13
16
  },
14
- "keywords": [],
15
- "author": "",
16
- "license": "ISC",
17
+ "files": [
18
+ "lib"
19
+ ],
17
20
  "dependencies": {
18
- "@modelcontextprotocol/sdk": "^1.7.0",
19
- "axios": "^1.4.0",
20
- "zod": "^3.22.2",
21
- "yargs": "^17.7.2",
22
- "xml2js": "0.6.2"
23
- }
24
- }
21
+ "@modelcontextprotocol/sdk": "^1.6.1",
22
+ "fast-xml-parser": "^5.0.9",
23
+ "zod-to-json-schema": "^3.24.4"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/eslintrc": "^3.2.0",
27
+ "@eslint/js": "^9.19.0",
28
+ "@stylistic/eslint-plugin": "^3.0.1",
29
+ "@types/mocha": "^10.0.10",
30
+ "@types/node": "^22.13.10",
31
+ "@typescript-eslint/eslint-plugin": "^8.28.0",
32
+ "@typescript-eslint/parser": "^8.26.1",
33
+ "@typescript-eslint/utils": "^8.26.1",
34
+ "eslint": "^9.19.0",
35
+ "eslint-plugin": "^1.0.1",
36
+ "eslint-plugin-import": "^2.31.0",
37
+ "eslint-plugin-notice": "^1.0.0",
38
+ "husky": "^9.1.7",
39
+ "nyc": "^17.1.0",
40
+ "mocha": "^11.1.0",
41
+ "ts-node": "^10.9.2",
42
+ "typescript": "^5.8.2"
43
+ },
44
+ "main": "index.js",
45
+ "bin": {
46
+ "mcp-appium": "lib/index.js"
47
+ },
48
+ "directories": {
49
+ "lib": "lib"
50
+ },
51
+ "author": "nbakka"
52
+ }
package/bin/mcp-appium.js DELETED
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { fileURLToPath } from 'url';
4
- import { dirname, resolve } from 'path';
5
- import { spawn } from 'child_process';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = dirname(__filename);
9
-
10
- const serverPath = resolve(__dirname, '../src/lib/server.js');
11
-
12
- // Start the server
13
- const child = spawn('node', [serverPath], {
14
- stdio: 'inherit'
15
- });
16
-
17
- child.on('error', (error) => {
18
- console.error(`Error starting server: ${error.message}`);
19
- process.exit(1);
20
- });
21
-
22
- // Handle process termination
23
- process.on('SIGTERM', () => {
24
- child.kill('SIGTERM');
25
- });
26
-
27
- process.on('SIGINT', () => {
28
- child.kill('SIGINT');
29
- });#!/usr/bin/env node
30
-
31
- import { fileURLToPath } from 'url';
32
- import { dirname, resolve } from 'path';
33
- import { spawn } from 'child_process';
34
-
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = dirname(__filename);
37
-
38
- const serverPath = resolve(__dirname, '../src/lib/server.js');
39
-
40
- // Start the server
41
- const child = spawn('node', [serverPath], {
42
- stdio: 'inherit'
43
- });
44
-
45
- child.on('error', (error) => {
46
- console.error(`Error starting server: ${error.message}`);
47
- process.exit(1);
48
- });
49
-
50
- // Handle process termination
51
- process.on('SIGTERM', () => {
52
- child.kill('SIGTERM');
53
- });
54
-
55
- process.on('SIGINT', () => {
56
- child.kill('SIGINT');
57
- });