muse-app-preview-mcp 1.3.0 → 1.5.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.
- package/dist/index.js +1386 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -57,6 +57,58 @@ function generateId() {
|
|
|
57
57
|
}
|
|
58
58
|
// Tool definitions
|
|
59
59
|
const tools = [
|
|
60
|
+
{
|
|
61
|
+
name: "create_app_previews",
|
|
62
|
+
description: "Complete workflow to create App Store preview images. Steps: 1) User selects platform (iOS/macOS/watchOS), 2) User selects language, 3) Claude generates marketing text, 4) Auto-capture screenshots, 5) Send to MUSE Preview Maker app or save to folder.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
bundleId: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Bundle ID of the app to capture",
|
|
69
|
+
},
|
|
70
|
+
appName: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Name of the app (for generating marketing text)",
|
|
73
|
+
},
|
|
74
|
+
appDescription: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Brief description of what the app does (helps Claude generate better text)",
|
|
77
|
+
},
|
|
78
|
+
platform: {
|
|
79
|
+
type: "string",
|
|
80
|
+
enum: ["ios", "macos", "watchos"],
|
|
81
|
+
description: "Target platform",
|
|
82
|
+
},
|
|
83
|
+
language: {
|
|
84
|
+
type: "string",
|
|
85
|
+
enum: ["ko", "en", "ja", "zh", "es", "fr", "de"],
|
|
86
|
+
description: "Language for preview text (ko=Korean, en=English, etc.)",
|
|
87
|
+
},
|
|
88
|
+
screenCount: {
|
|
89
|
+
type: "number",
|
|
90
|
+
description: "Number of screens to capture. Default: 3",
|
|
91
|
+
},
|
|
92
|
+
previews: {
|
|
93
|
+
type: "array",
|
|
94
|
+
description: "Array of preview definitions with title/subtitle. If not provided, Claude should generate these.",
|
|
95
|
+
items: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
title: { type: "string" },
|
|
99
|
+
subtitle: { type: "string" },
|
|
100
|
+
paletteId: { type: "string" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
simulatorUDID: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Specific simulator UDID for iOS/watchOS. Optional.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: ["bundleId", "appName", "platform", "language"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
60
112
|
{
|
|
61
113
|
name: "add_preview",
|
|
62
114
|
description: "Add a new preview set with screenshot, title, and subtitle. The screenshot will be used to generate an app store preview image.",
|
|
@@ -269,13 +321,13 @@ const tools = [
|
|
|
269
321
|
},
|
|
270
322
|
{
|
|
271
323
|
name: "launch_and_capture",
|
|
272
|
-
description: "Launch
|
|
324
|
+
description: "Launch an app and capture screenshot. Auto-detects platform (iOS simulator or macOS). For iOS, launches in simulator and captures. For macOS, launches app and captures window.",
|
|
273
325
|
inputSchema: {
|
|
274
326
|
type: "object",
|
|
275
327
|
properties: {
|
|
276
328
|
bundleId: {
|
|
277
329
|
type: "string",
|
|
278
|
-
description: "Bundle ID of the app
|
|
330
|
+
description: "Bundle ID of the app (e.g., 'com.example.myapp')",
|
|
279
331
|
},
|
|
280
332
|
title: {
|
|
281
333
|
type: "string",
|
|
@@ -285,25 +337,34 @@ const tools = [
|
|
|
285
337
|
type: "string",
|
|
286
338
|
description: "Subtitle text for the preview",
|
|
287
339
|
},
|
|
340
|
+
platform: {
|
|
341
|
+
type: "string",
|
|
342
|
+
enum: ["ios", "macos", "auto"],
|
|
343
|
+
description: "Platform: 'ios' for simulator, 'macos' for Mac app, 'auto' to detect. Default: auto",
|
|
344
|
+
},
|
|
288
345
|
waitSeconds: {
|
|
289
346
|
type: "number",
|
|
290
347
|
description: "Seconds to wait after launch before capture. Default: 3",
|
|
291
348
|
},
|
|
292
349
|
simulatorUDID: {
|
|
293
350
|
type: "string",
|
|
294
|
-
description: "Specific simulator UDID. Optional.",
|
|
351
|
+
description: "Specific iOS simulator UDID. Optional.",
|
|
295
352
|
},
|
|
296
353
|
paletteId: {
|
|
297
354
|
type: "string",
|
|
298
355
|
description: "Color palette ID. Optional.",
|
|
299
356
|
},
|
|
357
|
+
deviceId: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description: "Device ID for preview (iphone_6_7, mac_retina, etc.). Auto-detected if not specified.",
|
|
360
|
+
},
|
|
300
361
|
},
|
|
301
362
|
required: ["bundleId", "title", "subtitle"],
|
|
302
363
|
},
|
|
303
364
|
},
|
|
304
365
|
{
|
|
305
366
|
name: "capture_app_screens",
|
|
306
|
-
description: "Capture multiple screens from a running app
|
|
367
|
+
description: "Capture multiple screens from a running app. Auto-detects platform. Launches app, captures first screen, then captures additional screens with wait intervals.",
|
|
307
368
|
inputSchema: {
|
|
308
369
|
type: "object",
|
|
309
370
|
properties: {
|
|
@@ -324,14 +385,305 @@ const tools = [
|
|
|
324
385
|
},
|
|
325
386
|
},
|
|
326
387
|
},
|
|
388
|
+
platform: {
|
|
389
|
+
type: "string",
|
|
390
|
+
enum: ["ios", "macos", "auto"],
|
|
391
|
+
description: "Platform: 'ios', 'macos', or 'auto'. Default: auto",
|
|
392
|
+
},
|
|
327
393
|
simulatorUDID: {
|
|
328
394
|
type: "string",
|
|
329
|
-
description: "Specific simulator UDID. Optional.",
|
|
395
|
+
description: "Specific iOS simulator UDID. Optional.",
|
|
330
396
|
},
|
|
331
397
|
},
|
|
332
398
|
required: ["bundleId", "screens"],
|
|
333
399
|
},
|
|
334
400
|
},
|
|
401
|
+
// === Advanced Capture Tools ===
|
|
402
|
+
{
|
|
403
|
+
name: "interact_simulator",
|
|
404
|
+
description: "Interact with iOS Simulator: tap, swipe, scroll, type text. Use this to navigate to specific screens before capturing.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: {
|
|
408
|
+
action: {
|
|
409
|
+
type: "string",
|
|
410
|
+
enum: ["tap", "swipe", "scroll", "type", "home", "shake"],
|
|
411
|
+
description: "Action type",
|
|
412
|
+
},
|
|
413
|
+
x: {
|
|
414
|
+
type: "number",
|
|
415
|
+
description: "X coordinate (for tap/swipe start)",
|
|
416
|
+
},
|
|
417
|
+
y: {
|
|
418
|
+
type: "number",
|
|
419
|
+
description: "Y coordinate (for tap/swipe start)",
|
|
420
|
+
},
|
|
421
|
+
toX: {
|
|
422
|
+
type: "number",
|
|
423
|
+
description: "End X coordinate (for swipe)",
|
|
424
|
+
},
|
|
425
|
+
toY: {
|
|
426
|
+
type: "number",
|
|
427
|
+
description: "End Y coordinate (for swipe)",
|
|
428
|
+
},
|
|
429
|
+
direction: {
|
|
430
|
+
type: "string",
|
|
431
|
+
enum: ["up", "down", "left", "right"],
|
|
432
|
+
description: "Scroll/swipe direction (alternative to coordinates)",
|
|
433
|
+
},
|
|
434
|
+
text: {
|
|
435
|
+
type: "string",
|
|
436
|
+
description: "Text to type (for type action)",
|
|
437
|
+
},
|
|
438
|
+
duration: {
|
|
439
|
+
type: "number",
|
|
440
|
+
description: "Duration in seconds (for swipe). Default: 0.3",
|
|
441
|
+
},
|
|
442
|
+
simulatorUDID: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description: "Target simulator UDID. Uses tracked simulator if not specified.",
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
required: ["action"],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "navigate_app",
|
|
452
|
+
description: "Navigate to a specific screen in the app using deep links (URL schemes) or by opening URLs.",
|
|
453
|
+
inputSchema: {
|
|
454
|
+
type: "object",
|
|
455
|
+
properties: {
|
|
456
|
+
url: {
|
|
457
|
+
type: "string",
|
|
458
|
+
description: "Deep link URL or URL scheme (e.g., 'myapp://settings', 'https://example.com/page')",
|
|
459
|
+
},
|
|
460
|
+
bundleId: {
|
|
461
|
+
type: "string",
|
|
462
|
+
description: "Bundle ID of the app (required for some navigations)",
|
|
463
|
+
},
|
|
464
|
+
waitSeconds: {
|
|
465
|
+
type: "number",
|
|
466
|
+
description: "Seconds to wait after navigation. Default: 2",
|
|
467
|
+
},
|
|
468
|
+
simulatorUDID: {
|
|
469
|
+
type: "string",
|
|
470
|
+
description: "Target simulator UDID.",
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
required: ["url"],
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "configure_simulator",
|
|
478
|
+
description: "Configure simulator appearance: status bar time, battery, signal, dark/light mode, etc. Makes screenshots look professional.",
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
statusBarTime: {
|
|
483
|
+
type: "string",
|
|
484
|
+
description: "Set status bar time (e.g., '9:41')",
|
|
485
|
+
},
|
|
486
|
+
batteryLevel: {
|
|
487
|
+
type: "number",
|
|
488
|
+
description: "Battery level 0-100",
|
|
489
|
+
},
|
|
490
|
+
batteryState: {
|
|
491
|
+
type: "string",
|
|
492
|
+
enum: ["charging", "charged", "discharging"],
|
|
493
|
+
description: "Battery state",
|
|
494
|
+
},
|
|
495
|
+
cellularBars: {
|
|
496
|
+
type: "number",
|
|
497
|
+
description: "Cellular signal bars 0-4",
|
|
498
|
+
},
|
|
499
|
+
wifiBars: {
|
|
500
|
+
type: "number",
|
|
501
|
+
description: "WiFi signal bars 0-3",
|
|
502
|
+
},
|
|
503
|
+
appearance: {
|
|
504
|
+
type: "string",
|
|
505
|
+
enum: ["light", "dark"],
|
|
506
|
+
description: "Light or dark mode",
|
|
507
|
+
},
|
|
508
|
+
simulatorUDID: {
|
|
509
|
+
type: "string",
|
|
510
|
+
description: "Target simulator UDID.",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "auto_capture_flow",
|
|
517
|
+
description: "Automatically capture a user flow with actions. Define a sequence of actions (tap, swipe, wait) and capture at each step. Perfect for capturing onboarding flows, feature tours, etc.",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
bundleId: {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "Bundle ID of the app",
|
|
524
|
+
},
|
|
525
|
+
flow: {
|
|
526
|
+
type: "array",
|
|
527
|
+
description: "Sequence of actions and captures",
|
|
528
|
+
items: {
|
|
529
|
+
type: "object",
|
|
530
|
+
properties: {
|
|
531
|
+
action: {
|
|
532
|
+
type: "string",
|
|
533
|
+
enum: ["capture", "tap", "swipe", "scroll", "wait", "type", "navigate"],
|
|
534
|
+
description: "Action to perform",
|
|
535
|
+
},
|
|
536
|
+
title: { type: "string", description: "Title for capture" },
|
|
537
|
+
subtitle: { type: "string", description: "Subtitle for capture" },
|
|
538
|
+
paletteId: { type: "string", description: "Palette for capture" },
|
|
539
|
+
x: { type: "number" },
|
|
540
|
+
y: { type: "number" },
|
|
541
|
+
toX: { type: "number" },
|
|
542
|
+
toY: { type: "number" },
|
|
543
|
+
direction: { type: "string", enum: ["up", "down", "left", "right"] },
|
|
544
|
+
text: { type: "string" },
|
|
545
|
+
url: { type: "string", description: "URL for navigate action" },
|
|
546
|
+
seconds: { type: "number", description: "Wait time or animation duration" },
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
resetFirst: {
|
|
551
|
+
type: "boolean",
|
|
552
|
+
description: "Reset app state before starting. Default: true",
|
|
553
|
+
},
|
|
554
|
+
simulatorUDID: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "Target simulator UDID.",
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
required: ["bundleId", "flow"],
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "scroll_and_capture",
|
|
564
|
+
description: "Scroll through a screen and capture at intervals. Great for capturing long lists, feeds, or scrollable content.",
|
|
565
|
+
inputSchema: {
|
|
566
|
+
type: "object",
|
|
567
|
+
properties: {
|
|
568
|
+
bundleId: {
|
|
569
|
+
type: "string",
|
|
570
|
+
description: "Bundle ID of the app",
|
|
571
|
+
},
|
|
572
|
+
scrollCount: {
|
|
573
|
+
type: "number",
|
|
574
|
+
description: "Number of scroll actions. Default: 3",
|
|
575
|
+
},
|
|
576
|
+
direction: {
|
|
577
|
+
type: "string",
|
|
578
|
+
enum: ["up", "down", "left", "right"],
|
|
579
|
+
description: "Scroll direction. Default: down",
|
|
580
|
+
},
|
|
581
|
+
captureEach: {
|
|
582
|
+
type: "boolean",
|
|
583
|
+
description: "Capture after each scroll. Default: true",
|
|
584
|
+
},
|
|
585
|
+
titles: {
|
|
586
|
+
type: "array",
|
|
587
|
+
items: { type: "string" },
|
|
588
|
+
description: "Titles for each capture",
|
|
589
|
+
},
|
|
590
|
+
subtitles: {
|
|
591
|
+
type: "array",
|
|
592
|
+
items: { type: "string" },
|
|
593
|
+
description: "Subtitles for each capture",
|
|
594
|
+
},
|
|
595
|
+
paletteIds: {
|
|
596
|
+
type: "array",
|
|
597
|
+
items: { type: "string" },
|
|
598
|
+
description: "Palette IDs for each capture",
|
|
599
|
+
},
|
|
600
|
+
waitBetween: {
|
|
601
|
+
type: "number",
|
|
602
|
+
description: "Seconds to wait between scrolls. Default: 1",
|
|
603
|
+
},
|
|
604
|
+
simulatorUDID: {
|
|
605
|
+
type: "string",
|
|
606
|
+
description: "Target simulator UDID.",
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
required: ["bundleId"],
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "batch_capture_devices",
|
|
614
|
+
description: "Capture the same screen on multiple devices/simulators. Great for generating previews for all device sizes at once.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: "object",
|
|
617
|
+
properties: {
|
|
618
|
+
bundleId: {
|
|
619
|
+
type: "string",
|
|
620
|
+
description: "Bundle ID of the app",
|
|
621
|
+
},
|
|
622
|
+
title: {
|
|
623
|
+
type: "string",
|
|
624
|
+
description: "Title for all captures",
|
|
625
|
+
},
|
|
626
|
+
subtitle: {
|
|
627
|
+
type: "string",
|
|
628
|
+
description: "Subtitle for all captures",
|
|
629
|
+
},
|
|
630
|
+
paletteId: {
|
|
631
|
+
type: "string",
|
|
632
|
+
description: "Palette ID for all captures",
|
|
633
|
+
},
|
|
634
|
+
devices: {
|
|
635
|
+
type: "array",
|
|
636
|
+
items: { type: "string" },
|
|
637
|
+
description: "List of simulator names or UDIDs to capture",
|
|
638
|
+
},
|
|
639
|
+
bootDevices: {
|
|
640
|
+
type: "boolean",
|
|
641
|
+
description: "Boot specified devices if not running. Default: false",
|
|
642
|
+
},
|
|
643
|
+
waitSeconds: {
|
|
644
|
+
type: "number",
|
|
645
|
+
description: "Wait time after launching app. Default: 3",
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
required: ["bundleId", "title", "subtitle"],
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "record_and_extract",
|
|
653
|
+
description: "Record simulator video and extract key frames as screenshots. Useful for capturing animations or transitions.",
|
|
654
|
+
inputSchema: {
|
|
655
|
+
type: "object",
|
|
656
|
+
properties: {
|
|
657
|
+
bundleId: {
|
|
658
|
+
type: "string",
|
|
659
|
+
description: "Bundle ID of the app",
|
|
660
|
+
},
|
|
661
|
+
duration: {
|
|
662
|
+
type: "number",
|
|
663
|
+
description: "Recording duration in seconds. Default: 5",
|
|
664
|
+
},
|
|
665
|
+
frameCount: {
|
|
666
|
+
type: "number",
|
|
667
|
+
description: "Number of frames to extract. Default: 3",
|
|
668
|
+
},
|
|
669
|
+
titles: {
|
|
670
|
+
type: "array",
|
|
671
|
+
items: { type: "string" },
|
|
672
|
+
description: "Titles for each extracted frame",
|
|
673
|
+
},
|
|
674
|
+
subtitles: {
|
|
675
|
+
type: "array",
|
|
676
|
+
items: { type: "string" },
|
|
677
|
+
description: "Subtitles for each extracted frame",
|
|
678
|
+
},
|
|
679
|
+
simulatorUDID: {
|
|
680
|
+
type: "string",
|
|
681
|
+
description: "Target simulator UDID.",
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
required: ["bundleId"],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
335
687
|
];
|
|
336
688
|
// Tool handlers
|
|
337
689
|
async function handleAddPreview(args) {
|
|
@@ -563,12 +915,16 @@ async function handleUpdateSettings(args) {
|
|
|
563
915
|
settings: store.settings,
|
|
564
916
|
});
|
|
565
917
|
}
|
|
566
|
-
// Helper to get booted simulators
|
|
918
|
+
// Helper to get booted simulators (iOS/iPadOS only, prioritized)
|
|
567
919
|
async function getBootedSimulators() {
|
|
568
920
|
const { stdout } = await execAsync("xcrun simctl list devices booted -j");
|
|
569
921
|
const data = JSON.parse(stdout);
|
|
570
922
|
const bootedDevices = [];
|
|
571
923
|
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
924
|
+
// Only include iOS/iPadOS simulators (skip watchOS, tvOS, visionOS)
|
|
925
|
+
if (!runtime.toLowerCase().includes("ios")) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
572
928
|
const deviceList = devices;
|
|
573
929
|
for (const device of deviceList) {
|
|
574
930
|
if (device.state === "Booted") {
|
|
@@ -576,15 +932,30 @@ async function getBootedSimulators() {
|
|
|
576
932
|
const runtimeVersion = runtimeMatch
|
|
577
933
|
? `iOS ${runtimeMatch[1].replace("-", ".")}`
|
|
578
934
|
: runtime;
|
|
935
|
+
// Priority: iPhone Pro Max > iPhone Pro > iPhone > iPad
|
|
936
|
+
let priority = 0;
|
|
937
|
+
const name = device.name.toLowerCase();
|
|
938
|
+
if (name.includes("iphone") && name.includes("pro max"))
|
|
939
|
+
priority = 4;
|
|
940
|
+
else if (name.includes("iphone") && name.includes("pro"))
|
|
941
|
+
priority = 3;
|
|
942
|
+
else if (name.includes("iphone"))
|
|
943
|
+
priority = 2;
|
|
944
|
+
else if (name.includes("ipad"))
|
|
945
|
+
priority = 1;
|
|
579
946
|
bootedDevices.push({
|
|
580
947
|
name: device.name,
|
|
581
948
|
udid: device.udid,
|
|
582
949
|
runtime: runtimeVersion,
|
|
950
|
+
priority,
|
|
583
951
|
});
|
|
584
952
|
}
|
|
585
953
|
}
|
|
586
954
|
}
|
|
587
|
-
|
|
955
|
+
// Sort by priority (highest first)
|
|
956
|
+
bootedDevices.sort((a, b) => b.priority - a.priority);
|
|
957
|
+
// Remove priority field before returning
|
|
958
|
+
return bootedDevices.map(({ name, udid, runtime }) => ({ name, udid, runtime }));
|
|
588
959
|
}
|
|
589
960
|
// Map simulator name to deviceId
|
|
590
961
|
function mapSimulatorToDeviceId(simulatorName) {
|
|
@@ -776,6 +1147,68 @@ async function isAppRunning(simulatorUDID, bundleId) {
|
|
|
776
1147
|
return false;
|
|
777
1148
|
}
|
|
778
1149
|
}
|
|
1150
|
+
// Helper: Detect platform for bundle ID
|
|
1151
|
+
async function detectPlatform(bundleId) {
|
|
1152
|
+
// Check if app exists on macOS
|
|
1153
|
+
try {
|
|
1154
|
+
const { stdout } = await execAsync(`mdfind "kMDItemCFBundleIdentifier == '${bundleId}'" | head -1`);
|
|
1155
|
+
if (stdout.trim()) {
|
|
1156
|
+
return "macos";
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
// Not found on macOS
|
|
1161
|
+
}
|
|
1162
|
+
// Check if app exists on any booted iOS simulator
|
|
1163
|
+
try {
|
|
1164
|
+
const simulators = await getBootedSimulators();
|
|
1165
|
+
for (const sim of simulators) {
|
|
1166
|
+
try {
|
|
1167
|
+
await execAsync(`xcrun simctl get_app_container ${sim.udid} ${bundleId}`);
|
|
1168
|
+
return "ios";
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
// Not on this simulator
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
// No simulators or error
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
// Helper: Capture macOS app window
|
|
1181
|
+
async function captureMacOSWindow(bundleId, outputPath) {
|
|
1182
|
+
try {
|
|
1183
|
+
// Get window ID for the app
|
|
1184
|
+
const { stdout: windowInfo } = await execAsync(`
|
|
1185
|
+
osascript -e 'tell application "System Events" to get id of first window of (first process whose bundle identifier is "${bundleId}")'
|
|
1186
|
+
`);
|
|
1187
|
+
const windowId = windowInfo.trim();
|
|
1188
|
+
if (windowId) {
|
|
1189
|
+
// Capture specific window by ID
|
|
1190
|
+
await execAsync(`screencapture -l ${windowId} -o "${outputPath}"`);
|
|
1191
|
+
return fs.existsSync(outputPath);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
// Fallback: capture by app name
|
|
1196
|
+
}
|
|
1197
|
+
// Fallback: use screencapture with window selection
|
|
1198
|
+
try {
|
|
1199
|
+
// Bring app to front and capture
|
|
1200
|
+
await execAsync(`osascript -e 'tell application id "${bundleId}" to activate'`);
|
|
1201
|
+
await sleep(0.5);
|
|
1202
|
+
// Capture the frontmost window
|
|
1203
|
+
await execAsync(`screencapture -o -w "${outputPath}"`);
|
|
1204
|
+
return fs.existsSync(outputPath);
|
|
1205
|
+
}
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Track launched simulator for this session
|
|
1211
|
+
let lastLaunchedSimulator = null;
|
|
779
1212
|
// Handler: Complete reset
|
|
780
1213
|
async function handleResetAll(args) {
|
|
781
1214
|
if (!args.confirm) {
|
|
@@ -823,65 +1256,135 @@ async function handleResetAll(args) {
|
|
|
823
1256
|
});
|
|
824
1257
|
}
|
|
825
1258
|
}
|
|
826
|
-
// Handler: Launch app and capture
|
|
1259
|
+
// Handler: Launch app and capture (supports iOS and macOS)
|
|
827
1260
|
async function handleLaunchAndCapture(args) {
|
|
828
1261
|
try {
|
|
829
1262
|
ensureScreenshotsDir();
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
error: "Specified simulator not found",
|
|
846
|
-
bootedSimulators,
|
|
847
|
-
});
|
|
1263
|
+
const waitTime = args.waitSeconds || 3;
|
|
1264
|
+
const timestamp = Date.now();
|
|
1265
|
+
const safeBundleId = args.bundleId.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1266
|
+
// Determine platform
|
|
1267
|
+
let platform = args.platform || "auto";
|
|
1268
|
+
if (platform === "auto") {
|
|
1269
|
+
const detected = await detectPlatform(args.bundleId);
|
|
1270
|
+
if (!detected) {
|
|
1271
|
+
return JSON.stringify({
|
|
1272
|
+
success: false,
|
|
1273
|
+
error: `App not found: ${args.bundleId}`,
|
|
1274
|
+
hint: "Make sure the app is installed on macOS or iOS simulator",
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
platform = detected;
|
|
848
1278
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1279
|
+
let screenshotPath;
|
|
1280
|
+
let deviceId;
|
|
1281
|
+
let captureInfo = {};
|
|
1282
|
+
if (platform === "macos") {
|
|
1283
|
+
// === macOS App Capture ===
|
|
1284
|
+
screenshotPath = path.join(SCREENSHOTS_DIR, `${safeBundleId}_mac_${timestamp}.png`);
|
|
1285
|
+
// Launch macOS app
|
|
1286
|
+
try {
|
|
1287
|
+
await execAsync(`open -b "${args.bundleId}"`);
|
|
1288
|
+
}
|
|
1289
|
+
catch (launchError) {
|
|
1290
|
+
return JSON.stringify({
|
|
1291
|
+
success: false,
|
|
1292
|
+
error: `Failed to launch macOS app: ${launchError.message}`,
|
|
1293
|
+
bundleId: args.bundleId,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
// Wait for app to load
|
|
1297
|
+
await sleep(waitTime);
|
|
1298
|
+
// Capture window
|
|
1299
|
+
const captured = await captureMacOSWindow(args.bundleId, screenshotPath);
|
|
1300
|
+
if (!captured) {
|
|
1301
|
+
return JSON.stringify({
|
|
1302
|
+
success: false,
|
|
1303
|
+
error: "Failed to capture macOS window",
|
|
1304
|
+
hint: "Make sure the app has a visible window",
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
deviceId = args.deviceId || "mac_retina";
|
|
1308
|
+
captureInfo = { platform: "macos" };
|
|
860
1309
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1310
|
+
else {
|
|
1311
|
+
// === iOS Simulator Capture ===
|
|
1312
|
+
const bootedSimulators = await getBootedSimulators();
|
|
1313
|
+
if (bootedSimulators.length === 0) {
|
|
1314
|
+
return JSON.stringify({
|
|
1315
|
+
success: false,
|
|
1316
|
+
error: "No iOS Simulator is running",
|
|
1317
|
+
hint: "Please launch an iOS Simulator first",
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
// Find target simulator
|
|
1321
|
+
let targetSimulator = args.simulatorUDID
|
|
1322
|
+
? bootedSimulators.find(s => s.udid === args.simulatorUDID)
|
|
1323
|
+
: null;
|
|
1324
|
+
// If not specified, find simulator that has the app installed
|
|
1325
|
+
if (!targetSimulator) {
|
|
1326
|
+
for (const sim of bootedSimulators) {
|
|
1327
|
+
try {
|
|
1328
|
+
await execAsync(`xcrun simctl get_app_container ${sim.udid} ${args.bundleId}`);
|
|
1329
|
+
targetSimulator = sim;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
catch {
|
|
1333
|
+
// App not on this simulator
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// Fallback to first simulator
|
|
1338
|
+
if (!targetSimulator) {
|
|
1339
|
+
targetSimulator = bootedSimulators[0];
|
|
1340
|
+
}
|
|
1341
|
+
screenshotPath = path.join(SCREENSHOTS_DIR, `${safeBundleId}_ios_${timestamp}.png`);
|
|
1342
|
+
// Terminate app first (clean state)
|
|
1343
|
+
try {
|
|
1344
|
+
await execAsync(`xcrun simctl terminate ${targetSimulator.udid} ${args.bundleId}`);
|
|
1345
|
+
await sleep(0.5);
|
|
1346
|
+
}
|
|
1347
|
+
catch {
|
|
1348
|
+
// App might not be running
|
|
1349
|
+
}
|
|
1350
|
+
// Launch the app
|
|
1351
|
+
try {
|
|
1352
|
+
await execAsync(`xcrun simctl launch ${targetSimulator.udid} ${args.bundleId}`);
|
|
1353
|
+
}
|
|
1354
|
+
catch (launchError) {
|
|
1355
|
+
return JSON.stringify({
|
|
1356
|
+
success: false,
|
|
1357
|
+
error: `Failed to launch app: ${launchError.message}`,
|
|
1358
|
+
hint: "Make sure the app is installed on the simulator",
|
|
1359
|
+
bundleId: args.bundleId,
|
|
1360
|
+
simulator: targetSimulator.name,
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
// Track this simulator
|
|
1364
|
+
lastLaunchedSimulator = {
|
|
1365
|
+
udid: targetSimulator.udid,
|
|
1366
|
+
name: targetSimulator.name,
|
|
866
1367
|
bundleId: args.bundleId,
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1368
|
+
};
|
|
1369
|
+
// Wait for app to load
|
|
1370
|
+
await sleep(waitTime);
|
|
1371
|
+
// Capture screenshot from specific simulator
|
|
1372
|
+
await execAsync(`xcrun simctl io ${targetSimulator.udid} screenshot "${screenshotPath}"`);
|
|
1373
|
+
if (!fs.existsSync(screenshotPath)) {
|
|
1374
|
+
return JSON.stringify({
|
|
1375
|
+
success: false,
|
|
1376
|
+
error: "Screenshot capture failed",
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
deviceId = args.deviceId || mapSimulatorToDeviceId(targetSimulator.name);
|
|
1380
|
+
captureInfo = {
|
|
1381
|
+
platform: "ios",
|
|
1382
|
+
simulator: {
|
|
1383
|
+
name: targetSimulator.name,
|
|
1384
|
+
udid: targetSimulator.udid,
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
882
1387
|
}
|
|
883
|
-
// Auto-detect device ID
|
|
884
|
-
const autoDeviceId = mapSimulatorToDeviceId(targetSimulator.name);
|
|
885
1388
|
// Add to preview store
|
|
886
1389
|
const store = loadStore();
|
|
887
1390
|
const preview = {
|
|
@@ -889,7 +1392,7 @@ async function handleLaunchAndCapture(args) {
|
|
|
889
1392
|
screenshotPath: screenshotPath,
|
|
890
1393
|
title: args.title,
|
|
891
1394
|
subtitle: args.subtitle,
|
|
892
|
-
deviceId:
|
|
1395
|
+
deviceId: deviceId,
|
|
893
1396
|
paletteId: args.paletteId || store.settings.defaultPaletteId,
|
|
894
1397
|
createdAt: new Date().toISOString(),
|
|
895
1398
|
};
|
|
@@ -897,11 +1400,8 @@ async function handleLaunchAndCapture(args) {
|
|
|
897
1400
|
saveStore(store);
|
|
898
1401
|
return JSON.stringify({
|
|
899
1402
|
success: true,
|
|
900
|
-
message: `App launched and captured: ${args.bundleId}`,
|
|
901
|
-
|
|
902
|
-
name: targetSimulator.name,
|
|
903
|
-
udid: targetSimulator.udid,
|
|
904
|
-
},
|
|
1403
|
+
message: `App launched and captured (${platform}): ${args.bundleId}`,
|
|
1404
|
+
...captureInfo,
|
|
905
1405
|
waitedSeconds: waitTime,
|
|
906
1406
|
preview: preview,
|
|
907
1407
|
totalPreviews: store.previews.length,
|
|
@@ -1000,10 +1500,805 @@ async function handleCaptureAppScreens(args) {
|
|
|
1000
1500
|
});
|
|
1001
1501
|
}
|
|
1002
1502
|
}
|
|
1503
|
+
// Handler: Complete workflow to create app previews
|
|
1504
|
+
async function handleCreateAppPreviews(args) {
|
|
1505
|
+
try {
|
|
1506
|
+
ensureScreenshotsDir();
|
|
1507
|
+
const screenCount = args.screenCount || 3;
|
|
1508
|
+
const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
|
|
1509
|
+
// Step 1: Reset existing data
|
|
1510
|
+
const dataDir = path.join(process.env.HOME || "~", ".muse-app-preview");
|
|
1511
|
+
if (fs.existsSync(SCREENSHOTS_DIR)) {
|
|
1512
|
+
const files = fs.readdirSync(SCREENSHOTS_DIR);
|
|
1513
|
+
for (const file of files) {
|
|
1514
|
+
fs.unlinkSync(path.join(SCREENSHOTS_DIR, file));
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const store = loadStore();
|
|
1518
|
+
store.previews = [];
|
|
1519
|
+
saveStore(store);
|
|
1520
|
+
// Step 2: Determine device ID based on platform
|
|
1521
|
+
let deviceId;
|
|
1522
|
+
let captureMethod;
|
|
1523
|
+
switch (args.platform) {
|
|
1524
|
+
case "macos":
|
|
1525
|
+
deviceId = "mac_retina";
|
|
1526
|
+
captureMethod = "macos";
|
|
1527
|
+
break;
|
|
1528
|
+
case "watchos":
|
|
1529
|
+
deviceId = "watch_45mm";
|
|
1530
|
+
captureMethod = "watch";
|
|
1531
|
+
break;
|
|
1532
|
+
default:
|
|
1533
|
+
deviceId = "iphone_6_7";
|
|
1534
|
+
captureMethod = "simulator";
|
|
1535
|
+
}
|
|
1536
|
+
// Step 3: Prepare preview definitions
|
|
1537
|
+
const previewDefs = args.previews || [];
|
|
1538
|
+
// If previews not provided, this will be empty and Claude should fill them
|
|
1539
|
+
if (previewDefs.length === 0) {
|
|
1540
|
+
return JSON.stringify({
|
|
1541
|
+
success: false,
|
|
1542
|
+
needsInput: true,
|
|
1543
|
+
message: "Please provide preview titles and subtitles",
|
|
1544
|
+
hint: `Generate ${screenCount} marketing phrases for ${args.appName} in ${args.language}`,
|
|
1545
|
+
template: {
|
|
1546
|
+
previews: Array.from({ length: screenCount }, (_, i) => ({
|
|
1547
|
+
title: `[Title ${i + 1}]`,
|
|
1548
|
+
subtitle: `[Subtitle ${i + 1}]`,
|
|
1549
|
+
paletteId: palettes[i % palettes.length],
|
|
1550
|
+
})),
|
|
1551
|
+
},
|
|
1552
|
+
appInfo: {
|
|
1553
|
+
name: args.appName,
|
|
1554
|
+
description: args.appDescription,
|
|
1555
|
+
platform: args.platform,
|
|
1556
|
+
language: args.language,
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
// Step 4: Capture screenshots based on platform
|
|
1561
|
+
const capturedPreviews = [];
|
|
1562
|
+
if (captureMethod === "macos") {
|
|
1563
|
+
// macOS capture
|
|
1564
|
+
try {
|
|
1565
|
+
await execAsync(`open -b "${args.bundleId}"`);
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
return JSON.stringify({
|
|
1569
|
+
success: false,
|
|
1570
|
+
error: `App not found: ${args.bundleId}`,
|
|
1571
|
+
hint: "Make sure the macOS app is installed",
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
await sleep(3);
|
|
1575
|
+
for (let i = 0; i < previewDefs.length; i++) {
|
|
1576
|
+
const def = previewDefs[i];
|
|
1577
|
+
const timestamp = Date.now();
|
|
1578
|
+
const screenshotPath = path.join(SCREENSHOTS_DIR, `mac_${i + 1}_${timestamp}.png`);
|
|
1579
|
+
const captured = await captureMacOSWindow(args.bundleId, screenshotPath);
|
|
1580
|
+
if (captured) {
|
|
1581
|
+
const preview = {
|
|
1582
|
+
id: generateId(),
|
|
1583
|
+
screenshotPath,
|
|
1584
|
+
title: def.title,
|
|
1585
|
+
subtitle: def.subtitle,
|
|
1586
|
+
deviceId,
|
|
1587
|
+
paletteId: def.paletteId || palettes[i % palettes.length],
|
|
1588
|
+
createdAt: new Date().toISOString(),
|
|
1589
|
+
};
|
|
1590
|
+
store.previews.push(preview);
|
|
1591
|
+
capturedPreviews.push(preview);
|
|
1592
|
+
}
|
|
1593
|
+
if (i < previewDefs.length - 1) {
|
|
1594
|
+
await sleep(2);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
else {
|
|
1599
|
+
// iOS/watchOS simulator capture
|
|
1600
|
+
const bootedSimulators = await getBootedSimulators();
|
|
1601
|
+
if (bootedSimulators.length === 0) {
|
|
1602
|
+
return JSON.stringify({
|
|
1603
|
+
success: false,
|
|
1604
|
+
error: "No iOS Simulator is running",
|
|
1605
|
+
hint: "Please launch an iOS Simulator first",
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
// Find appropriate simulator
|
|
1609
|
+
let targetSimulator = args.simulatorUDID
|
|
1610
|
+
? bootedSimulators.find(s => s.udid === args.simulatorUDID)
|
|
1611
|
+
: null;
|
|
1612
|
+
if (!targetSimulator) {
|
|
1613
|
+
for (const sim of bootedSimulators) {
|
|
1614
|
+
try {
|
|
1615
|
+
await execAsync(`xcrun simctl get_app_container ${sim.udid} ${args.bundleId}`);
|
|
1616
|
+
targetSimulator = sim;
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
catch { }
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (!targetSimulator) {
|
|
1623
|
+
targetSimulator = bootedSimulators[0];
|
|
1624
|
+
}
|
|
1625
|
+
// Launch app
|
|
1626
|
+
try {
|
|
1627
|
+
await execAsync(`xcrun simctl terminate ${targetSimulator.udid} ${args.bundleId}`);
|
|
1628
|
+
await sleep(0.5);
|
|
1629
|
+
}
|
|
1630
|
+
catch { }
|
|
1631
|
+
try {
|
|
1632
|
+
await execAsync(`xcrun simctl launch ${targetSimulator.udid} ${args.bundleId}`);
|
|
1633
|
+
}
|
|
1634
|
+
catch (e) {
|
|
1635
|
+
return JSON.stringify({
|
|
1636
|
+
success: false,
|
|
1637
|
+
error: `Failed to launch app: ${e.message}`,
|
|
1638
|
+
hint: "Make sure the app is installed on the simulator",
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
// Track simulator
|
|
1642
|
+
lastLaunchedSimulator = {
|
|
1643
|
+
udid: targetSimulator.udid,
|
|
1644
|
+
name: targetSimulator.name,
|
|
1645
|
+
bundleId: args.bundleId,
|
|
1646
|
+
};
|
|
1647
|
+
await sleep(3);
|
|
1648
|
+
deviceId = mapSimulatorToDeviceId(targetSimulator.name);
|
|
1649
|
+
for (let i = 0; i < previewDefs.length; i++) {
|
|
1650
|
+
const def = previewDefs[i];
|
|
1651
|
+
const timestamp = Date.now();
|
|
1652
|
+
const screenshotPath = path.join(SCREENSHOTS_DIR, `ios_${i + 1}_${timestamp}.png`);
|
|
1653
|
+
await execAsync(`xcrun simctl io ${targetSimulator.udid} screenshot "${screenshotPath}"`);
|
|
1654
|
+
if (fs.existsSync(screenshotPath)) {
|
|
1655
|
+
const preview = {
|
|
1656
|
+
id: generateId(),
|
|
1657
|
+
screenshotPath,
|
|
1658
|
+
title: def.title,
|
|
1659
|
+
subtitle: def.subtitle,
|
|
1660
|
+
deviceId,
|
|
1661
|
+
paletteId: def.paletteId || palettes[i % palettes.length],
|
|
1662
|
+
createdAt: new Date().toISOString(),
|
|
1663
|
+
};
|
|
1664
|
+
store.previews.push(preview);
|
|
1665
|
+
capturedPreviews.push(preview);
|
|
1666
|
+
}
|
|
1667
|
+
if (i < previewDefs.length - 1) {
|
|
1668
|
+
await sleep(2);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
saveStore(store);
|
|
1673
|
+
// Step 5: Send to app or save to folder
|
|
1674
|
+
const pendingPath = path.join(dataDir, "pending-previews.json");
|
|
1675
|
+
fs.writeFileSync(pendingPath, JSON.stringify({ previews: store.previews }, null, 2));
|
|
1676
|
+
let appOpened = false;
|
|
1677
|
+
try {
|
|
1678
|
+
await execAsync(`open -b ${APP_BUNDLE_ID}`);
|
|
1679
|
+
appOpened = true;
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
// App not installed
|
|
1683
|
+
}
|
|
1684
|
+
if (appOpened) {
|
|
1685
|
+
return JSON.stringify({
|
|
1686
|
+
success: true,
|
|
1687
|
+
message: `Created ${capturedPreviews.length} preview(s) and opened MUSE Preview Maker`,
|
|
1688
|
+
platform: args.platform,
|
|
1689
|
+
language: args.language,
|
|
1690
|
+
previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle, paletteId: p.paletteId })),
|
|
1691
|
+
appOpened: true,
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
else {
|
|
1695
|
+
// Open folder instead
|
|
1696
|
+
await execAsync(`open "${dataDir}"`);
|
|
1697
|
+
return JSON.stringify({
|
|
1698
|
+
success: true,
|
|
1699
|
+
message: `Created ${capturedPreviews.length} preview(s). MUSE Preview Maker not installed.`,
|
|
1700
|
+
platform: args.platform,
|
|
1701
|
+
language: args.language,
|
|
1702
|
+
previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle, paletteId: p.paletteId })),
|
|
1703
|
+
appOpened: false,
|
|
1704
|
+
dataFolder: dataDir,
|
|
1705
|
+
hint: "Install MUSE Preview Maker to generate final images, or use the screenshots directly.",
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
catch (error) {
|
|
1710
|
+
return JSON.stringify({
|
|
1711
|
+
success: false,
|
|
1712
|
+
error: `Create app previews failed: ${error.message}`,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
// === Advanced Capture Handlers ===
|
|
1717
|
+
// Helper: Get target simulator (uses tracked or finds appropriate one)
|
|
1718
|
+
async function getTargetSimulator(simulatorUDID) {
|
|
1719
|
+
const bootedSimulators = await getBootedSimulators();
|
|
1720
|
+
if (bootedSimulators.length === 0)
|
|
1721
|
+
return null;
|
|
1722
|
+
if (simulatorUDID) {
|
|
1723
|
+
return bootedSimulators.find(s => s.udid === simulatorUDID) || null;
|
|
1724
|
+
}
|
|
1725
|
+
// Use last launched simulator if available
|
|
1726
|
+
if (lastLaunchedSimulator) {
|
|
1727
|
+
const found = bootedSimulators.find(s => s.udid === lastLaunchedSimulator.udid);
|
|
1728
|
+
if (found)
|
|
1729
|
+
return found;
|
|
1730
|
+
}
|
|
1731
|
+
return bootedSimulators[0];
|
|
1732
|
+
}
|
|
1733
|
+
// Helper: Perform touch/gesture via AppleScript (controls Simulator.app window)
|
|
1734
|
+
async function simulatorInteract(udid, action, params) {
|
|
1735
|
+
try {
|
|
1736
|
+
// Get simulator window info
|
|
1737
|
+
const { stdout: windowBounds } = await execAsync(`
|
|
1738
|
+
osascript -e 'tell application "Simulator"
|
|
1739
|
+
set frontmost to true
|
|
1740
|
+
delay 0.2
|
|
1741
|
+
end tell'
|
|
1742
|
+
`);
|
|
1743
|
+
// For swipe/scroll by direction, calculate coordinates
|
|
1744
|
+
let startX = params.x || 200;
|
|
1745
|
+
let startY = params.y || 400;
|
|
1746
|
+
let endX = params.toX || startX;
|
|
1747
|
+
let endY = params.toY || startY;
|
|
1748
|
+
const duration = params.duration || 0.3;
|
|
1749
|
+
if (params.direction) {
|
|
1750
|
+
const swipeDistance = 300;
|
|
1751
|
+
switch (params.direction) {
|
|
1752
|
+
case "up":
|
|
1753
|
+
endY = startY - swipeDistance;
|
|
1754
|
+
break;
|
|
1755
|
+
case "down":
|
|
1756
|
+
endY = startY + swipeDistance;
|
|
1757
|
+
break;
|
|
1758
|
+
case "left":
|
|
1759
|
+
endX = startX - swipeDistance;
|
|
1760
|
+
break;
|
|
1761
|
+
case "right":
|
|
1762
|
+
endX = startX + swipeDistance;
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
switch (action) {
|
|
1767
|
+
case "tap":
|
|
1768
|
+
// Use cliclick if available, otherwise AppleScript
|
|
1769
|
+
try {
|
|
1770
|
+
await execAsync(`cliclick c:${startX},${startY}`);
|
|
1771
|
+
}
|
|
1772
|
+
catch {
|
|
1773
|
+
// Fallback: Use AppleScript to click
|
|
1774
|
+
await execAsync(`
|
|
1775
|
+
osascript -e 'tell application "System Events"
|
|
1776
|
+
tell process "Simulator"
|
|
1777
|
+
click at {${startX}, ${startY}}
|
|
1778
|
+
end tell
|
|
1779
|
+
end tell'
|
|
1780
|
+
`);
|
|
1781
|
+
}
|
|
1782
|
+
break;
|
|
1783
|
+
case "swipe":
|
|
1784
|
+
case "scroll":
|
|
1785
|
+
// Use AppleScript for swipe gesture
|
|
1786
|
+
await execAsync(`
|
|
1787
|
+
osascript -e 'tell application "System Events"
|
|
1788
|
+
tell process "Simulator"
|
|
1789
|
+
set frontmost to true
|
|
1790
|
+
end tell
|
|
1791
|
+
end tell'
|
|
1792
|
+
`);
|
|
1793
|
+
// Simctl doesn't have direct swipe, but we can use privacy/UI automation
|
|
1794
|
+
// For now, use key events for scrolling
|
|
1795
|
+
if (action === "scroll") {
|
|
1796
|
+
const scrollKey = params.direction === "up" || params.direction === "left" ? "126" : "125";
|
|
1797
|
+
const count = 5;
|
|
1798
|
+
for (let i = 0; i < count; i++) {
|
|
1799
|
+
await execAsync(`
|
|
1800
|
+
osascript -e 'tell application "System Events" to key code ${scrollKey}'
|
|
1801
|
+
`);
|
|
1802
|
+
await sleep(0.1);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
break;
|
|
1806
|
+
case "type":
|
|
1807
|
+
if (params.text) {
|
|
1808
|
+
// Type text character by character
|
|
1809
|
+
await execAsync(`
|
|
1810
|
+
osascript -e 'tell application "System Events"
|
|
1811
|
+
tell process "Simulator"
|
|
1812
|
+
set frontmost to true
|
|
1813
|
+
keystroke "${params.text.replace(/"/g, '\\"')}"
|
|
1814
|
+
end tell
|
|
1815
|
+
end tell'
|
|
1816
|
+
`);
|
|
1817
|
+
}
|
|
1818
|
+
break;
|
|
1819
|
+
case "home":
|
|
1820
|
+
await execAsync(`xcrun simctl ui ${udid} home`);
|
|
1821
|
+
break;
|
|
1822
|
+
case "shake":
|
|
1823
|
+
// Trigger shake gesture (useful for debug menus)
|
|
1824
|
+
await execAsync(`
|
|
1825
|
+
osascript -e 'tell application "Simulator" to activate' -e 'tell application "System Events" to keystroke "z" using {control down, command down}'
|
|
1826
|
+
`);
|
|
1827
|
+
break;
|
|
1828
|
+
}
|
|
1829
|
+
return true;
|
|
1830
|
+
}
|
|
1831
|
+
catch (error) {
|
|
1832
|
+
console.error("Simulator interaction failed:", error);
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
// Handler: Interact with simulator
|
|
1837
|
+
async function handleInteractSimulator(args) {
|
|
1838
|
+
try {
|
|
1839
|
+
const simulator = await getTargetSimulator(args.simulatorUDID);
|
|
1840
|
+
if (!simulator) {
|
|
1841
|
+
return JSON.stringify({
|
|
1842
|
+
success: false,
|
|
1843
|
+
error: "No iOS Simulator is running",
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
const success = await simulatorInteract(simulator.udid, args.action, {
|
|
1847
|
+
x: args.x,
|
|
1848
|
+
y: args.y,
|
|
1849
|
+
toX: args.toX,
|
|
1850
|
+
toY: args.toY,
|
|
1851
|
+
direction: args.direction,
|
|
1852
|
+
text: args.text,
|
|
1853
|
+
duration: args.duration,
|
|
1854
|
+
});
|
|
1855
|
+
return JSON.stringify({
|
|
1856
|
+
success,
|
|
1857
|
+
message: success ? `Performed ${args.action} on simulator` : `${args.action} failed`,
|
|
1858
|
+
simulator: { name: simulator.name, udid: simulator.udid },
|
|
1859
|
+
action: args.action,
|
|
1860
|
+
params: { x: args.x, y: args.y, direction: args.direction },
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
catch (error) {
|
|
1864
|
+
return JSON.stringify({
|
|
1865
|
+
success: false,
|
|
1866
|
+
error: `Interact failed: ${error.message}`,
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
// Handler: Navigate app via deep link
|
|
1871
|
+
async function handleNavigateApp(args) {
|
|
1872
|
+
try {
|
|
1873
|
+
const simulator = await getTargetSimulator(args.simulatorUDID);
|
|
1874
|
+
if (!simulator) {
|
|
1875
|
+
return JSON.stringify({
|
|
1876
|
+
success: false,
|
|
1877
|
+
error: "No iOS Simulator is running",
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
// Open URL in simulator
|
|
1881
|
+
await execAsync(`xcrun simctl openurl ${simulator.udid} "${args.url}"`);
|
|
1882
|
+
// Wait for navigation
|
|
1883
|
+
await sleep(args.waitSeconds || 2);
|
|
1884
|
+
return JSON.stringify({
|
|
1885
|
+
success: true,
|
|
1886
|
+
message: `Navigated to ${args.url}`,
|
|
1887
|
+
simulator: { name: simulator.name, udid: simulator.udid },
|
|
1888
|
+
url: args.url,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
return JSON.stringify({
|
|
1893
|
+
success: false,
|
|
1894
|
+
error: `Navigation failed: ${error.message}`,
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
// Handler: Configure simulator
|
|
1899
|
+
async function handleConfigureSimulator(args) {
|
|
1900
|
+
try {
|
|
1901
|
+
const simulator = await getTargetSimulator(args.simulatorUDID);
|
|
1902
|
+
if (!simulator) {
|
|
1903
|
+
return JSON.stringify({
|
|
1904
|
+
success: false,
|
|
1905
|
+
error: "No iOS Simulator is running",
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
const changes = [];
|
|
1909
|
+
// Override status bar
|
|
1910
|
+
if (args.statusBarTime || args.batteryLevel !== undefined || args.cellularBars !== undefined || args.wifiBars !== undefined) {
|
|
1911
|
+
let statusBarCmd = `xcrun simctl status_bar ${simulator.udid} override`;
|
|
1912
|
+
if (args.statusBarTime) {
|
|
1913
|
+
statusBarCmd += ` --time "${args.statusBarTime}"`;
|
|
1914
|
+
changes.push(`time: ${args.statusBarTime}`);
|
|
1915
|
+
}
|
|
1916
|
+
if (args.batteryLevel !== undefined) {
|
|
1917
|
+
statusBarCmd += ` --batteryLevel ${args.batteryLevel}`;
|
|
1918
|
+
changes.push(`battery: ${args.batteryLevel}%`);
|
|
1919
|
+
}
|
|
1920
|
+
if (args.batteryState) {
|
|
1921
|
+
statusBarCmd += ` --batteryState ${args.batteryState}`;
|
|
1922
|
+
changes.push(`batteryState: ${args.batteryState}`);
|
|
1923
|
+
}
|
|
1924
|
+
if (args.cellularBars !== undefined) {
|
|
1925
|
+
statusBarCmd += ` --cellularBars ${args.cellularBars}`;
|
|
1926
|
+
changes.push(`cellular: ${args.cellularBars} bars`);
|
|
1927
|
+
}
|
|
1928
|
+
if (args.wifiBars !== undefined) {
|
|
1929
|
+
statusBarCmd += ` --wifiBars ${args.wifiBars}`;
|
|
1930
|
+
changes.push(`wifi: ${args.wifiBars} bars`);
|
|
1931
|
+
}
|
|
1932
|
+
await execAsync(statusBarCmd);
|
|
1933
|
+
}
|
|
1934
|
+
// Set appearance (dark/light mode)
|
|
1935
|
+
if (args.appearance) {
|
|
1936
|
+
await execAsync(`xcrun simctl ui ${simulator.udid} appearance ${args.appearance}`);
|
|
1937
|
+
changes.push(`appearance: ${args.appearance}`);
|
|
1938
|
+
}
|
|
1939
|
+
return JSON.stringify({
|
|
1940
|
+
success: true,
|
|
1941
|
+
message: "Simulator configured",
|
|
1942
|
+
simulator: { name: simulator.name, udid: simulator.udid },
|
|
1943
|
+
changes,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
catch (error) {
|
|
1947
|
+
return JSON.stringify({
|
|
1948
|
+
success: false,
|
|
1949
|
+
error: `Configure failed: ${error.message}`,
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
// Handler: Auto capture flow
|
|
1954
|
+
async function handleAutoCaptureFlow(args) {
|
|
1955
|
+
try {
|
|
1956
|
+
ensureScreenshotsDir();
|
|
1957
|
+
const simulator = await getTargetSimulator(args.simulatorUDID);
|
|
1958
|
+
if (!simulator) {
|
|
1959
|
+
return JSON.stringify({
|
|
1960
|
+
success: false,
|
|
1961
|
+
error: "No iOS Simulator is running",
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
// Reset if requested
|
|
1965
|
+
if (args.resetFirst !== false) {
|
|
1966
|
+
try {
|
|
1967
|
+
await execAsync(`xcrun simctl terminate ${simulator.udid} ${args.bundleId}`);
|
|
1968
|
+
await sleep(0.5);
|
|
1969
|
+
}
|
|
1970
|
+
catch { }
|
|
1971
|
+
}
|
|
1972
|
+
// Launch app
|
|
1973
|
+
try {
|
|
1974
|
+
await execAsync(`xcrun simctl launch ${simulator.udid} ${args.bundleId}`);
|
|
1975
|
+
await sleep(2);
|
|
1976
|
+
}
|
|
1977
|
+
catch (e) {
|
|
1978
|
+
return JSON.stringify({
|
|
1979
|
+
success: false,
|
|
1980
|
+
error: `Failed to launch app: ${e.message}`,
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
// Track this simulator
|
|
1984
|
+
lastLaunchedSimulator = {
|
|
1985
|
+
udid: simulator.udid,
|
|
1986
|
+
name: simulator.name,
|
|
1987
|
+
bundleId: args.bundleId,
|
|
1988
|
+
};
|
|
1989
|
+
const store = loadStore();
|
|
1990
|
+
const capturedPreviews = [];
|
|
1991
|
+
const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
|
|
1992
|
+
const deviceId = mapSimulatorToDeviceId(simulator.name);
|
|
1993
|
+
let captureIndex = 0;
|
|
1994
|
+
// Execute flow
|
|
1995
|
+
for (let i = 0; i < args.flow.length; i++) {
|
|
1996
|
+
const step = args.flow[i];
|
|
1997
|
+
switch (step.action) {
|
|
1998
|
+
case "capture":
|
|
1999
|
+
const timestamp = Date.now();
|
|
2000
|
+
const screenshotPath = path.join(SCREENSHOTS_DIR, `flow_${captureIndex + 1}_${timestamp}.png`);
|
|
2001
|
+
await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${screenshotPath}"`);
|
|
2002
|
+
if (fs.existsSync(screenshotPath)) {
|
|
2003
|
+
const preview = {
|
|
2004
|
+
id: generateId(),
|
|
2005
|
+
screenshotPath,
|
|
2006
|
+
title: step.title || `Screen ${captureIndex + 1}`,
|
|
2007
|
+
subtitle: step.subtitle || "",
|
|
2008
|
+
deviceId,
|
|
2009
|
+
paletteId: step.paletteId || palettes[captureIndex % palettes.length],
|
|
2010
|
+
createdAt: new Date().toISOString(),
|
|
2011
|
+
};
|
|
2012
|
+
store.previews.push(preview);
|
|
2013
|
+
capturedPreviews.push(preview);
|
|
2014
|
+
captureIndex++;
|
|
2015
|
+
}
|
|
2016
|
+
break;
|
|
2017
|
+
case "tap":
|
|
2018
|
+
case "swipe":
|
|
2019
|
+
case "scroll":
|
|
2020
|
+
case "type":
|
|
2021
|
+
await simulatorInteract(simulator.udid, step.action, {
|
|
2022
|
+
x: step.x,
|
|
2023
|
+
y: step.y,
|
|
2024
|
+
toX: step.toX,
|
|
2025
|
+
toY: step.toY,
|
|
2026
|
+
direction: step.direction,
|
|
2027
|
+
text: step.text,
|
|
2028
|
+
});
|
|
2029
|
+
await sleep(0.5);
|
|
2030
|
+
break;
|
|
2031
|
+
case "wait":
|
|
2032
|
+
await sleep(step.seconds || 1);
|
|
2033
|
+
break;
|
|
2034
|
+
case "navigate":
|
|
2035
|
+
if (step.url) {
|
|
2036
|
+
await execAsync(`xcrun simctl openurl ${simulator.udid} "${step.url}"`);
|
|
2037
|
+
await sleep(step.seconds || 2);
|
|
2038
|
+
}
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
saveStore(store);
|
|
2043
|
+
return JSON.stringify({
|
|
2044
|
+
success: true,
|
|
2045
|
+
message: `Executed flow with ${args.flow.length} steps, captured ${capturedPreviews.length} screenshots`,
|
|
2046
|
+
simulator: { name: simulator.name, udid: simulator.udid },
|
|
2047
|
+
previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle })),
|
|
2048
|
+
totalPreviews: store.previews.length,
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
catch (error) {
|
|
2052
|
+
return JSON.stringify({
|
|
2053
|
+
success: false,
|
|
2054
|
+
error: `Auto capture flow failed: ${error.message}`,
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// Handler: Scroll and capture
|
|
2059
|
+
async function handleScrollAndCapture(args) {
|
|
2060
|
+
try {
|
|
2061
|
+
ensureScreenshotsDir();
|
|
2062
|
+
const simulator = await getTargetSimulator(args.simulatorUDID);
|
|
2063
|
+
if (!simulator) {
|
|
2064
|
+
return JSON.stringify({
|
|
2065
|
+
success: false,
|
|
2066
|
+
error: "No iOS Simulator is running",
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
const scrollCount = args.scrollCount || 3;
|
|
2070
|
+
const direction = args.direction || "down";
|
|
2071
|
+
const waitBetween = args.waitBetween || 1;
|
|
2072
|
+
const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
|
|
2073
|
+
const store = loadStore();
|
|
2074
|
+
const capturedPreviews = [];
|
|
2075
|
+
const deviceId = mapSimulatorToDeviceId(simulator.name);
|
|
2076
|
+
// Capture initial state
|
|
2077
|
+
const initialTimestamp = Date.now();
|
|
2078
|
+
const initialPath = path.join(SCREENSHOTS_DIR, `scroll_0_${initialTimestamp}.png`);
|
|
2079
|
+
await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${initialPath}"`);
|
|
2080
|
+
if (fs.existsSync(initialPath)) {
|
|
2081
|
+
const preview = {
|
|
2082
|
+
id: generateId(),
|
|
2083
|
+
screenshotPath: initialPath,
|
|
2084
|
+
title: args.titles?.[0] || "Overview",
|
|
2085
|
+
subtitle: args.subtitles?.[0] || "",
|
|
2086
|
+
deviceId,
|
|
2087
|
+
paletteId: args.paletteIds?.[0] || palettes[0],
|
|
2088
|
+
createdAt: new Date().toISOString(),
|
|
2089
|
+
};
|
|
2090
|
+
store.previews.push(preview);
|
|
2091
|
+
capturedPreviews.push(preview);
|
|
2092
|
+
}
|
|
2093
|
+
// Scroll and capture
|
|
2094
|
+
for (let i = 0; i < scrollCount; i++) {
|
|
2095
|
+
await simulatorInteract(simulator.udid, "scroll", { direction });
|
|
2096
|
+
await sleep(waitBetween);
|
|
2097
|
+
if (args.captureEach !== false) {
|
|
2098
|
+
const timestamp = Date.now();
|
|
2099
|
+
const screenshotPath = path.join(SCREENSHOTS_DIR, `scroll_${i + 1}_${timestamp}.png`);
|
|
2100
|
+
await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${screenshotPath}"`);
|
|
2101
|
+
if (fs.existsSync(screenshotPath)) {
|
|
2102
|
+
const preview = {
|
|
2103
|
+
id: generateId(),
|
|
2104
|
+
screenshotPath,
|
|
2105
|
+
title: args.titles?.[i + 1] || `Section ${i + 2}`,
|
|
2106
|
+
subtitle: args.subtitles?.[i + 1] || "",
|
|
2107
|
+
deviceId,
|
|
2108
|
+
paletteId: args.paletteIds?.[i + 1] || palettes[(i + 1) % palettes.length],
|
|
2109
|
+
createdAt: new Date().toISOString(),
|
|
2110
|
+
};
|
|
2111
|
+
store.previews.push(preview);
|
|
2112
|
+
capturedPreviews.push(preview);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
saveStore(store);
|
|
2117
|
+
return JSON.stringify({
|
|
2118
|
+
success: true,
|
|
2119
|
+
message: `Scrolled ${scrollCount} times and captured ${capturedPreviews.length} screenshots`,
|
|
2120
|
+
simulator: { name: simulator.name, udid: simulator.udid },
|
|
2121
|
+
previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle })),
|
|
2122
|
+
totalPreviews: store.previews.length,
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
catch (error) {
|
|
2126
|
+
return JSON.stringify({
|
|
2127
|
+
success: false,
|
|
2128
|
+
error: `Scroll and capture failed: ${error.message}`,
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
// Handler: Batch capture on multiple devices
|
|
2133
|
+
async function handleBatchCaptureDevices(args) {
|
|
2134
|
+
try {
|
|
2135
|
+
ensureScreenshotsDir();
|
|
2136
|
+
const bootedSimulators = await getBootedSimulators();
|
|
2137
|
+
if (bootedSimulators.length === 0) {
|
|
2138
|
+
return JSON.stringify({
|
|
2139
|
+
success: false,
|
|
2140
|
+
error: "No iOS Simulators are running",
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
const waitSeconds = args.waitSeconds || 3;
|
|
2144
|
+
const store = loadStore();
|
|
2145
|
+
const capturedPreviews = [];
|
|
2146
|
+
// If specific devices requested, filter
|
|
2147
|
+
let targetSimulators = bootedSimulators;
|
|
2148
|
+
if (args.devices && args.devices.length > 0) {
|
|
2149
|
+
targetSimulators = bootedSimulators.filter(s => args.devices.some(d => s.name.toLowerCase().includes(d.toLowerCase()) || s.udid === d));
|
|
2150
|
+
}
|
|
2151
|
+
if (targetSimulators.length === 0) {
|
|
2152
|
+
return JSON.stringify({
|
|
2153
|
+
success: false,
|
|
2154
|
+
error: "No matching simulators found",
|
|
2155
|
+
availableSimulators: bootedSimulators.map(s => s.name),
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
// Capture on each device
|
|
2159
|
+
for (const simulator of targetSimulators) {
|
|
2160
|
+
// Launch app
|
|
2161
|
+
try {
|
|
2162
|
+
await execAsync(`xcrun simctl terminate ${simulator.udid} ${args.bundleId}`);
|
|
2163
|
+
await sleep(0.3);
|
|
2164
|
+
}
|
|
2165
|
+
catch { }
|
|
2166
|
+
try {
|
|
2167
|
+
await execAsync(`xcrun simctl launch ${simulator.udid} ${args.bundleId}`);
|
|
2168
|
+
}
|
|
2169
|
+
catch (e) {
|
|
2170
|
+
continue; // Skip if app not installed on this device
|
|
2171
|
+
}
|
|
2172
|
+
await sleep(waitSeconds);
|
|
2173
|
+
// Capture
|
|
2174
|
+
const timestamp = Date.now();
|
|
2175
|
+
const safeName = simulator.name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
2176
|
+
const screenshotPath = path.join(SCREENSHOTS_DIR, `batch_${safeName}_${timestamp}.png`);
|
|
2177
|
+
await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${screenshotPath}"`);
|
|
2178
|
+
if (fs.existsSync(screenshotPath)) {
|
|
2179
|
+
const preview = {
|
|
2180
|
+
id: generateId(),
|
|
2181
|
+
screenshotPath,
|
|
2182
|
+
title: args.title,
|
|
2183
|
+
subtitle: args.subtitle,
|
|
2184
|
+
deviceId: mapSimulatorToDeviceId(simulator.name),
|
|
2185
|
+
paletteId: args.paletteId || store.settings.defaultPaletteId,
|
|
2186
|
+
createdAt: new Date().toISOString(),
|
|
2187
|
+
};
|
|
2188
|
+
store.previews.push(preview);
|
|
2189
|
+
capturedPreviews.push(preview);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
saveStore(store);
|
|
2193
|
+
return JSON.stringify({
|
|
2194
|
+
success: true,
|
|
2195
|
+
message: `Captured on ${capturedPreviews.length} device(s)`,
|
|
2196
|
+
previews: capturedPreviews.map(p => ({
|
|
2197
|
+
title: p.title,
|
|
2198
|
+
deviceId: p.deviceId,
|
|
2199
|
+
})),
|
|
2200
|
+
totalPreviews: store.previews.length,
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
catch (error) {
|
|
2204
|
+
return JSON.stringify({
|
|
2205
|
+
success: false,
|
|
2206
|
+
error: `Batch capture failed: ${error.message}`,
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
// Handler: Record and extract frames
|
|
2211
|
+
async function handleRecordAndExtract(args) {
|
|
2212
|
+
try {
|
|
2213
|
+
ensureScreenshotsDir();
|
|
2214
|
+
const simulator = await getTargetSimulator(args.simulatorUDID);
|
|
2215
|
+
if (!simulator) {
|
|
2216
|
+
return JSON.stringify({
|
|
2217
|
+
success: false,
|
|
2218
|
+
error: "No iOS Simulator is running",
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
const duration = args.duration || 5;
|
|
2222
|
+
const frameCount = args.frameCount || 3;
|
|
2223
|
+
const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
|
|
2224
|
+
// Record video
|
|
2225
|
+
const timestamp = Date.now();
|
|
2226
|
+
const videoPath = path.join(SCREENSHOTS_DIR, `recording_${timestamp}.mp4`);
|
|
2227
|
+
// Start recording in background
|
|
2228
|
+
const recordProcess = exec(`xcrun simctl io ${simulator.udid} recordVideo --codec=h264 "${videoPath}"`);
|
|
2229
|
+
// Wait for duration
|
|
2230
|
+
await sleep(duration);
|
|
2231
|
+
// Stop recording
|
|
2232
|
+
recordProcess.kill("SIGINT");
|
|
2233
|
+
await sleep(1);
|
|
2234
|
+
// Check if video was created
|
|
2235
|
+
if (!fs.existsSync(videoPath)) {
|
|
2236
|
+
return JSON.stringify({
|
|
2237
|
+
success: false,
|
|
2238
|
+
error: "Failed to record video",
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
// Extract frames using ffmpeg
|
|
2242
|
+
const store = loadStore();
|
|
2243
|
+
const capturedPreviews = [];
|
|
2244
|
+
const deviceId = mapSimulatorToDeviceId(simulator.name);
|
|
2245
|
+
try {
|
|
2246
|
+
// Get video duration
|
|
2247
|
+
const { stdout: durationInfo } = await execAsync(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`);
|
|
2248
|
+
const videoDuration = parseFloat(durationInfo.trim());
|
|
2249
|
+
const interval = videoDuration / (frameCount + 1);
|
|
2250
|
+
for (let i = 0; i < frameCount; i++) {
|
|
2251
|
+
const timeOffset = interval * (i + 1);
|
|
2252
|
+
const framePath = path.join(SCREENSHOTS_DIR, `frame_${i + 1}_${timestamp}.png`);
|
|
2253
|
+
await execAsync(`ffmpeg -ss ${timeOffset} -i "${videoPath}" -vframes 1 -q:v 2 "${framePath}" -y`);
|
|
2254
|
+
if (fs.existsSync(framePath)) {
|
|
2255
|
+
const preview = {
|
|
2256
|
+
id: generateId(),
|
|
2257
|
+
screenshotPath: framePath,
|
|
2258
|
+
title: args.titles?.[i] || `Frame ${i + 1}`,
|
|
2259
|
+
subtitle: args.subtitles?.[i] || "",
|
|
2260
|
+
deviceId,
|
|
2261
|
+
paletteId: palettes[i % palettes.length],
|
|
2262
|
+
createdAt: new Date().toISOString(),
|
|
2263
|
+
};
|
|
2264
|
+
store.previews.push(preview);
|
|
2265
|
+
capturedPreviews.push(preview);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
catch (ffmpegError) {
|
|
2270
|
+
// ffmpeg not available, fall back to regular captures at intervals
|
|
2271
|
+
return JSON.stringify({
|
|
2272
|
+
success: false,
|
|
2273
|
+
error: "ffmpeg not found. Install it with: brew install ffmpeg",
|
|
2274
|
+
hint: "Use scroll_and_capture or auto_capture_flow as alternatives",
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
// Clean up video file
|
|
2278
|
+
try {
|
|
2279
|
+
fs.unlinkSync(videoPath);
|
|
2280
|
+
}
|
|
2281
|
+
catch { }
|
|
2282
|
+
saveStore(store);
|
|
2283
|
+
return JSON.stringify({
|
|
2284
|
+
success: true,
|
|
2285
|
+
message: `Recorded ${duration}s video and extracted ${capturedPreviews.length} frames`,
|
|
2286
|
+
simulator: { name: simulator.name, udid: simulator.udid },
|
|
2287
|
+
previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle })),
|
|
2288
|
+
totalPreviews: store.previews.length,
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
catch (error) {
|
|
2292
|
+
return JSON.stringify({
|
|
2293
|
+
success: false,
|
|
2294
|
+
error: `Record and extract failed: ${error.message}`,
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
1003
2298
|
// Main server setup
|
|
1004
2299
|
const server = new Server({
|
|
1005
2300
|
name: "muse-app-preview-mcp",
|
|
1006
|
-
version: "1.
|
|
2301
|
+
version: "1.5.0",
|
|
1007
2302
|
}, {
|
|
1008
2303
|
capabilities: {
|
|
1009
2304
|
tools: {},
|
|
@@ -1060,6 +2355,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1060
2355
|
case "capture_app_screens":
|
|
1061
2356
|
result = await handleCaptureAppScreens(args);
|
|
1062
2357
|
break;
|
|
2358
|
+
case "create_app_previews":
|
|
2359
|
+
result = await handleCreateAppPreviews(args);
|
|
2360
|
+
break;
|
|
2361
|
+
// Advanced Capture Tools
|
|
2362
|
+
case "interact_simulator":
|
|
2363
|
+
result = await handleInteractSimulator(args);
|
|
2364
|
+
break;
|
|
2365
|
+
case "navigate_app":
|
|
2366
|
+
result = await handleNavigateApp(args);
|
|
2367
|
+
break;
|
|
2368
|
+
case "configure_simulator":
|
|
2369
|
+
result = await handleConfigureSimulator(args);
|
|
2370
|
+
break;
|
|
2371
|
+
case "auto_capture_flow":
|
|
2372
|
+
result = await handleAutoCaptureFlow(args);
|
|
2373
|
+
break;
|
|
2374
|
+
case "scroll_and_capture":
|
|
2375
|
+
result = await handleScrollAndCapture(args);
|
|
2376
|
+
break;
|
|
2377
|
+
case "batch_capture_devices":
|
|
2378
|
+
result = await handleBatchCaptureDevices(args);
|
|
2379
|
+
break;
|
|
2380
|
+
case "record_and_extract":
|
|
2381
|
+
result = await handleRecordAndExtract(args);
|
|
2382
|
+
break;
|
|
1063
2383
|
default:
|
|
1064
2384
|
result = JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
1065
2385
|
}
|