diff --git a/manifests/tools/clone_sims.yaml b/manifests/tools/clone_sims.yaml new file mode 100644 index 00000000..a883fd28 --- /dev/null +++ b/manifests/tools/clone_sims.yaml @@ -0,0 +1,25 @@ +id: clone_sims +module: mcp/tools/simulator-management/clone_sims +names: + mcp: clone_sims + cli: clone +description: Clone an existing simulator. +outputSchema: + schema: xcodebuildmcp.output.simulator-action-result + version: "1" +annotations: + title: Clone Simulator + readOnlyHint: false + destructiveHint: false + openWorldHint: false +nextSteps: + - label: List simulators to see the clone + toolId: list_sims + priority: 1 + when: success + - label: Boot the cloned simulator + toolId: boot_sim + params: + simulatorId: NEW_UDID + priority: 2 + when: success diff --git a/manifests/tools/create_sim.yaml b/manifests/tools/create_sim.yaml new file mode 100644 index 00000000..cac18dc4 --- /dev/null +++ b/manifests/tools/create_sim.yaml @@ -0,0 +1,25 @@ +id: create_sim +module: mcp/tools/simulator-management/create_sim +names: + mcp: create_sim + cli: create +description: Create a new simulator. +outputSchema: + schema: xcodebuildmcp.output.simulator-action-result + version: "1" +annotations: + title: Create Simulator + readOnlyHint: false + destructiveHint: false + openWorldHint: false +nextSteps: + - label: List simulators to see the new device + toolId: list_sims + priority: 1 + when: success + - label: Boot the new simulator + toolId: boot_sim + params: + simulatorId: NEW_UDID + priority: 2 + when: success diff --git a/manifests/tools/delete_sims.yaml b/manifests/tools/delete_sims.yaml new file mode 100644 index 00000000..966d8c99 --- /dev/null +++ b/manifests/tools/delete_sims.yaml @@ -0,0 +1,19 @@ +id: delete_sims +module: mcp/tools/simulator-management/delete_sims +names: + mcp: delete_sims + cli: delete +description: Delete simulators by UDID, all simulators, or unavailable simulators. +outputSchema: + schema: xcodebuildmcp.output.simulator-action-result + version: "1" +annotations: + title: Delete Simulators + readOnlyHint: false + destructiveHint: true + openWorldHint: false +nextSteps: + - label: List remaining simulators + toolId: list_sims + priority: 1 + when: success diff --git a/manifests/workflows/simulator-management.yaml b/manifests/workflows/simulator-management.yaml index c1ce1ddc..7afe8ef2 100644 --- a/manifests/workflows/simulator-management.yaml +++ b/manifests/workflows/simulator-management.yaml @@ -10,6 +10,9 @@ tools: - set_sim_location - reset_sim_location - set_sim_appearance + - clone_sims + - create_sim + - delete_sims - sim_statusbar - toggle_software_keyboard - toggle_connect_hardware_keyboard diff --git a/src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts new file mode 100644 index 00000000..306203f6 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { schema, clone_simsLogic } from '../clone_sims.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; + +describe('clone_sims tool', () => { + describe('Plugin Structure', () => { + it('should expose schema', () => { + expect(schema).toBeDefined(); + }); + }); + + describe('Happy path', () => { + it('clones a simulator and captures the new UDID', async () => { + const newUdid = '00000000-0000-0000-0000-000000000001'; + const mock = createMockExecutor({ success: true, output: `${newUdid}\n` }); + const res = await runLogic(() => + clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBeFalsy(); + const text = allText(res); + expect(text).toContain('Simulator cloned successfully'); + }); + + it('clones with a custom name', async () => { + const mock = createMockExecutor({ success: true, output: 'UUID1\n' }); + const res = await runLogic(() => + clone_simsLogic( + { + sourceSimulatorId: '00000000-0000-0000-0000-000000000000', + newName: 'My Clone', + }, + mock, + ), + ); + expect(res.isError).toBeFalsy(); + }); + }); + + describe('Failure path', () => { + it('returns failure when clone fails', async () => { + const mock = createMockExecutor({ success: false, error: 'No such device' }); + const res = await runLogic(() => + clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBe(true); + const text = allText(res); + expect(text).toContain('Clone simulator failed'); + expect(text).toContain('No such device'); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/__tests__/create_sim.test.ts b/src/mcp/tools/simulator-management/__tests__/create_sim.test.ts new file mode 100644 index 00000000..581a4583 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/create_sim.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { schema, create_simLogic } from '../create_sim.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; + +describe('create_sim tool', () => { + describe('Plugin Structure', () => { + it('should expose schema', () => { + expect(schema).toBeDefined(); + }); + }); + + describe('Happy path', () => { + it('creates a simulator and captures the new UDID', async () => { + const newUdid = '00000000-0000-0000-0000-000000000001'; + const mock = createMockExecutor({ success: true, output: `${newUdid}\n` }); + const res = await runLogic(() => + create_simLogic( + { + name: 'Test Sim', + deviceType: 'iPhone 17', + runtime: 'iOS 26.4', + }, + mock, + ), + ); + expect(res.isError).toBeFalsy(); + const text = allText(res); + expect(text).toContain('Simulator created successfully'); + }); + }); + + describe('Failure path', () => { + it('returns failure when create fails', async () => { + const mock = createMockExecutor({ success: false, error: 'Invalid device type' }); + const res = await runLogic(() => + create_simLogic( + { + name: 'Bad Sim', + deviceType: 'NonExistent', + runtime: 'iOS 99', + }, + mock, + ), + ); + expect(res.isError).toBe(true); + const text = allText(res); + expect(text).toContain('Create simulator failed'); + expect(text).toContain('Invalid device type'); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts new file mode 100644 index 00000000..0ba52220 --- /dev/null +++ b/src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest'; +import { schema, delete_simsLogic } from '../delete_sims.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; + +describe('delete_sims tool', () => { + describe('Plugin Structure', () => { + it('should expose schema', () => { + expect(schema).toBeDefined(); + }); + }); + + describe('Happy path', () => { + it('deletes a simulator by UDID', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await runLogic(() => + delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBeFalsy(); + const text = allText(res); + expect(text).toContain('Simulator(s) deleted successfully'); + }); + + it('deletes all simulators', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await runLogic(() => delete_simsLogic({ target: 'all' }, mock)); + expect(res.isError).toBeFalsy(); + }); + + it('deletes unavailable simulators', async () => { + const mock = createMockExecutor({ success: true, output: 'OK' }); + const res = await runLogic(() => delete_simsLogic({ target: 'unavailable' }, mock)); + expect(res.isError).toBeFalsy(); + }); + }); + + describe('Shutdown first', () => { + it('shuts down before deleting when shutdownFirst=true', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + const res = await runLogic(() => + delete_simsLogic( + { target: '00000000-0000-0000-0000-000000000000', shutdownFirst: true }, + exec as any, + ), + ); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'shutdown', '00000000-0000-0000-0000-000000000000'], + ['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'], + ]); + expect(res.isError).toBeFalsy(); + }); + + it('shuts down all before deleting when target=all', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + const res = await runLogic(() => + delete_simsLogic({ target: 'all', shutdownFirst: true }, exec as any), + ); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'shutdown', 'all'], + ['xcrun', 'simctl', 'delete', 'all'], + ]); + expect(res.isError).toBeFalsy(); + }); + + it('skips shutdown when target=unavailable', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + const res = await runLogic(() => + delete_simsLogic({ target: 'unavailable', shutdownFirst: true }, exec as any), + ); + expect(calls).toEqual([['xcrun', 'simctl', 'delete', 'unavailable']]); + expect(res.isError).toBeFalsy(); + }); + + it('does not shut down when shutdownFirst is not set', async () => { + const calls: any[] = []; + const exec = async (cmd: string[]) => { + calls.push(cmd); + return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; + }; + await runLogic(() => + delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, exec as any), + ); + expect(calls).toEqual([ + ['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'], + ]); + }); + }); + + describe('Failure path', () => { + it('returns failure when delete fails', async () => { + const mock = createMockExecutor({ success: false, error: 'Unable to delete' }); + const res = await runLogic(() => + delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock), + ); + expect(res.isError).toBe(true); + const text = allText(res); + expect(text).toContain('Failed to delete simulator'); + expect(text).toContain('Unable to delete'); + }); + }); +}); diff --git a/src/mcp/tools/simulator-management/clone_sims.ts b/src/mcp/tools/simulator-management/clone_sims.ts new file mode 100644 index 00000000..d0b1f6ab --- /dev/null +++ b/src/mcp/tools/simulator-management/clone_sims.ts @@ -0,0 +1,147 @@ +import * as z from 'zod'; +import type { ToolHandlerContext } from '../../../rendering/types.ts'; +import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts'; +import type { NonStreamingExecutor } from '../../../types/tool-execution.ts'; +import { log } from '../../../utils/logging/index.ts'; +import type { CommandExecutor } from '../../../utils/execution/index.ts'; +import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { + createSessionAwareTool, + getSessionAwareToolSchemaShape, + getHandlerContext, + toInternalSchema, +} from '../../../utils/typed-tool-factory.ts'; +import { toErrorMessage } from '../../../utils/errors.ts'; +import { createBasicDiagnostics } from '../../../utils/diagnostics.ts'; + +const baseSchemaObject = z.object({ + sourceSimulatorId: z.string().uuid().describe('UDID of the simulator to clone'), + newName: z + .string() + .optional() + .describe('Name for the cloned simulator. If omitted, simctl auto-generates one.'), +}); + +const internalSchemaObject = z.object({ + sourceSimulatorId: z.string().uuid(), + newName: z.string().optional(), +}); + +type CloneSimsParams = z.infer; +type CloneSimsResult = SimulatorActionResultDomainResult; + +function createCloneSimsResult(params: { + sourceSimulatorId: string; + clonedSimulatorId?: string; + didError: boolean; + error?: string; + diagnosticMessage?: string; +}): CloneSimsResult { + return { + kind: 'simulator-action-result', + didError: params.didError, + error: params.error ?? null, + summary: { + status: params.didError ? 'FAILED' : 'SUCCEEDED', + }, + action: { + type: 'clone', + sourceSimulatorId: params.sourceSimulatorId, + }, + ...(params.diagnosticMessage + ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } + : {}), + artifacts: { + simulatorId: params.clonedSimulatorId ?? params.sourceSimulatorId, + }, + }; +} + +function setStructuredOutput(ctx: ToolHandlerContext, result: CloneSimsResult): void { + ctx.structuredOutput = { + result, + schema: 'xcodebuildmcp.output.simulator-action-result', + schemaVersion: '1', + }; +} + +export function createCloneSimsExecutor( + executor: CommandExecutor, +): NonStreamingExecutor { + return async (params) => { + try { + const command = ['xcrun', 'simctl', 'clone', params.sourceSimulatorId]; + if (params.newName) { + command.push(params.newName); + } + + const result = await executor(command, 'Clone Simulator', false); + + if (!result.success) { + const diagnosticMessage = result.error ?? 'Unknown error'; + return createCloneSimsResult({ + sourceSimulatorId: params.sourceSimulatorId, + didError: true, + error: 'Clone simulator failed.', + diagnosticMessage, + }); + } + + const clonedSimulatorId = result.output.trim(); + return createCloneSimsResult({ + sourceSimulatorId: params.sourceSimulatorId, + clonedSimulatorId, + didError: false, + }); + } catch (error) { + const diagnosticMessage = toErrorMessage(error); + return createCloneSimsResult({ + sourceSimulatorId: params.sourceSimulatorId, + didError: true, + error: 'Clone simulator failed.', + diagnosticMessage, + }); + } + }; +} + +export async function clone_simsLogic( + params: CloneSimsParams, + executor: CommandExecutor, +): Promise { + log( + 'info', + `Cloning simulator ${params.sourceSimulatorId}${params.newName ? ` as "${params.newName}"` : ''}`, + ); + + const ctx = getHandlerContext(); + const executeCloneSims = createCloneSimsExecutor(executor); + const result = await executeCloneSims(params); + setStructuredOutput(ctx, result); + + if (result.didError) { + log('error', `Error cloning simulator: ${result.error ?? 'Unknown error'}`); + return; + } + + const newSimulatorId = result.artifacts?.simulatorId ?? params.sourceSimulatorId; + ctx.nextStepParams = { + boot_sim: { simulatorId: newSimulatorId }, + open_sim: {}, + install_app_sim: { simulatorId: newSimulatorId, appPath: 'PATH_TO_YOUR_APP' }, + launch_app_sim: { simulatorId: newSimulatorId, bundleId: 'YOUR_APP_BUNDLE_ID' }, + list_sims: {}, + }; +} + +export const schema = getSessionAwareToolSchemaShape({ + sessionAware: baseSchemaObject, + legacy: baseSchemaObject, +}); + +export const handler = createSessionAwareTool({ + internalSchema: toInternalSchema(internalSchemaObject), + logicFunction: clone_simsLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [], +}); diff --git a/src/mcp/tools/simulator-management/create_sim.ts b/src/mcp/tools/simulator-management/create_sim.ts new file mode 100644 index 00000000..d27882c9 --- /dev/null +++ b/src/mcp/tools/simulator-management/create_sim.ts @@ -0,0 +1,161 @@ +import * as z from 'zod'; +import type { ToolHandlerContext } from '../../../rendering/types.ts'; +import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts'; +import type { NonStreamingExecutor } from '../../../types/tool-execution.ts'; +import { log } from '../../../utils/logging/index.ts'; +import type { CommandExecutor } from '../../../utils/execution/index.ts'; +import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { + createSessionAwareTool, + getSessionAwareToolSchemaShape, + getHandlerContext, + toInternalSchema, +} from '../../../utils/typed-tool-factory.ts'; +import { toErrorMessage } from '../../../utils/errors.ts'; +import { createBasicDiagnostics } from '../../../utils/diagnostics.ts'; + +const baseSchemaObject = z.object({ + name: z.string().min(1).describe('Name for the new simulator (e.g., "iPhone 17 Test")'), + deviceType: z + .string() + .min(1) + .describe( + 'Device type identifier (e.g., "iPhone 17" or "com.apple.CoreSimulator.SimDeviceType.iPhone-17"). Use list_sims to see available device types.', + ), + runtime: z + .string() + .min(1) + .describe( + 'Runtime identifier (e.g., "iOS 26" or "com.apple.CoreSimulator.SimRuntime.iOS-26"). Use list_sims to see available runtimes.', + ), +}); + +const internalSchemaObject = z.object({ + name: z.string().min(1), + deviceType: z.string().min(1), + runtime: z.string().min(1), +}); + +type CreateSimParams = z.infer; +type CreateSimResult = SimulatorActionResultDomainResult; + +function createCreateSimResult(params: { + name: string; + deviceType: string; + runtime: string; + simulatorId?: string; + didError: boolean; + error?: string; + diagnosticMessage?: string; +}): CreateSimResult { + return { + kind: 'simulator-action-result', + didError: params.didError, + error: params.error ?? null, + summary: { + status: params.didError ? 'FAILED' : 'SUCCEEDED', + }, + action: { + type: 'create', + name: params.name, + deviceType: params.deviceType, + runtime: params.runtime, + }, + ...(params.diagnosticMessage + ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } + : {}), + ...(params.simulatorId ? { artifacts: { simulatorId: params.simulatorId } } : {}), + }; +} + +function setStructuredOutput(ctx: ToolHandlerContext, result: CreateSimResult): void { + ctx.structuredOutput = { + result, + schema: 'xcodebuildmcp.output.simulator-action-result', + schemaVersion: '1', + }; +} + +export function createCreateSimExecutor( + executor: CommandExecutor, +): NonStreamingExecutor { + return async (params) => { + try { + const result = await executor( + ['xcrun', 'simctl', 'create', params.name, params.deviceType, params.runtime], + 'Create Simulator', + false, + ); + + if (!result.success) { + const diagnosticMessage = result.error ?? 'Unknown error'; + return createCreateSimResult({ + name: params.name, + deviceType: params.deviceType, + runtime: params.runtime, + didError: true, + error: 'Create simulator failed.', + diagnosticMessage, + }); + } + + const simulatorId = result.output.trim(); + return createCreateSimResult({ + name: params.name, + deviceType: params.deviceType, + runtime: params.runtime, + simulatorId, + didError: false, + }); + } catch (error) { + const diagnosticMessage = toErrorMessage(error); + return createCreateSimResult({ + name: params.name, + deviceType: params.deviceType, + runtime: params.runtime, + didError: true, + error: 'Create simulator failed.', + diagnosticMessage, + }); + } + }; +} + +export async function create_simLogic( + params: CreateSimParams, + executor: CommandExecutor, +): Promise { + log( + 'info', + `Creating simulator "${params.name}" (device type: ${params.deviceType}, runtime: ${params.runtime})`, + ); + + const ctx = getHandlerContext(); + const executeCreateSim = createCreateSimExecutor(executor); + const result = await executeCreateSim(params); + setStructuredOutput(ctx, result); + + if (result.didError) { + log('error', `Error creating simulator: ${result.error ?? 'Unknown error'}`); + return; + } + + const newSimulatorId = result.artifacts?.simulatorId; + ctx.nextStepParams = { + boot_sim: newSimulatorId ? { simulatorId: newSimulatorId } : {}, + open_sim: {}, + list_sims: {}, + }; +} + +export const schema = getSessionAwareToolSchemaShape({ + sessionAware: baseSchemaObject, + legacy: baseSchemaObject, +}); + +export const handler = createSessionAwareTool({ + internalSchema: toInternalSchema(internalSchemaObject), + logicFunction: create_simLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [], +}); diff --git a/src/mcp/tools/simulator-management/delete_sims.ts b/src/mcp/tools/simulator-management/delete_sims.ts new file mode 100644 index 00000000..4548c1c1 --- /dev/null +++ b/src/mcp/tools/simulator-management/delete_sims.ts @@ -0,0 +1,158 @@ +import * as z from 'zod'; +import type { ToolHandlerContext } from '../../../rendering/types.ts'; +import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts'; +import type { NonStreamingExecutor } from '../../../types/tool-execution.ts'; +import { log } from '../../../utils/logging/index.ts'; +import type { CommandExecutor } from '../../../utils/execution/index.ts'; +import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { + createSessionAwareTool, + getSessionAwareToolSchemaShape, + getHandlerContext, + toInternalSchema, +} from '../../../utils/typed-tool-factory.ts'; +import { toErrorMessage } from '../../../utils/errors.ts'; +import { createBasicDiagnostics } from '../../../utils/diagnostics.ts'; + +const baseSchemaObject = z.object({ + target: z + .string() + .min(1) + .describe( + 'UDID of the simulator to delete, "all" to delete all simulators, or "unavailable" to delete unavailable simulators.', + ), + shutdownFirst: z + .boolean() + .optional() + .describe('Shutdown the simulator before deleting. Useful for booted simulators.'), +}); + +const internalSchemaObject = z.object({ + target: z.string().min(1), + shutdownFirst: z.boolean().optional(), +}); + +type DeleteSimsParams = z.infer; +type DeleteSimsResult = SimulatorActionResultDomainResult; + +function createDeleteSimsResult(params: { + target: string; + didError: boolean; + error?: string; + diagnosticMessage?: string; +}): DeleteSimsResult { + return { + kind: 'simulator-action-result', + didError: params.didError, + error: params.error ?? null, + summary: { + status: params.didError ? 'FAILED' : 'SUCCEEDED', + }, + action: { + type: 'delete', + target: params.target, + }, + ...(params.diagnosticMessage + ? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) } + : {}), + }; +} + +function setStructuredOutput(ctx: ToolHandlerContext, result: DeleteSimsResult): void { + ctx.structuredOutput = { + result, + schema: 'xcodebuildmcp.output.simulator-action-result', + schemaVersion: '1', + }; +} + +export function createDeleteSimsExecutor( + executor: CommandExecutor, +): NonStreamingExecutor { + return async (params) => { + try { + const target = params.target; + + if (params.shutdownFirst && target !== 'unavailable') { + try { + await executor( + ['xcrun', 'simctl', 'shutdown', target], + 'Shutdown Simulator', + true, + undefined, + ); + } catch { + // ignore shutdown errors; proceed to delete attempt + } + } + + const result = await executor( + ['xcrun', 'simctl', 'delete', target], + 'Delete Simulator', + true, + undefined, + ); + + if (!result.success) { + const diagnosticMessage = result.error ?? 'Unknown error'; + return createDeleteSimsResult({ + target, + didError: true, + error: 'Failed to delete simulator(s).', + diagnosticMessage, + }); + } + + return createDeleteSimsResult({ + target, + didError: false, + }); + } catch (error) { + const diagnosticMessage = toErrorMessage(error); + return createDeleteSimsResult({ + target: params.target, + didError: true, + error: 'Failed to delete simulator(s).', + diagnosticMessage, + }); + } + }; +} + +export async function delete_simsLogic( + params: DeleteSimsParams, + executor: CommandExecutor, +): Promise { + const target = params.target; + + log( + 'info', + `Deleting simulator(s) ${target}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, + ); + + const ctx = getHandlerContext(); + const executeDeleteSims = createDeleteSimsExecutor(executor); + const result = await executeDeleteSims(params); + setStructuredOutput(ctx, result); + + if (result.didError) { + log('error', `Error deleting simulators: ${result.error ?? 'Unknown error'}`); + return; + } + + ctx.nextStepParams = { + list_sims: {}, + }; +} + +export const schema = getSessionAwareToolSchemaShape({ + sessionAware: baseSchemaObject, + legacy: baseSchemaObject, +}); + +export const handler = createSessionAwareTool({ + internalSchema: toInternalSchema(internalSchemaObject), + logicFunction: delete_simsLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [], +}); diff --git a/src/types/domain-results.ts b/src/types/domain-results.ts index 236b7ca6..473596e2 100644 --- a/src/types/domain-results.ts +++ b/src/types/domain-results.ts @@ -421,8 +421,25 @@ export interface SimulatorActionToggleSoftwareKeyboard { export interface SimulatorActionToggleConnectHardwareKeyboard { type: 'toggle-connect-hardware-keyboard'; } +export interface SimulatorActionClone { + type: 'clone'; + sourceSimulatorId: string; +} +export interface SimulatorActionCreate { + type: 'create'; + name: string; + deviceType: string; + runtime: string; +} +export interface SimulatorActionDelete { + type: 'delete'; + target: string; +} export type SimulatorAction = | SimulatorActionBoot + | SimulatorActionClone + | SimulatorActionCreate + | SimulatorActionDelete | SimulatorActionErase | SimulatorActionOpen | SimulatorActionResetLocation diff --git a/src/utils/renderers/domain-result-text.ts b/src/utils/renderers/domain-result-text.ts index 40a5f618..4434a0ab 100644 --- a/src/utils/renderers/domain-result-text.ts +++ b/src/utils/renderers/domain-result-text.ts @@ -1162,6 +1162,9 @@ function createSimulatorActionItems( ): TextRenderableItem[] { const titleMap: Record = { boot: 'Boot Simulator', + clone: 'Clone Simulator', + create: 'Create Simulator', + delete: 'Delete Simulator', erase: 'Erase Simulator', open: 'Open Simulator', 'reset-location': 'Reset Location', @@ -1195,6 +1198,9 @@ function createSimulatorActionItems( } else { const successMessages: Record = { boot: 'Simulator booted successfully', + clone: 'Simulator cloned successfully', + create: 'Simulator created successfully', + delete: 'Simulator(s) deleted successfully', erase: 'Simulators were erased successfully', open: 'Simulator opened successfully', 'reset-location': 'Location successfully reset to default',