diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d07743fa..17a06a74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,27 +16,29 @@ jobs: steps: - uses: actions/checkout@v3 - + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' - + - name: Install dependencies run: npm ci - + - name: Build run: npm run build - + - name: Lint run: npm run lint - + - name: Check formatting run: npm run format:check - + - name: Type check run: npm run typecheck - name: Run tests run: npm test + + - run: npx pkg-pr-new publish diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml deleted file mode 100644 index 2afbf690..00000000 --- a/.github/workflows/pkg-pr-new.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Publish Package Previews - -on: - pull_request_review: - types: [submitted] - pull_request: - types: [opened, synchronize] - -permissions: - contents: read - pull-requests: write - -jobs: - # Publish on all PRs (for testing/development) - publish-pr: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Publish to pkg.pr.new - run: npx pkg-pr-new publish --comment=update - - # Publish on approved PRs (recommended pattern) - publish-approved: - if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Publish to pkg.pr.new (Approved) - run: npx pkg-pr-new publish --comment=create \ No newline at end of file diff --git a/src/mcp/tools/utilities/__tests__/clean.test.ts b/src/mcp/tools/utilities/__tests__/clean.test.ts index 3812b855..59191cd1 100644 --- a/src/mcp/tools/utilities/__tests__/clean.test.ts +++ b/src/mcp/tools/utilities/__tests__/clean.test.ts @@ -48,4 +48,80 @@ describe('clean (unified) tool', () => { const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? ''); expect(text).toContain('Invalid parameters'); }); + + it('uses iOS platform by default', async () => { + let capturedCommand: string[] = []; + const mockExecutor = async (command: string[]) => { + capturedCommand = command; + return { success: true, output: 'clean success' }; + }; + + const result = await cleanLogic( + { projectPath: '/p.xcodeproj', scheme: 'App' } as any, + mockExecutor, + ); + expect(result.isError).not.toBe(true); + + // Check that the command contains iOS platform destination + const commandStr = capturedCommand.join(' '); + expect(commandStr).toContain('-destination'); + expect(commandStr).toContain('platform=iOS'); + }); + + it('accepts custom platform parameter', async () => { + let capturedCommand: string[] = []; + const mockExecutor = async (command: string[]) => { + capturedCommand = command; + return { success: true, output: 'clean success' }; + }; + + const result = await cleanLogic( + { + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'macOS', + } as any, + mockExecutor, + ); + expect(result.isError).not.toBe(true); + + // Check that the command contains macOS platform destination + const commandStr = capturedCommand.join(' '); + expect(commandStr).toContain('-destination'); + expect(commandStr).toContain('platform=macOS'); + }); + + it('accepts iOS Simulator platform parameter (maps to iOS for clean)', async () => { + let capturedCommand: string[] = []; + const mockExecutor = async (command: string[]) => { + capturedCommand = command; + return { success: true, output: 'clean success' }; + }; + + const result = await cleanLogic( + { + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'iOS Simulator', + } as any, + mockExecutor, + ); + expect(result.isError).not.toBe(true); + + // For clean operations, iOS Simulator should be mapped to iOS platform + const commandStr = capturedCommand.join(' '); + expect(commandStr).toContain('-destination'); + expect(commandStr).toContain('platform=iOS'); + }); + + it('handler validation: rejects invalid platform values', async () => { + const result = await (tool as any).handler({ + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'InvalidPlatform', + }); + expect(result.isError).toBe(true); + const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? ''); + expect(text).toContain('Invalid parameters'); + }); }); diff --git a/src/mcp/tools/utilities/clean.ts b/src/mcp/tools/utilities/clean.ts index 5e0fa9a5..f18db019 100644 --- a/src/mcp/tools/utilities/clean.ts +++ b/src/mcp/tools/utilities/clean.ts @@ -32,6 +32,22 @@ const baseOptions = { .describe( 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', ), + platform: z + .enum([ + 'macOS', + 'iOS', + 'iOS Simulator', + 'watchOS', + 'watchOS Simulator', + 'tvOS', + 'tvOS Simulator', + 'visionOS', + 'visionOS Simulator', + ]) + .optional() + .describe( + 'Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator', + ), }; const baseSchemaObject = z.object({ @@ -67,6 +83,32 @@ export async function cleanLogic( 'Invalid parameters:\nscheme: scheme is required when workspacePath is provided.', ); } + + // Use provided platform or default to iOS + const targetPlatform = params.platform ?? 'iOS'; + + // Map human-friendly platform names to XcodePlatform enum values + // This is safer than direct key lookup and handles the space-containing simulator names + const platformMap = { + macOS: XcodePlatform.macOS, + iOS: XcodePlatform.iOS, + 'iOS Simulator': XcodePlatform.iOSSimulator, + watchOS: XcodePlatform.watchOS, + 'watchOS Simulator': XcodePlatform.watchOSSimulator, + tvOS: XcodePlatform.tvOS, + 'tvOS Simulator': XcodePlatform.tvOSSimulator, + visionOS: XcodePlatform.visionOS, + 'visionOS Simulator': XcodePlatform.visionOSSimulator, + }; + + const platformEnum = platformMap[targetPlatform]; + if (!platformEnum) { + return createErrorResponse( + 'Parameter validation failed', + `Invalid parameters:\nplatform: unsupported value "${targetPlatform}".`, + ); + } + const hasProjectPath = typeof params.projectPath === 'string'; const typedParams: SharedBuildParams = { ...(hasProjectPath @@ -80,10 +122,22 @@ export async function cleanLogic( extraArgs: params.extraArgs, }; + // For clean operations, simulator platforms should be mapped to their device equivalents + // since clean works at the build product level, not runtime level, and build products + // are shared between device and simulator platforms + const cleanPlatformMap: Partial> = { + [XcodePlatform.iOSSimulator]: XcodePlatform.iOS, + [XcodePlatform.watchOSSimulator]: XcodePlatform.watchOS, + [XcodePlatform.tvOSSimulator]: XcodePlatform.tvOS, + [XcodePlatform.visionOSSimulator]: XcodePlatform.visionOS, + }; + + const cleanPlatform = cleanPlatformMap[platformEnum] ?? platformEnum; + return executeXcodeBuildCommand( typedParams, { - platform: XcodePlatform.macOS, + platform: cleanPlatform, logPrefix: 'Clean', }, false, @@ -95,7 +149,7 @@ export async function cleanLogic( export default { name: 'clean', description: - "Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })", + "Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Platform defaults to iOS if not specified. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', platform: 'iOS' })", schema: baseSchemaObject.shape, handler: createTypedTool( cleanSchema as z.ZodType,