crosspad-mcp-server 4.0.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.
Files changed (80) hide show
  1. package/README.md +187 -0
  2. package/dist/config.d.ts +10 -0
  3. package/dist/config.js +33 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +360 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/tools/architecture.d.ts +16 -0
  9. package/dist/tools/architecture.js +198 -0
  10. package/dist/tools/architecture.js.map +1 -0
  11. package/dist/tools/build-check.d.ts +23 -0
  12. package/dist/tools/build-check.js +162 -0
  13. package/dist/tools/build-check.js.map +1 -0
  14. package/dist/tools/build.d.ts +14 -0
  15. package/dist/tools/build.js +101 -0
  16. package/dist/tools/build.js.map +1 -0
  17. package/dist/tools/diff-core.d.ts +24 -0
  18. package/dist/tools/diff-core.js +88 -0
  19. package/dist/tools/diff-core.js.map +1 -0
  20. package/dist/tools/idf-build.d.ts +10 -0
  21. package/dist/tools/idf-build.js +155 -0
  22. package/dist/tools/idf-build.js.map +1 -0
  23. package/dist/tools/input.d.ts +36 -0
  24. package/dist/tools/input.js +61 -0
  25. package/dist/tools/input.js.map +1 -0
  26. package/dist/tools/log.d.ts +16 -0
  27. package/dist/tools/log.js +49 -0
  28. package/dist/tools/log.js.map +1 -0
  29. package/dist/tools/repos.d.ts +12 -0
  30. package/dist/tools/repos.js +63 -0
  31. package/dist/tools/repos.js.map +1 -0
  32. package/dist/tools/scaffold.d.ts +15 -0
  33. package/dist/tools/scaffold.js +192 -0
  34. package/dist/tools/scaffold.js.map +1 -0
  35. package/dist/tools/screenshot.d.ts +24 -0
  36. package/dist/tools/screenshot.js +80 -0
  37. package/dist/tools/screenshot.js.map +1 -0
  38. package/dist/tools/settings.d.ts +25 -0
  39. package/dist/tools/settings.js +48 -0
  40. package/dist/tools/settings.js.map +1 -0
  41. package/dist/tools/stats.d.ts +18 -0
  42. package/dist/tools/stats.js +31 -0
  43. package/dist/tools/stats.js.map +1 -0
  44. package/dist/tools/symbols.d.ts +20 -0
  45. package/dist/tools/symbols.js +157 -0
  46. package/dist/tools/symbols.js.map +1 -0
  47. package/dist/tools/test.d.ts +24 -0
  48. package/dist/tools/test.js +227 -0
  49. package/dist/tools/test.js.map +1 -0
  50. package/dist/utils/exec.d.ts +58 -0
  51. package/dist/utils/exec.js +292 -0
  52. package/dist/utils/exec.js.map +1 -0
  53. package/dist/utils/git.d.ts +10 -0
  54. package/dist/utils/git.js +29 -0
  55. package/dist/utils/git.js.map +1 -0
  56. package/dist/utils/remote-client.d.ts +17 -0
  57. package/dist/utils/remote-client.js +94 -0
  58. package/dist/utils/remote-client.js.map +1 -0
  59. package/package.json +21 -0
  60. package/server.json +23 -0
  61. package/src/config.ts +45 -0
  62. package/src/index.ts +484 -0
  63. package/src/tools/architecture.ts +260 -0
  64. package/src/tools/build-check.ts +178 -0
  65. package/src/tools/build.ts +130 -0
  66. package/src/tools/diff-core.ts +130 -0
  67. package/src/tools/idf-build.ts +182 -0
  68. package/src/tools/input.ts +80 -0
  69. package/src/tools/log.ts +75 -0
  70. package/src/tools/repos.ts +75 -0
  71. package/src/tools/scaffold.ts +229 -0
  72. package/src/tools/screenshot.ts +100 -0
  73. package/src/tools/settings.ts +68 -0
  74. package/src/tools/stats.ts +38 -0
  75. package/src/tools/symbols.ts +185 -0
  76. package/src/tools/test.ts +264 -0
  77. package/src/utils/exec.ts +376 -0
  78. package/src/utils/git.ts +45 -0
  79. package/src/utils/remote-client.ts +107 -0
  80. package/tsconfig.json +16 -0
@@ -0,0 +1,376 @@
1
+ import { execSync, spawn, ChildProcess, SpawnOptions } from "child_process";
2
+ import { VCVARSALL, IS_WINDOWS, IDF_PATH } from "../config.js";
3
+
4
+ /** Callback invoked for each line of stdout/stderr during streaming exec. */
5
+ export type OnLine = (stream: "stdout" | "stderr", line: string) => void;
6
+
7
+ let cachedMsvcEnv: Record<string, string> | null = null;
8
+
9
+ /**
10
+ * Capture MSVC environment by running vcvarsall.bat and parsing `set` output.
11
+ * Cached for the lifetime of the server process. Windows-only.
12
+ */
13
+ export function getMsvcEnv(): Record<string, string> {
14
+ if (!IS_WINDOWS) {
15
+ return { ...process.env } as Record<string, string>;
16
+ }
17
+ if (cachedMsvcEnv) return cachedMsvcEnv;
18
+
19
+ const cmd = `"${VCVARSALL}" x64 >nul 2>&1 && set`;
20
+ const output = execSync(cmd, {
21
+ shell: "cmd.exe",
22
+ encoding: "utf-8",
23
+ timeout: 30_000,
24
+ });
25
+
26
+ const env: Record<string, string> = {};
27
+ for (const line of output.split("\n")) {
28
+ const eq = line.indexOf("=");
29
+ if (eq > 0) {
30
+ env[line.slice(0, eq)] = line.slice(eq + 1).trimEnd();
31
+ }
32
+ }
33
+
34
+ cachedMsvcEnv = env;
35
+ return env;
36
+ }
37
+
38
+ export interface ExecResult {
39
+ success: boolean;
40
+ stdout: string;
41
+ stderr: string;
42
+ exitCode: number;
43
+ durationMs: number;
44
+ }
45
+
46
+ /**
47
+ * Run a command with the MSVC environment, capturing output.
48
+ */
49
+ export function runWithMsvc(
50
+ cmd: string,
51
+ cwd: string,
52
+ timeoutMs = 300_000
53
+ ): ExecResult {
54
+ const env = getMsvcEnv();
55
+ const start = Date.now();
56
+
57
+ try {
58
+ const stdout = execSync(cmd, {
59
+ cwd,
60
+ env,
61
+ shell: "cmd.exe",
62
+ encoding: "utf-8",
63
+ timeout: timeoutMs,
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ });
66
+ return {
67
+ success: true,
68
+ stdout: normalizeLineEndings(stdout),
69
+ stderr: "",
70
+ exitCode: 0,
71
+ durationMs: Date.now() - start,
72
+ };
73
+ } catch (err: any) {
74
+ return {
75
+ success: false,
76
+ stdout: normalizeLineEndings(err.stdout?.toString() ?? ""),
77
+ stderr: normalizeLineEndings(err.stderr?.toString() ?? ""),
78
+ exitCode: err.status ?? 1,
79
+ durationMs: Date.now() - start,
80
+ };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Run a command in the default shell, capturing output.
86
+ */
87
+ export function runCommand(
88
+ cmd: string,
89
+ cwd: string,
90
+ timeoutMs = 60_000
91
+ ): ExecResult {
92
+ const start = Date.now();
93
+ try {
94
+ const stdout = execSync(cmd, {
95
+ cwd,
96
+ encoding: "utf-8",
97
+ timeout: timeoutMs,
98
+ stdio: ["pipe", "pipe", "pipe"],
99
+ });
100
+ return {
101
+ success: true,
102
+ stdout: normalizeLineEndings(stdout),
103
+ stderr: "",
104
+ exitCode: 0,
105
+ durationMs: Date.now() - start,
106
+ };
107
+ } catch (err: any) {
108
+ return {
109
+ success: false,
110
+ stdout: normalizeLineEndings(err.stdout?.toString() ?? ""),
111
+ stderr: normalizeLineEndings(err.stderr?.toString() ?? ""),
112
+ exitCode: err.status ?? 1,
113
+ durationMs: Date.now() - start,
114
+ };
115
+ }
116
+ }
117
+
118
+ // ═══════════════════════════════════════════════════════════════════════
119
+ // STREAMING VARIANTS (spawn-based, line-by-line callbacks)
120
+ // ═══════════════════════════════════════════════════════════════════════
121
+
122
+ /**
123
+ * Helper: spawn a process and stream stdout/stderr line-by-line via onLine.
124
+ * Returns the same ExecResult as the sync variants.
125
+ */
126
+ function spawnStreaming(
127
+ cmd: string,
128
+ cwd: string,
129
+ env: Record<string, string> | undefined,
130
+ shell: string | boolean,
131
+ onLine: OnLine,
132
+ timeoutMs: number
133
+ ): Promise<ExecResult> {
134
+ return new Promise((resolve) => {
135
+ const start = Date.now();
136
+ const child = spawn(cmd, [], {
137
+ cwd,
138
+ env,
139
+ shell,
140
+ stdio: ["pipe", "pipe", "pipe"],
141
+ windowsHide: true,
142
+ });
143
+
144
+ let stdout = "";
145
+ let stderr = "";
146
+ let stdoutBuf = "";
147
+ let stderrBuf = "";
148
+ let killed = false;
149
+
150
+ const timer = setTimeout(() => {
151
+ killed = true;
152
+ child.kill("SIGTERM");
153
+ }, timeoutMs);
154
+
155
+ function flushLines(buf: string, stream: "stdout" | "stderr"): string {
156
+ const parts = buf.split("\n");
157
+ // Last part is incomplete — keep it in buffer
158
+ for (let i = 0; i < parts.length - 1; i++) {
159
+ const line = parts[i].replace(/\r$/, "");
160
+ onLine(stream, line);
161
+ }
162
+ return parts[parts.length - 1];
163
+ }
164
+
165
+ child.stdout?.on("data", (chunk: Buffer) => {
166
+ const text = chunk.toString();
167
+ stdout += text;
168
+ stdoutBuf += text;
169
+ stdoutBuf = flushLines(stdoutBuf, "stdout");
170
+ });
171
+
172
+ child.stderr?.on("data", (chunk: Buffer) => {
173
+ const text = chunk.toString();
174
+ stderr += text;
175
+ stderrBuf += text;
176
+ stderrBuf = flushLines(stderrBuf, "stderr");
177
+ });
178
+
179
+ child.on("close", (code) => {
180
+ clearTimeout(timer);
181
+ // Flush remaining partial lines
182
+ if (stdoutBuf.length > 0) onLine("stdout", stdoutBuf.replace(/\r$/, ""));
183
+ if (stderrBuf.length > 0) onLine("stderr", stderrBuf.replace(/\r$/, ""));
184
+
185
+ resolve({
186
+ success: killed ? false : code === 0,
187
+ stdout: normalizeLineEndings(stdout),
188
+ stderr: normalizeLineEndings(stderr),
189
+ exitCode: killed ? -1 : (code ?? 1),
190
+ durationMs: Date.now() - start,
191
+ });
192
+ });
193
+
194
+ child.on("error", (err) => {
195
+ clearTimeout(timer);
196
+ resolve({
197
+ success: false,
198
+ stdout: normalizeLineEndings(stdout),
199
+ stderr: normalizeLineEndings(stderr + "\n" + err.message),
200
+ exitCode: 1,
201
+ durationMs: Date.now() - start,
202
+ });
203
+ });
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Run a command with the MSVC environment, streaming output line-by-line.
209
+ */
210
+ export function runWithMsvcStream(
211
+ cmd: string,
212
+ cwd: string,
213
+ onLine: OnLine,
214
+ timeoutMs = 300_000
215
+ ): Promise<ExecResult> {
216
+ const env = getMsvcEnv();
217
+ return spawnStreaming(cmd, cwd, env, "cmd.exe", onLine, timeoutMs);
218
+ }
219
+
220
+ /**
221
+ * Run a command in the default shell, streaming output line-by-line.
222
+ */
223
+ export function runCommandStream(
224
+ cmd: string,
225
+ cwd: string,
226
+ onLine: OnLine,
227
+ timeoutMs = 60_000
228
+ ): Promise<ExecResult> {
229
+ return spawnStreaming(cmd, cwd, undefined, true, onLine, timeoutMs);
230
+ }
231
+
232
+ // ═══════════════════════════════════════════════════════════════════════
233
+ // ESP-IDF ENVIRONMENT
234
+ // ═══════════════════════════════════════════════════════════════════════
235
+
236
+ let cachedIdfEnv: Record<string, string> | null = null;
237
+
238
+ /**
239
+ * Capture ESP-IDF environment by running export.bat and parsing `set` output.
240
+ * Must unset MSYSTEM to prevent ESP-IDF from rejecting MSYS shells.
241
+ * Cached for the lifetime of the server process. Windows-only.
242
+ */
243
+ export function getIdfEnv(): Record<string, string> {
244
+ if (!IS_WINDOWS) {
245
+ return { ...process.env } as Record<string, string>;
246
+ }
247
+ if (cachedIdfEnv) return cachedIdfEnv;
248
+
249
+ const exportBat = `${IDF_PATH}\\export.bat`;
250
+ const cmd = `set MSYSTEM=&& set PYTHONIOENCODING=utf-8&& call ${exportBat} >nul 2>&1 && set`;
251
+ const output = execSync(cmd, {
252
+ shell: "cmd.exe",
253
+ encoding: "utf-8",
254
+ timeout: 60_000,
255
+ });
256
+
257
+ const env: Record<string, string> = {};
258
+ for (const line of output.split("\n")) {
259
+ const eq = line.indexOf("=");
260
+ if (eq > 0) {
261
+ env[line.slice(0, eq)] = line.slice(eq + 1).trimEnd();
262
+ }
263
+ }
264
+
265
+ cachedIdfEnv = env;
266
+ return env;
267
+ }
268
+
269
+ /**
270
+ * Run a command with the ESP-IDF environment, capturing output.
271
+ */
272
+ export function runWithIdf(
273
+ cmd: string,
274
+ cwd: string,
275
+ timeoutMs = 600_000
276
+ ): ExecResult {
277
+ const env = getIdfEnv();
278
+ const start = Date.now();
279
+
280
+ try {
281
+ const stdout = execSync(cmd, {
282
+ cwd,
283
+ env,
284
+ shell: "cmd.exe",
285
+ encoding: "utf-8",
286
+ timeout: timeoutMs,
287
+ stdio: ["pipe", "pipe", "pipe"],
288
+ });
289
+ return {
290
+ success: true,
291
+ stdout: normalizeLineEndings(stdout),
292
+ stderr: "",
293
+ exitCode: 0,
294
+ durationMs: Date.now() - start,
295
+ };
296
+ } catch (err: any) {
297
+ return {
298
+ success: false,
299
+ stdout: normalizeLineEndings(err.stdout?.toString() ?? ""),
300
+ stderr: normalizeLineEndings(err.stderr?.toString() ?? ""),
301
+ exitCode: err.status ?? 1,
302
+ durationMs: Date.now() - start,
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Run a command with the ESP-IDF environment, streaming output line-by-line.
309
+ */
310
+ export function runWithIdfStream(
311
+ cmd: string,
312
+ cwd: string,
313
+ onLine: OnLine,
314
+ timeoutMs = 600_000
315
+ ): Promise<ExecResult> {
316
+ const env = getIdfEnv();
317
+ return spawnStreaming(cmd, cwd, env, "cmd.exe", onLine, timeoutMs);
318
+ }
319
+
320
+ // ═══════════════════════════════════════════════════════════════════════
321
+ // PLATFORM-AGNOSTIC BUILD WRAPPERS
322
+ // ═══════════════════════════════════════════════════════════════════════
323
+
324
+ /**
325
+ * Run a build command with the appropriate environment for the current platform.
326
+ * Windows: MSVC env + cmd.exe shell. Unix: default shell.
327
+ */
328
+ export function runBuild(
329
+ cmd: string,
330
+ cwd: string,
331
+ timeoutMs = 300_000
332
+ ): ExecResult {
333
+ if (IS_WINDOWS) {
334
+ return runWithMsvc(cmd, cwd, timeoutMs);
335
+ }
336
+ return runCommand(cmd, cwd, timeoutMs);
337
+ }
338
+
339
+ /**
340
+ * Run a build command with streaming output, platform-aware.
341
+ * Windows: MSVC env + cmd.exe shell. Unix: default shell.
342
+ */
343
+ export function runBuildStream(
344
+ cmd: string,
345
+ cwd: string,
346
+ onLine: OnLine,
347
+ timeoutMs = 300_000
348
+ ): Promise<ExecResult> {
349
+ if (IS_WINDOWS) {
350
+ return runWithMsvcStream(cmd, cwd, onLine, timeoutMs);
351
+ }
352
+ return runCommandStream(cmd, cwd, onLine, timeoutMs);
353
+ }
354
+
355
+ /** Strip \r from Windows line endings */
356
+ function normalizeLineEndings(s: string): string {
357
+ return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
358
+ }
359
+
360
+ /**
361
+ * Spawn a detached process (for crosspad_run).
362
+ */
363
+ export function spawnDetached(
364
+ exe: string,
365
+ args: string[],
366
+ cwd: string
367
+ ): number | null {
368
+ const opts: SpawnOptions = {
369
+ cwd,
370
+ detached: true,
371
+ stdio: "ignore",
372
+ };
373
+ const child = spawn(exe, args, opts);
374
+ child.unref();
375
+ return child.pid ?? null;
376
+ }
@@ -0,0 +1,45 @@
1
+ import { runCommand } from "./exec.js";
2
+
3
+ export interface RepoStatus {
4
+ name: string;
5
+ path: string;
6
+ branch: string;
7
+ head: string;
8
+ dirtyFiles: string[];
9
+ }
10
+
11
+ export function getRepoStatus(name: string, repoPath: string): RepoStatus {
12
+ const branch = runCommand("git branch --show-current", repoPath);
13
+ const log = runCommand("git log --oneline -1", repoPath);
14
+ const status = runCommand("git status --porcelain", repoPath);
15
+
16
+ return {
17
+ name,
18
+ path: repoPath,
19
+ branch: branch.stdout.trim(),
20
+ head: log.stdout.trim(),
21
+ dirtyFiles: status.stdout
22
+ .trim()
23
+ .split("\n")
24
+ .filter((l) => l.length > 0),
25
+ };
26
+ }
27
+
28
+ export function getSubmodulePin(
29
+ repoPath: string,
30
+ submodule: string
31
+ ): string | null {
32
+ const result = runCommand(
33
+ `git submodule status ${submodule}`,
34
+ repoPath
35
+ );
36
+ if (!result.success) return null;
37
+ // Output format: " abc1234 submodule-name (desc)" or "+abc1234 ..."
38
+ const match = result.stdout.match(/[+ -]?([0-9a-f]+)/);
39
+ return match ? match[1] : null;
40
+ }
41
+
42
+ export function getHead(repoPath: string): string | null {
43
+ const result = runCommand("git rev-parse HEAD", repoPath);
44
+ return result.success ? result.stdout.trim() : null;
45
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * TCP client for communicating with the CrossPad simulator's remote control server.
3
+ * Protocol: newline-delimited JSON over TCP on localhost:19840.
4
+ */
5
+
6
+ import { Socket } from "net";
7
+
8
+ const REMOTE_PORT = 19840;
9
+ const REMOTE_HOST = "127.0.0.1";
10
+ const CONNECT_TIMEOUT = 3000;
11
+ const RESPONSE_TIMEOUT = 15000;
12
+
13
+ export interface RemoteResponse {
14
+ ok: boolean;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ /**
19
+ * Send a JSON command to the running simulator and return the response.
20
+ * Opens a fresh TCP connection per call (simple, stateless).
21
+ */
22
+ export function sendRemoteCommand(command: Record<string, unknown>): Promise<RemoteResponse> {
23
+ return new Promise((resolve, reject) => {
24
+ const socket = new Socket();
25
+ let buffer = "";
26
+ let resolved = false;
27
+
28
+ const cleanup = () => {
29
+ if (!resolved) {
30
+ resolved = true;
31
+ socket.destroy();
32
+ }
33
+ };
34
+
35
+ // Connect timeout
36
+ socket.setTimeout(CONNECT_TIMEOUT);
37
+
38
+ socket.on("connect", () => {
39
+ // Extend timeout for response
40
+ socket.setTimeout(RESPONSE_TIMEOUT);
41
+
42
+ // Send command as newline-delimited JSON
43
+ const msg = JSON.stringify(command) + "\n";
44
+ socket.write(msg);
45
+ });
46
+
47
+ socket.on("data", (data) => {
48
+ buffer += data.toString();
49
+
50
+ // Look for newline-delimited response
51
+ const nlIdx = buffer.indexOf("\n");
52
+ if (nlIdx >= 0) {
53
+ const line = buffer.slice(0, nlIdx);
54
+ resolved = true;
55
+ socket.destroy();
56
+ try {
57
+ resolve(JSON.parse(line) as RemoteResponse);
58
+ } catch {
59
+ resolve({ ok: false, error: "invalid JSON response", raw: line });
60
+ }
61
+ }
62
+ });
63
+
64
+ socket.on("timeout", () => {
65
+ cleanup();
66
+ reject(new Error("Connection/response timeout — is the simulator running?"));
67
+ });
68
+
69
+ socket.on("error", (err: NodeJS.ErrnoException) => {
70
+ cleanup();
71
+ if (err.code === "ECONNREFUSED") {
72
+ reject(new Error("Connection refused — simulator is not running or remote control is disabled. Start with crosspad_run first."));
73
+ } else {
74
+ reject(new Error(`TCP error: ${err.message}`));
75
+ }
76
+ });
77
+
78
+ socket.on("close", () => {
79
+ if (!resolved) {
80
+ resolved = true;
81
+ if (buffer.length > 0) {
82
+ try {
83
+ resolve(JSON.parse(buffer) as RemoteResponse);
84
+ } catch {
85
+ resolve({ ok: false, error: "incomplete response", raw: buffer });
86
+ }
87
+ } else {
88
+ reject(new Error("Connection closed without response"));
89
+ }
90
+ }
91
+ });
92
+
93
+ socket.connect(REMOTE_PORT, REMOTE_HOST);
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Check if the simulator's remote control server is reachable.
99
+ */
100
+ export async function isSimulatorRunning(): Promise<boolean> {
101
+ try {
102
+ const resp = await sendRemoteCommand({ cmd: "ping" });
103
+ return resp.ok === true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }