@@ -13,12 +13,23 @@ import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts";
1313import type { ProjectMode } from "@cheatcode/types/api" ;
1414import type { UIMessageChunk } from "ai" ;
1515import {
16+ ensureAppBuilderRuntime ,
1617 ensureExpoWebSupport ,
1718 installAppBuilderDependencies ,
1819 scaffoldAppBuilder ,
1920 scaffoldExpoApp ,
2021 writeAppBuilderFiles ,
22+ writeExpoRuntimeFiles ,
2123} from "./agent-run-app-builder-scaffold" ;
24+ import {
25+ EXPO_RUNTIME_BIN ,
26+ EXPO_TEMPLATE_DIR ,
27+ NEXT_RUNTIME_BIN ,
28+ NEXT_TEMPLATE_DIR ,
29+ projectLocalCacheDir ,
30+ projectLocalModulesDir ,
31+ projectLocalRuntimeDir ,
32+ } from "./project-sandbox-package-runtime" ;
2233
2334export type ProjectSandboxStub = CodeRuntimeContext [ "sandbox" ] ;
2435export type AgentRunLogger = ReturnType < typeof createLogger > ;
@@ -182,7 +193,7 @@ function templateContextNote(workspace: AppBuilderWorkspace): string {
182193 const mobile = workspace . mobile
183194 ? " For this mobile build, that internal address renders the react-native-web preview."
184195 : "" ;
185- return `[context] A ${ stack } workspace is scaffolded in ${ workspace . dir } , and its managed dev server is running internally at http://localhost:${ workspace . port } . Build the user's app by editing files under ${ workspace . dir } ; it hot-reloads on save. Verify it with the sandbox's headed browser at that internal localhost address.${ mobile } Never request or paste an external preview or Expo URL.` ;
196+ return `[context] A ${ stack } workspace is scaffolded in ${ workspace . dir } , and its managed dev server is running internally at http://localhost:${ workspace . port } . Build the user's app by editing files under ${ workspace . dir } ; it hot-reloads on save. Use pnpm for dependency changes. Verify it with the sandbox's headed browser at that internal localhost address.${ mobile } Never request or paste an external preview or Expo URL.` ;
186197}
187198
188199async function prepareTemplateWorkspace (
@@ -191,9 +202,10 @@ async function prepareTemplateWorkspace(
191202 const { input, logger, sandbox, setRunStage, shouldBootstrap, workspace } = options ;
192203 const mobile = workspace . mobile ;
193204 setRunStage ( mobile ? "Preparing the Expo workspace." : "Preparing the Next.js workspace." ) ;
205+ await ensureAppBuilderRuntime ( sandbox , mobile ) ;
194206 if ( ! shouldBootstrap ) {
195207 setRunStage ( "Restoring the app workspace." ) ;
196- if ( ! ( await hasInstalledAppBuilderDependencies ( sandbox , workspace . dir , mobile ) ) ) {
208+ if ( ! ( await hasInstalledAppBuilderDependencies ( sandbox , workspace ) ) ) {
197209 await installAppBuilderDependencies ( sandbox , logger , workspace . dir , mobile ) ;
198210 }
199211 if ( mobile ) {
@@ -205,10 +217,10 @@ async function prepareTemplateWorkspace(
205217 throwIfRunCanceled ( options . abortSignal ) ;
206218 if ( mobile ) {
207219 await scaffoldExpoApp ( sandbox , logger , workspace . dir ) ;
220+ await writeExpoRuntimeFiles ( sandbox , workspace . dir , workspace . slug ) ;
208221 } else {
209222 await scaffoldAppBuilder ( sandbox , logger , workspace . dir ) ;
210223 }
211- await installAppBuilderDependencies ( sandbox , logger , workspace . dir , mobile ) ;
212224 throwIfRunCanceled ( options . abortSignal ) ;
213225 if ( mobile ) {
214226 await ensureExpoWebSupport ( sandbox , workspace . dir ) ;
@@ -403,10 +415,7 @@ async function installImportedDependencies(
403415 if ( ! ( await pathExists ( sandbox , `${ dir } /package.json` ) ) ) {
404416 return false ;
405417 }
406- const usesNpm = await pathExists ( sandbox , `${ dir } /package-lock.json` ) ;
407- const command = usesNpm
408- ? [ "npm" , "install" , "--no-audit" , "--no-fund" ]
409- : [ "pnpm" , "install" , "--prefer-offline" , "--network-concurrency" , "4" ] ;
418+ const command = [ "pnpm" , "install" , "--prefer-offline" , "--network-concurrency" , "4" ] ;
410419 try {
411420 await executeShellExec ( { command, cwd : dir , timeoutMs : 300_000 } , { sandbox } ) ;
412421 return true ;
@@ -473,7 +482,7 @@ function parseGitHubRepo(url: string): GitHubRepoRef | null {
473482function importedContextNote ( workspace : AppBuilderWorkspace , repoUrl ?: string ) : string {
474483 const origin = repoUrl ? ` from ${ repoUrl } ` : "" ;
475484 const mobilePort = DEFAULT_MOBILE_PORT ;
476- return `[context] This project was imported${ origin } into ${ workspace . dir } . Inspect it, complete any setup, and start the dev server on port ${ workspace . port } with code_start_dev_server (Expo on ${ mobilePort } for mobile).` ;
485+ return `[context] This project was imported${ origin } into ${ workspace . dir } . Inspect it, use pnpm for dependency changes, complete any setup, and start the dev server on port ${ workspace . port } with code_start_dev_server (Expo on ${ mobilePort } for mobile).` ;
477486}
478487
479488function repoImportError ( message : string ) : APIError {
@@ -517,9 +526,7 @@ async function startExpoDevServer(
517526 // independent among the other flags): harmless on the initial boot, and what
518527 // makes the post-edit restart (restartMobilePreview) re-crawl from a clean slate.
519528 command : [
520- "pnpm" ,
521- "exec" ,
522- "expo" ,
529+ EXPO_RUNTIME_BIN ,
523530 "start" ,
524531 "-c" ,
525532 "--web" ,
@@ -530,6 +537,7 @@ async function startExpoDevServer(
530537 ] ,
531538 cwd : workspace . dir ,
532539 env : {
540+ CHEATCODE_APP_RUNTIME : "expo" ,
533541 CI : "1" ,
534542 EXPO_NO_TELEMETRY : "1" ,
535543 ...( signedUrl ? { EXPO_PACKAGER_PROXY_URL : signedUrl } : { } ) ,
@@ -572,9 +580,7 @@ async function startAppBuilderDevServer(
572580 await executeStartDevServer (
573581 {
574582 command : [
575- "pnpm" ,
576- "exec" ,
577- "next" ,
583+ NEXT_RUNTIME_BIN ,
578584 "dev" ,
579585 "--webpack" ,
580586 "--hostname" ,
@@ -584,6 +590,7 @@ async function startAppBuilderDevServer(
584590 ] ,
585591 cwd : workspace . dir ,
586592 env : {
593+ CHEATCODE_APP_RUNTIME : "next" ,
587594 CHOKIDAR_USEPOLLING : "true" ,
588595 WATCHPACK_POLLING : "1000" ,
589596 } ,
@@ -616,20 +623,29 @@ async function hasExistingAppBuilderWorkspace(
616623
617624async function hasInstalledAppBuilderDependencies (
618625 sandbox : ProjectSandboxStub ,
619- dir : string ,
620- mobile : boolean ,
626+ workspace : AppBuilderWorkspace ,
621627) : Promise < boolean > {
622- const requiredPaths = mobile
628+ const templateDir = workspace . mobile ? EXPO_TEMPLATE_DIR : NEXT_TEMPLATE_DIR ;
629+ const modulesDir = projectLocalModulesDir ( workspace . slug ) ;
630+ const requiredPaths = workspace . mobile
623631 ? [
624- "node_modules /.pnpm" ,
625- "node_modules /@expo/metro-runtime" ,
626- "node_modules /react-dom" ,
627- "node_modules /react-native-web" ,
632+ ` ${ modulesDir } /.pnpm` ,
633+ ` ${ modulesDir } /@expo/metro-runtime` ,
634+ ` ${ modulesDir } /react-dom` ,
635+ ` ${ modulesDir } /react-native-web` ,
628636 ]
629- : [ "node_modules/.pnpm" , "node_modules/next" , "node_modules/react" , "node_modules/react-dom" ] ;
637+ : [
638+ `${ modulesDir } /.pnpm` ,
639+ `${ modulesDir } /next` ,
640+ `${ modulesDir } /react` ,
641+ `${ modulesDir } /react-dom` ,
642+ ] ;
630643 const result = await executeShellTerminal (
631644 {
632- command : requiredPaths . map ( ( path ) => `test -d ${ dir } /${ path } ` ) . join ( " && " ) ,
645+ command :
646+ `(cmp -s ${ workspace . dir } /package.json ${ templateDir } /package.json && ` +
647+ `cmp -s ${ workspace . dir } /pnpm-lock.yaml ${ templateDir } /pnpm-lock.yaml) || ` +
648+ `(${ requiredPaths . map ( ( path ) => `test -d ${ path } ` ) . join ( " && " ) } )` ,
633649 cwd : "/workspace" ,
634650 timeoutMs : 10_000 ,
635651 } ,
@@ -642,6 +658,15 @@ async function resetTemplateAppBuilderDirectory(
642658 sandbox : ProjectSandboxStub ,
643659 dir : string ,
644660) : Promise < void > {
661+ const workspaceSlug = dir . slice ( dir . lastIndexOf ( "/" ) + 1 ) ;
662+ await executeShellExec (
663+ {
664+ command : [ "rm" , "-rf" , projectLocalRuntimeDir ( workspaceSlug ) ] ,
665+ cwd : "/workspace" ,
666+ timeoutMs : 30_000 ,
667+ } ,
668+ { sandbox } ,
669+ ) ;
645670 await executeShellExec (
646671 {
647672 command : [
@@ -673,10 +698,11 @@ async function clearBuildCache(
673698 dir : string ,
674699 mobile : boolean ,
675700) : Promise < void > {
676- const cacheDir = mobile ? ".expo" : ".next" ;
701+ const workspaceSlug = dir . slice ( dir . lastIndexOf ( "/" ) + 1 ) ;
702+ const cacheDir = mobile ? `${ dir } /.expo` : `${ projectLocalCacheDir ( workspaceSlug ) } /next` ;
677703 await executeShellExec (
678704 {
679- command : [ "rm" , "-rf" , ` ${ dir } / ${ cacheDir } ` ] ,
705+ command : [ "rm" , "-rf" , cacheDir ] ,
680706 cwd : "/workspace" ,
681707 timeoutMs : 120_000 ,
682708 } ,
0 commit comments