|
1 | | -import { |
2 | | - executeShellExec, |
3 | | - executeShellTerminal, |
4 | | - executeStartDevServer, |
5 | | -} from "@cheatcode/agent-core/tools/code"; |
| 1 | +import { executeShellExec, executeShellTerminal } from "@cheatcode/agent-core/tools/code"; |
6 | 2 | import { |
7 | 3 | type AnalyticsBindings, |
8 | 4 | APIError, |
@@ -56,6 +52,9 @@ const DEFAULT_MOBILE_PORT = 8081; |
56 | 52 | // Keep the header-free Expo Go capability short-lived. It is re-minted whenever |
57 | 53 | // the preview wakes, so a fresh QR is available without leaving a day-long URL live. |
58 | 54 | const SIGNED_PREVIEW_TTL_SECONDS = 60 * 60; |
| 55 | +const PREVIEW_KEEP_ALIVE_MS = 60 * 60 * 1_000; |
| 56 | +const PREVIEW_MAX_RESTARTS = 3; |
| 57 | +const PREVIEW_START_TIMEOUT_MS = 180_000; |
59 | 58 |
|
60 | 59 | type AppendChunk = (chunk: UIMessageChunk) => Promise<void>; |
61 | 60 |
|
@@ -510,34 +509,23 @@ async function startExpoDevServer( |
510 | 509 | // header-free URL, and Metro must know its public host (EXPO_PACKAGER_PROXY_URL) so the manifest's |
511 | 510 | // launchAsset/bundle URLs point at the signed host instead of 127.0.0.1 (which Expo Go can't hit). |
512 | 511 | const signedUrl = await getSignedMetroUrl(sandbox, logger, workspace.port); |
513 | | - await executeStartDevServer( |
514 | | - { |
515 | | - // `--web` makes the single Metro dev server also serve the react-native-web |
516 | | - // build as a real web page at `/` (iframe-renderable in the Computer panel), |
517 | | - // while the SAME server keeps answering exp:// manifests for Expo Go — so we |
518 | | - // get both the in-panel preview and the QR from one process on the project port. |
519 | | - // Metro runs against the same supervised native-disk source mirror as Next. Durable |
520 | | - // workspace edits reach that mirror atomically, so Metro's native watcher hot-reloads them |
521 | | - // without the old post-run restart. |
522 | | - command: localExpoPreviewCommand({ |
523 | | - port: workspace.port, |
524 | | - sourceDir: workspace.dir, |
525 | | - workspaceSlug: workspace.slug, |
526 | | - }), |
527 | | - cwd: workspace.dir, |
528 | | - env: { |
529 | | - CHEATCODE_APP_RUNTIME: "expo", |
530 | | - CI: "1", |
531 | | - EXPO_NO_TELEMETRY: "1", |
532 | | - ...(signedUrl ? { EXPO_PACKAGER_PROXY_URL: signedUrl } : {}), |
533 | | - }, |
534 | | - isMobile: true, |
535 | | - name: workspace.slot, |
| 512 | + // `--web` makes the single Metro dev server also serve the react-native-web build as a real page |
| 513 | + // in Computer while the same process answers Expo Go manifests. This command is already the |
| 514 | + // trusted native-mirror launcher, so it must not pass through model-command normalization. |
| 515 | + await startManagedPreview(sandbox, workspace, { |
| 516 | + command: localExpoPreviewCommand({ |
536 | 517 | port: workspace.port, |
537 | | - timeoutMs: 180_000, |
| 518 | + sourceDir: workspace.dir, |
| 519 | + workspaceSlug: workspace.slug, |
| 520 | + }), |
| 521 | + env: { |
| 522 | + CHEATCODE_APP_RUNTIME: "expo", |
| 523 | + CI: "1", |
| 524 | + EXPO_NO_TELEMETRY: "1", |
| 525 | + ...(signedUrl ? { EXPO_PACKAGER_PROXY_URL: signedUrl } : {}), |
538 | 526 | }, |
539 | | - { sandbox, workspaceDir: workspace.dir, workspaceSlug: workspace.slug }, |
540 | | - ); |
| 527 | + shouldReuseMatchingProcess: false, |
| 528 | + }); |
541 | 529 | } |
542 | 530 |
|
543 | 531 | // Best-effort Metro bootstrap capability. It is passed only to the live process so Metro emits |
@@ -566,28 +554,54 @@ async function startAppBuilderDevServer( |
566 | 554 | sandbox: ProjectSandboxStub, |
567 | 555 | workspace: AppBuilderWorkspace, |
568 | 556 | ): Promise<void> { |
569 | | - await executeStartDevServer( |
570 | | - { |
571 | | - command: localNextPreviewCommand({ |
572 | | - port: workspace.port, |
573 | | - sourceDir: workspace.dir, |
574 | | - workspaceSlug: workspace.slug, |
575 | | - }), |
576 | | - cwd: workspace.dir, |
577 | | - env: { |
578 | | - CHEATCODE_APP_RUNTIME: "next", |
579 | | - CHEATCODE_NEXT_DIST_DIR: "../cache/next", |
580 | | - CHOKIDAR_USEPOLLING: "true", |
581 | | - WATCHPACK_POLLING: "1000", |
582 | | - }, |
583 | | - isMobile: false, |
584 | | - name: workspace.slot, |
| 557 | + await startManagedPreview(sandbox, workspace, { |
| 558 | + command: localNextPreviewCommand({ |
585 | 559 | port: workspace.port, |
586 | | - shouldReuseMatchingProcess: true, |
587 | | - timeoutMs: 180_000, |
| 560 | + sourceDir: workspace.dir, |
| 561 | + workspaceSlug: workspace.slug, |
| 562 | + }), |
| 563 | + env: { |
| 564 | + CHEATCODE_APP_RUNTIME: "next", |
| 565 | + CHEATCODE_NEXT_DIST_DIR: "../cache/next", |
| 566 | + CHOKIDAR_USEPOLLING: "true", |
| 567 | + WATCHPACK_POLLING: "1000", |
588 | 568 | }, |
589 | | - { sandbox, workspaceDir: workspace.dir, workspaceSlug: workspace.slug }, |
590 | | - ); |
| 569 | + shouldReuseMatchingProcess: true, |
| 570 | + }); |
| 571 | +} |
| 572 | + |
| 573 | +interface ManagedPreviewOptions { |
| 574 | + command: string[]; |
| 575 | + env: Record<string, string>; |
| 576 | + shouldReuseMatchingProcess: boolean; |
| 577 | +} |
| 578 | + |
| 579 | +async function startManagedPreview( |
| 580 | + sandbox: ProjectSandboxStub, |
| 581 | + workspace: AppBuilderWorkspace, |
| 582 | + options: ManagedPreviewOptions, |
| 583 | +): Promise<void> { |
| 584 | + await sandbox.startProcess({ |
| 585 | + command: options.command, |
| 586 | + cwd: workspace.dir, |
| 587 | + env: { |
| 588 | + ...options.env, |
| 589 | + HOST: "0.0.0.0", |
| 590 | + HOSTNAME: "0.0.0.0", |
| 591 | + PORT: String(workspace.port), |
| 592 | + }, |
| 593 | + isMobile: workspace.mobile, |
| 594 | + keepAliveTimeoutMs: PREVIEW_KEEP_ALIVE_MS, |
| 595 | + maxRestarts: PREVIEW_MAX_RESTARTS, |
| 596 | + processId: workspace.slot, |
| 597 | + restartOnFailure: true, |
| 598 | + shouldReuseMatchingProcess: options.shouldReuseMatchingProcess, |
| 599 | + timeoutMs: PREVIEW_START_TIMEOUT_MS, |
| 600 | + waitForPort: { |
| 601 | + port: workspace.port, |
| 602 | + timeoutMs: PREVIEW_START_TIMEOUT_MS, |
| 603 | + }, |
| 604 | + }); |
591 | 605 | } |
592 | 606 |
|
593 | 607 | async function hasExistingAppBuilderWorkspace( |
|
0 commit comments