import { z } from 'zod';
import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { simctl } from '../executor/simctl.js';
import { resolveDevice } from '../utils/device-resolver.js';
export const pushSchema = z.object({
device: z
.string()
.describe('Device UDID, name, or "booted"'),
bundle_id: z
.string()
.describe('Bundle identifier of the app to receive the push'),
payload: z
.union([
z.string().describe('Path to a JSON file containing the push payload'),
z.object({}).passthrough().describe('Push notification payload object'),
])
.describe('Push notification payload (file path or JSON object)'),
});
export type PushInput = z.infer<typeof pushSchema>;
export const pushTool = {
name: 'simctl_push',
description: 'Send a push notification to an app on a simulator',
inputSchema: pushSchema,
handler: async (input: PushInput) => {
const udid = resolveDevice(input.device);
let payloadPath: string;
let tempFile = false;
if (typeof input.payload === 'string') {
// It's a file path
if (!existsSync(input.payload)) {
throw new Error(`Payload file not found: ${input.payload}`);
}
payloadPath = input.payload;
} else {
// It's a JSON object, write to temp file
const tempPath = join(tmpdir(), `push-${randomUUID()}.json`);
writeFileSync(tempPath, JSON.stringify(input.payload), 'utf-8');
payloadPath = tempPath;
tempFile = true;
}
try {
const result = simctl('push', [udid, input.bundle_id, payloadPath]);
if (result.exitCode !== 0) {
throw new Error(`Failed to send push: ${result.stderr || result.stdout}`);
}
return {
content: [
{
type: 'text' as const,
text: `Successfully sent push notification to ${input.bundle_id} on device ${input.device}`,
},
],
};
} finally {
if (tempFile) {
try {
unlinkSync(payloadPath);
} catch {
// Ignore cleanup errors
}
}
}
},
};