xcode-mcli 0.1.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 (42) hide show
  1. package/README.md +92 -0
  2. package/bin/xcode-mcli.ts +16 -0
  3. package/config/mcporter.json +11 -0
  4. package/package.json +57 -0
  5. package/skill/SKILL.md +70 -0
  6. package/skill/agents/openai.yaml +4 -0
  7. package/skill/references/api-reference.md +409 -0
  8. package/skill/references/apple-xcode-26.3.surface.json +1466 -0
  9. package/skill/references/compatibility.md +91 -0
  10. package/skill/references/setup.md +120 -0
  11. package/skill/references/troubleshooting.md +160 -0
  12. package/src/commands/daemon-restart.ts +22 -0
  13. package/src/commands/daemon-start.ts +22 -0
  14. package/src/commands/daemon-status.ts +29 -0
  15. package/src/commands/daemon-stop.ts +22 -0
  16. package/src/commands/index.ts +26 -0
  17. package/src/commands/project-build.ts +50 -0
  18. package/src/commands/setup.ts +26 -0
  19. package/src/commands/surface-snapshot.ts +45 -0
  20. package/src/commands/surface-verify.ts +46 -0
  21. package/src/commands/tool-command.ts +150 -0
  22. package/src/commands/windows-list.ts +43 -0
  23. package/src/commands/windows-use.ts +30 -0
  24. package/src/commands/xcode-tool-commands.ts +460 -0
  25. package/src/constants.ts +3 -0
  26. package/src/core/command-definition.ts +131 -0
  27. package/src/core/command-dispatch.ts +97 -0
  28. package/src/core/contracts.ts +25 -0
  29. package/src/core/errors.ts +52 -0
  30. package/src/core/package-version.ts +30 -0
  31. package/src/runtime/daemon-host-entry.ts +8 -0
  32. package/src/runtime/daemon-host.ts +455 -0
  33. package/src/runtime/daemon-state.ts +75 -0
  34. package/src/runtime/env.ts +37 -0
  35. package/src/runtime/mcp-jsonrpc.ts +114 -0
  36. package/src/runtime/output.ts +128 -0
  37. package/src/runtime/tab-resolver.ts +44 -0
  38. package/src/runtime/xcode-client.ts +192 -0
  39. package/src/runtime/xcode-surface.ts +166 -0
  40. package/src/runtime/xcode-tool-definition.ts +14 -0
  41. package/src/runtime/xcode-windows.ts +65 -0
  42. package/src/runtime/xcrun.ts +39 -0
@@ -0,0 +1,128 @@
1
+ import { EXIT_USAGE_ERROR } from "../constants.ts";
2
+ import type { GlobalOptions } from "../core/contracts.ts";
3
+ import type { TemplateError } from "../core/errors.ts";
4
+
5
+ type CommandResultInput = {
6
+ commandPath: readonly string[];
7
+ data?: unknown;
8
+ globals: GlobalOptions;
9
+ tabIdentifier?: string;
10
+ text?: string;
11
+ tool?: string;
12
+ };
13
+
14
+ type CommandErrorInput = {
15
+ argv: string[];
16
+ error: TemplateError;
17
+ };
18
+
19
+ function commandPathToString(commandPath: readonly string[]): string {
20
+ return commandPath.join(" ");
21
+ }
22
+
23
+ type JsonSuccessEnvelope = {
24
+ command: string;
25
+ data: unknown;
26
+ ok: true;
27
+ tabIdentifier?: string;
28
+ tool?: string;
29
+ };
30
+
31
+ const GLOBAL_BOOLEAN_FLAGS = new Set(["--help", "--json", "--verbose", "--version"]);
32
+ const GLOBAL_VALUE_FLAGS = new Set(["--tab-identifier"]);
33
+
34
+ function toJsonSuccessEnvelope(input: CommandResultInput): JsonSuccessEnvelope {
35
+ return {
36
+ ok: true,
37
+ command: commandPathToString(input.commandPath),
38
+ data: input.data ?? {},
39
+ tabIdentifier: input.tabIdentifier,
40
+ tool: input.tool,
41
+ };
42
+ }
43
+
44
+ function readGlobalOptionsFromArgv(argv: string[]): GlobalOptions {
45
+ return {
46
+ json: argv.includes("--json"),
47
+ verbose: argv.includes("--verbose"),
48
+ };
49
+ }
50
+
51
+ function readCommandNameFromArgv(argv: string[]): string {
52
+ const commandTokens: string[] = [];
53
+ let reachedCommand = false;
54
+ for (let index = 0; index < argv.length; index += 1) {
55
+ const token = argv[index];
56
+ if (typeof token !== "string") {
57
+ break;
58
+ }
59
+ if (!reachedCommand && GLOBAL_BOOLEAN_FLAGS.has(token)) {
60
+ continue;
61
+ }
62
+ if (!reachedCommand && GLOBAL_VALUE_FLAGS.has(token)) {
63
+ index += 1;
64
+ continue;
65
+ }
66
+ if (token.startsWith("-")) {
67
+ break;
68
+ }
69
+ reachedCommand = true;
70
+ commandTokens.push(token);
71
+ }
72
+ return commandTokens.join(" ");
73
+ }
74
+
75
+ function readErrorKind(error: TemplateError): "runtime" | "usage" {
76
+ return error.exitCode === EXIT_USAGE_ERROR ? "usage" : "runtime";
77
+ }
78
+
79
+ export function toCommandData(input: {
80
+ structuredContent?: unknown;
81
+ text?: string;
82
+ }): unknown {
83
+ if (input.structuredContent !== undefined) {
84
+ return input.structuredContent;
85
+ }
86
+ const text = input.text?.trimEnd() ?? "";
87
+ if (text.length > 0) {
88
+ return {
89
+ text,
90
+ };
91
+ }
92
+ return {};
93
+ }
94
+
95
+ export function printVerboseTool(globals: GlobalOptions, toolName: string): void {
96
+ if (!globals.verbose) {
97
+ return;
98
+ }
99
+ console.error(`Xcode MCP tool: ${toolName}`);
100
+ }
101
+
102
+ export function printCommandResult(input: CommandResultInput): void {
103
+ if (input.globals.json) {
104
+ console.log(JSON.stringify(toJsonSuccessEnvelope(input)));
105
+ return;
106
+ }
107
+ console.log(input.text ?? "");
108
+ }
109
+
110
+ export function printCommandError(input: CommandErrorInput): void {
111
+ const globals = readGlobalOptionsFromArgv(input.argv);
112
+ if (!globals.json) {
113
+ console.error(input.error.message);
114
+ return;
115
+ }
116
+ console.log(
117
+ JSON.stringify({
118
+ ok: false,
119
+ command: input.error.commandName ?? readCommandNameFromArgv(input.argv),
120
+ tool: input.error.toolName,
121
+ error: {
122
+ kind: readErrorKind(input.error),
123
+ message: input.error.message,
124
+ details: input.error.details,
125
+ },
126
+ }),
127
+ );
128
+ }
@@ -0,0 +1,44 @@
1
+ import { runtimeError } from "../core/errors.ts";
2
+ import { callDaemonTool } from "./daemon-host.ts";
3
+ import {
4
+ readDaemonState,
5
+ setActiveTabIdentifier,
6
+ setLastSeenWindows,
7
+ } from "./daemon-state.ts";
8
+ import { readWindowsFromToolResult, xcodeWindowsToolResultSchema } from "./xcode-windows.ts";
9
+
10
+ export async function resolveTabIdentifier(input: {
11
+ explicitTabIdentifier?: string;
12
+ }): Promise<string> {
13
+ const explicitTabIdentifier = input.explicitTabIdentifier?.trim();
14
+ if (explicitTabIdentifier) {
15
+ await setActiveTabIdentifier(explicitTabIdentifier);
16
+ return explicitTabIdentifier;
17
+ }
18
+ const daemonState = await readDaemonState();
19
+ if (daemonState.activeTabIdentifier) {
20
+ return daemonState.activeTabIdentifier;
21
+ }
22
+ const result = xcodeWindowsToolResultSchema.parse(
23
+ await callDaemonTool({
24
+ name: "XcodeListWindows",
25
+ arguments: {},
26
+ }),
27
+ );
28
+ const windows = readWindowsFromToolResult(result);
29
+ await setLastSeenWindows(windows);
30
+ if (windows.length === 1) {
31
+ const firstWindow = windows[0];
32
+ if (!firstWindow) {
33
+ throw runtimeError("No active Xcode workspace window found.");
34
+ }
35
+ await setActiveTabIdentifier(firstWindow.tabIdentifier);
36
+ return firstWindow.tabIdentifier;
37
+ }
38
+ if (windows.length === 0) {
39
+ throw runtimeError("No active Xcode workspace window found.");
40
+ }
41
+ throw runtimeError(
42
+ "Multiple Xcode windows are open. Run `xcode-mcli windows list` and `windows use --tab-identifier <id>`.",
43
+ );
44
+ }
@@ -0,0 +1,192 @@
1
+ import { spawn } from "node:child_process";
2
+ import { z } from "zod";
3
+
4
+ import { runtimeError } from "../core/errors.ts";
5
+ import { resolveXcrunPath } from "./env.ts";
6
+ import { JsonRpcPeer } from "./mcp-jsonrpc.ts";
7
+ import { type XcodeToolDefinition, xcodeToolDefinitionSchema } from "./xcode-tool-definition.ts";
8
+ import { normalizeXcodeToolText } from "./xcode-windows.ts";
9
+
10
+ type McpSurfaceEntry = {
11
+ name: string;
12
+ [key: string]: unknown;
13
+ };
14
+
15
+ type XcodeMcpSurface = {
16
+ protocolVersion: string;
17
+ prompts: McpSurfaceEntry[];
18
+ resources: McpSurfaceEntry[];
19
+ tools: XcodeToolDefinition[];
20
+ };
21
+
22
+ type XcodeToolCallResult = {
23
+ content: Array<Record<string, unknown>>;
24
+ isError: boolean;
25
+ structuredContent?: unknown;
26
+ text: string;
27
+ };
28
+
29
+ type XcodeBridgeClient = {
30
+ bridgeProcessId?: number;
31
+ callTool(
32
+ input: { arguments: Record<string, unknown>; name: string },
33
+ ): Promise<XcodeToolCallResult>;
34
+ close(): Promise<void>;
35
+ listPrompts(): Promise<McpSurfaceEntry[]>;
36
+ listResources(): Promise<McpSurfaceEntry[]>;
37
+ listTools(): Promise<XcodeToolDefinition[]>;
38
+ protocolVersion: string;
39
+ };
40
+
41
+ const MCP_PROTOCOL_VERSION = "2025-06-18";
42
+ const initializeResultSchema = z.object({
43
+ protocolVersion: z.string().min(1),
44
+ });
45
+ const mcpSurfaceEntrySchema = z.object({
46
+ name: z.string().min(1),
47
+ }).catchall(z.unknown());
48
+
49
+ function readTextContent(content: Array<Record<string, unknown>>): string {
50
+ return normalizeXcodeToolText(
51
+ content
52
+ .filter((item) => item.type === "text" && typeof item.text === "string")
53
+ .map((item) => String(item.text))
54
+ .join(""),
55
+ );
56
+ }
57
+
58
+ function parseStructuredContentFromText(text: string): unknown {
59
+ const trimmed = text.trim();
60
+ if (trimmed.length === 0) {
61
+ return undefined;
62
+ }
63
+ let parsed: unknown;
64
+ try {
65
+ parsed = JSON.parse(trimmed);
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ if (!parsed || typeof parsed !== "object") {
70
+ return undefined;
71
+ }
72
+ if ("message" in parsed && typeof parsed.message === "string") {
73
+ return undefined;
74
+ }
75
+ return parsed;
76
+ }
77
+
78
+ function toToolCallResult(result: unknown): XcodeToolCallResult {
79
+ const payload = toolCallResultSchema.parse(result ?? {});
80
+ const content = payload.content;
81
+ const text = readTextContent(content);
82
+ return {
83
+ content,
84
+ isError: payload.isError,
85
+ structuredContent: payload.structuredContent ?? parseStructuredContentFromText(text),
86
+ text,
87
+ };
88
+ }
89
+
90
+ async function initializeClient(peer: JsonRpcPeer): Promise<string> {
91
+ const result = initializeResultSchema.parse(
92
+ await peer.request("initialize", {
93
+ protocolVersion: MCP_PROTOCOL_VERSION,
94
+ capabilities: {},
95
+ clientInfo: {
96
+ name: "xcode-mcli",
97
+ version: "0.1.0",
98
+ },
99
+ }),
100
+ );
101
+ if (!result.protocolVersion) {
102
+ throw runtimeError("Bridge initialize did not return a protocol version.");
103
+ }
104
+ peer.notify("notifications/initialized", {});
105
+ return result.protocolVersion;
106
+ }
107
+
108
+ const listToolsResultSchema = z.object({
109
+ tools: z.array(xcodeToolDefinitionSchema).default([]),
110
+ });
111
+ const listPromptsResultSchema = z.object({
112
+ prompts: z.array(mcpSurfaceEntrySchema).default([]),
113
+ });
114
+ const listResourcesResultSchema = z.object({
115
+ resources: z.array(mcpSurfaceEntrySchema).default([]),
116
+ });
117
+ const toolCallResultSchema = z.object({
118
+ content: z.array(z.record(z.string(), z.unknown())).default([]),
119
+ isError: z.boolean().default(false),
120
+ structuredContent: z.unknown().optional(),
121
+ });
122
+
123
+ export async function createXcodeBridgeClient(): Promise<XcodeBridgeClient> {
124
+ const child = spawn(resolveXcrunPath(), ["mcpbridge"], {
125
+ stdio: ["pipe", "pipe", "pipe"],
126
+ env: process.env,
127
+ });
128
+ const peer = new JsonRpcPeer(child);
129
+ const protocolVersion = await initializeClient(peer);
130
+ let cachedTools: XcodeToolDefinition[] | null = null;
131
+ let cachedPrompts: McpSurfaceEntry[] | null = null;
132
+ let cachedResources: McpSurfaceEntry[] | null = null;
133
+ return {
134
+ protocolVersion,
135
+ bridgeProcessId: child.pid,
136
+ listTools: async () => {
137
+ if (cachedTools) {
138
+ return cachedTools;
139
+ }
140
+ const result = listToolsResultSchema.parse(await peer.request("tools/list", {}));
141
+ cachedTools = result.tools;
142
+ return cachedTools;
143
+ },
144
+ listPrompts: async () => {
145
+ if (cachedPrompts) {
146
+ return cachedPrompts;
147
+ }
148
+ const result = listPromptsResultSchema.parse(await peer.request("prompts/list", {}));
149
+ cachedPrompts = result.prompts;
150
+ return cachedPrompts;
151
+ },
152
+ listResources: async () => {
153
+ if (cachedResources) {
154
+ return cachedResources;
155
+ }
156
+ const result = listResourcesResultSchema.parse(
157
+ await peer.request("resources/list", {}),
158
+ );
159
+ cachedResources = result.resources;
160
+ return cachedResources;
161
+ },
162
+ callTool: async ({ name, arguments: toolArguments }) => {
163
+ const result = await peer.request("tools/call", {
164
+ name,
165
+ arguments: toolArguments,
166
+ });
167
+ return toToolCallResult(result);
168
+ },
169
+ close: async () => {
170
+ await peer.close();
171
+ },
172
+ };
173
+ }
174
+
175
+ export async function readXcodeMcpSurface(): Promise<XcodeMcpSurface> {
176
+ const client = await createXcodeBridgeClient();
177
+ try {
178
+ const [tools, prompts, resources] = await Promise.all([
179
+ client.listTools(),
180
+ client.listPrompts(),
181
+ client.listResources(),
182
+ ]);
183
+ return {
184
+ protocolVersion: client.protocolVersion,
185
+ tools,
186
+ prompts,
187
+ resources,
188
+ };
189
+ } finally {
190
+ await client.close();
191
+ }
192
+ }
@@ -0,0 +1,166 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { z } from "zod";
4
+
5
+ import { runtimeError } from "../core/errors.ts";
6
+ import { readXcodeMcpSurface } from "./xcode-client.ts";
7
+
8
+ type SurfaceItem = {
9
+ name: string;
10
+ [key: string]: unknown;
11
+ };
12
+
13
+ type SurfaceDelta = {
14
+ added: string[];
15
+ changed: string[];
16
+ removed: string[];
17
+ };
18
+
19
+ export type XcodeMcpSurfaceSnapshot = {
20
+ protocolVersion: string;
21
+ prompts: SurfaceItem[];
22
+ resources: SurfaceItem[];
23
+ tools: SurfaceItem[];
24
+ };
25
+
26
+ export type XcodeMcpSurfaceDiff = {
27
+ protocolVersionChanged: boolean;
28
+ prompts: SurfaceDelta;
29
+ resources: SurfaceDelta;
30
+ tools: SurfaceDelta;
31
+ };
32
+
33
+ const surfaceItemSchema = z.object({
34
+ name: z.string().min(1),
35
+ }).catchall(z.unknown());
36
+
37
+ const xcodeMcpSurfaceSnapshotSchema = z.object({
38
+ protocolVersion: z.string().min(1),
39
+ prompts: z.array(surfaceItemSchema).default([]),
40
+ resources: z.array(surfaceItemSchema).default([]),
41
+ tools: z.array(surfaceItemSchema).default([]),
42
+ });
43
+
44
+ function sortObjectKeys(value: unknown): unknown {
45
+ if (Array.isArray(value)) {
46
+ return value.map((item) => sortObjectKeys(item));
47
+ }
48
+ if (!value || typeof value !== "object") {
49
+ return value;
50
+ }
51
+ return Object.fromEntries(
52
+ Object.entries(value)
53
+ .sort(([left], [right]) => left.localeCompare(right))
54
+ .map(([key, itemValue]) => [key, sortObjectKeys(itemValue)]),
55
+ );
56
+ }
57
+
58
+ function normalizeNamedItems(items: SurfaceItem[]): SurfaceItem[] {
59
+ return items
60
+ .map((item) => surfaceItemSchema.parse(sortObjectKeys(item)))
61
+ .sort((left, right) => left.name.localeCompare(right.name));
62
+ }
63
+
64
+ function createSurfaceDelta(input: {
65
+ baseline: SurfaceItem[];
66
+ live: SurfaceItem[];
67
+ }): SurfaceDelta {
68
+ const baselineByName = new Map(input.baseline.map((item) => [item.name, item]));
69
+ const liveByName = new Map(input.live.map((item) => [item.name, item]));
70
+ const added = [...liveByName.keys()].filter((name) => !baselineByName.has(name)).sort();
71
+ const removed = [...baselineByName.keys()].filter((name) => !liveByName.has(name)).sort();
72
+ const changed = [...baselineByName.keys()]
73
+ .filter((name) => liveByName.has(name))
74
+ .filter((name) => {
75
+ return JSON.stringify(baselineByName.get(name)) !== JSON.stringify(liveByName.get(name));
76
+ })
77
+ .sort();
78
+ return {
79
+ added,
80
+ changed,
81
+ removed,
82
+ };
83
+ }
84
+
85
+ function hasDelta(delta: SurfaceDelta): boolean {
86
+ return delta.added.length > 0 || delta.changed.length > 0 || delta.removed.length > 0;
87
+ }
88
+
89
+ export function normalizeXcodeMcpSurfaceSnapshot(
90
+ snapshot: XcodeMcpSurfaceSnapshot,
91
+ ): XcodeMcpSurfaceSnapshot {
92
+ const parsed = xcodeMcpSurfaceSnapshotSchema.parse(snapshot);
93
+ return {
94
+ protocolVersion: parsed.protocolVersion,
95
+ prompts: normalizeNamedItems(parsed.prompts),
96
+ resources: normalizeNamedItems(parsed.resources),
97
+ tools: normalizeNamedItems(parsed.tools),
98
+ };
99
+ }
100
+
101
+ export async function captureXcodeMcpSurfaceSnapshot(): Promise<XcodeMcpSurfaceSnapshot> {
102
+ return normalizeXcodeMcpSurfaceSnapshot(await readXcodeMcpSurface());
103
+ }
104
+
105
+ export function diffXcodeMcpSurfaceSnapshots(input: {
106
+ baseline: XcodeMcpSurfaceSnapshot;
107
+ live: XcodeMcpSurfaceSnapshot;
108
+ }): XcodeMcpSurfaceDiff {
109
+ const baseline = normalizeXcodeMcpSurfaceSnapshot(input.baseline);
110
+ const live = normalizeXcodeMcpSurfaceSnapshot(input.live);
111
+ return {
112
+ protocolVersionChanged: baseline.protocolVersion !== live.protocolVersion,
113
+ prompts: createSurfaceDelta({
114
+ baseline: baseline.prompts,
115
+ live: live.prompts,
116
+ }),
117
+ resources: createSurfaceDelta({
118
+ baseline: baseline.resources,
119
+ live: live.resources,
120
+ }),
121
+ tools: createSurfaceDelta({
122
+ baseline: baseline.tools,
123
+ live: live.tools,
124
+ }),
125
+ };
126
+ }
127
+
128
+ export function xcodeMcpSurfaceDiffHasChanges(diff: XcodeMcpSurfaceDiff): boolean {
129
+ return diff.protocolVersionChanged
130
+ || hasDelta(diff.prompts)
131
+ || hasDelta(diff.resources)
132
+ || hasDelta(diff.tools);
133
+ }
134
+
135
+ export async function readXcodeMcpSurfaceSnapshotFile(
136
+ projectDir: string,
137
+ filePath: string,
138
+ ): Promise<XcodeMcpSurfaceSnapshot> {
139
+ const resolvedPath = resolve(projectDir, filePath);
140
+ try {
141
+ return normalizeXcodeMcpSurfaceSnapshot(
142
+ JSON.parse(await readFile(resolvedPath, "utf8")),
143
+ );
144
+ } catch (error) {
145
+ if (error instanceof Error) {
146
+ throw runtimeError(
147
+ `Failed to read Xcode MCP surface snapshot from ${resolvedPath}. ${error.message}`,
148
+ );
149
+ }
150
+ throw runtimeError(`Failed to read Xcode MCP surface snapshot from ${resolvedPath}.`);
151
+ }
152
+ }
153
+
154
+ export async function writeXcodeMcpSurfaceSnapshotFile(input: {
155
+ filePath: string;
156
+ projectDir: string;
157
+ snapshot: XcodeMcpSurfaceSnapshot;
158
+ }): Promise<string> {
159
+ const resolvedPath = resolve(input.projectDir, input.filePath);
160
+ await mkdir(dirname(resolvedPath), { recursive: true });
161
+ await writeFile(
162
+ resolvedPath,
163
+ `${JSON.stringify(normalizeXcodeMcpSurfaceSnapshot(input.snapshot), null, 2)}\n`,
164
+ );
165
+ return resolvedPath;
166
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+
3
+ export type XcodeToolDefinition = {
4
+ description?: string;
5
+ inputSchema?: Record<string, unknown>;
6
+ name: string;
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ export const xcodeToolDefinitionSchema = z.object({
11
+ name: z.string().min(1),
12
+ description: z.string().optional(),
13
+ inputSchema: z.record(z.string(), z.unknown()).optional(),
14
+ }).catchall(z.unknown());
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+
3
+ const wrappedMessageSchema = z.object({
4
+ message: z.string(),
5
+ });
6
+
7
+ export const xcodeWindowSchema = z.object({
8
+ tabIdentifier: z.string().min(1),
9
+ workspacePath: z.string().min(1),
10
+ });
11
+
12
+ export const xcodeWindowsToolResultSchema = z.object({
13
+ structuredContent: z
14
+ .object({
15
+ windows: z.array(xcodeWindowSchema),
16
+ })
17
+ .optional(),
18
+ text: z.string().default(""),
19
+ });
20
+
21
+ export function readWindowsFromToolResult(input: {
22
+ structuredContent?: {
23
+ windows: Array<z.infer<typeof xcodeWindowSchema>>;
24
+ };
25
+ text: string;
26
+ }): Array<z.infer<typeof xcodeWindowSchema>> {
27
+ return input.structuredContent?.windows ?? parseWindowsFromText(input.text);
28
+ }
29
+
30
+ export function normalizeXcodeToolText(text: string): string {
31
+ const trimmed = text.trim();
32
+ if (!trimmed.startsWith("{")) {
33
+ return text;
34
+ }
35
+ let decoded: unknown;
36
+ try {
37
+ decoded = JSON.parse(trimmed);
38
+ } catch {
39
+ return text;
40
+ }
41
+ const parsed = wrappedMessageSchema.safeParse(decoded);
42
+ if (!parsed.success) {
43
+ return text;
44
+ }
45
+ return parsed.data.message;
46
+ }
47
+
48
+ export function parseWindowsFromText(text: string): Array<z.infer<typeof xcodeWindowSchema>> {
49
+ return normalizeXcodeToolText(text)
50
+ .split("\n")
51
+ .map((line) => line.trim())
52
+ .filter((line) => line.startsWith("* tabIdentifier: "))
53
+ .flatMap((line) => {
54
+ const match = /^\* tabIdentifier: ([^,]+), workspacePath: (.+)$/.exec(line);
55
+ if (!match?.[1] || !match[2]) {
56
+ return [];
57
+ }
58
+ return [
59
+ {
60
+ tabIdentifier: match[1],
61
+ workspacePath: match[2],
62
+ },
63
+ ];
64
+ });
65
+ }
@@ -0,0 +1,39 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ import { runtimeError } from "../core/errors.ts";
5
+ import { resolveXcrunPath } from "./env.ts";
6
+
7
+ const execFile = promisify(execFileCallback);
8
+ const XCODE_TOOLS_GUIDANCE = "Enable Xcode Tools in Settings > Intelligence.";
9
+
10
+ function toXcrunRuntimeError(error: unknown, message: string) {
11
+ if (error instanceof Error) {
12
+ return runtimeError(`${message} ${error.message}`);
13
+ }
14
+ return runtimeError(message);
15
+ }
16
+
17
+ export async function findMcpbridgePath(): Promise<string> {
18
+ try {
19
+ const { stdout } = await execFile(resolveXcrunPath(), ["--find", "mcpbridge"]);
20
+ const bridgePath = stdout.trim();
21
+ if (bridgePath.length > 0) {
22
+ return bridgePath;
23
+ }
24
+ throw runtimeError("xcrun did not return a mcpbridge path.");
25
+ } catch (error) {
26
+ throw toXcrunRuntimeError(error, "Failed to locate xcrun mcpbridge.");
27
+ }
28
+ }
29
+
30
+ export async function verifyMcpbridgeHelp(): Promise<void> {
31
+ try {
32
+ await execFile(resolveXcrunPath(), ["mcpbridge", "--help"]);
33
+ } catch (error) {
34
+ throw toXcrunRuntimeError(
35
+ error,
36
+ `Failed to call xcrun mcpbridge --help. ${XCODE_TOOLS_GUIDANCE}`,
37
+ );
38
+ }
39
+ }