peachsim 0.1.1620 → 0.1.1635

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.
Files changed (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
@@ -46,8 +46,8 @@ import {
46
46
  RNX_METRO_MODULE_IDENTITY_VERSION,
47
47
  type RNXMetroModuleIdentityMetadata,
48
48
  } from '../src/metro-production-bundle.ts'
49
+ import { readPackageVersionsFooter } from '../src/packageVersionsContract'
49
50
  import { rnxPublicBrand } from '../src/public-brand.ts'
50
- import { PEACH_SIMULATOR_PATH } from '../src/simulator-route'
51
51
  import { CHOOSE_ACCOUNT_HINT, resolveCliAuth } from './auth.ts'
52
52
  import {
53
53
  createCloudSession,
@@ -274,6 +274,26 @@ async function loadBundleSource(bundleInput: string, command: string): Promise<s
274
274
  return source
275
275
  }
276
276
 
277
+ // a metro bundle carries react native's Platform module for exactly the
278
+ // platform it was built for, and that module's OS constant survives
279
+ // minification. a bundle naming only the other platform would boot against
280
+ // native modules it was never built for, so it refuses before a host exists.
281
+ export function assertBundleBuiltFor(
282
+ bytes: Buffer,
283
+ platform: RemotePlatform,
284
+ command: string,
285
+ ): void {
286
+ const source = bytes.toString('latin1')
287
+ const ios = /\bOS:\s*["']ios["']/.test(source)
288
+ const android = /\bOS:\s*["']android["']/.test(source)
289
+ if (ios === android) return
290
+ const built = ios ? 'ios' : 'android'
291
+ if (built === platform) return
292
+ throw new Error(
293
+ `${command} needs a bundle built for ${platform}; this one was built for ${built}. Rebuild with \`react-native bundle --platform ${platform}\` or run \`peach ${built} --remote\`.`,
294
+ )
295
+ }
296
+
277
297
  // `react-native bundle --assets-dest` dest paths are not always the JS
278
298
  // descriptor's httpServerLocation with `../` replaced by `_`. Metro 0.83 does
279
299
  // that (`__node_modules`); Metro 0.84 often dests `node_modules` and keeps the
@@ -746,6 +766,11 @@ export async function produceCloudBoxArtifact(
746
766
  assetsDirectory ? 'require-assets' : 'allow-placeholders',
747
767
  assetsDirectory,
748
768
  )
769
+ if (!readPackageVersionsFooter(prepared.source)?.['react-native']) {
770
+ throw new Error(
771
+ 'remote simulator bundle is missing __peachPackageVersions with the installed react-native version. Wrap your project Metro config with withPeach from peachsim/metro, then rebuild the bundle before running peach ios --remote or peach android --remote.',
772
+ )
773
+ }
749
774
  const warnings: string[] = []
750
775
  if (prepared.search.unconfirmed && prepared.dest) {
751
776
  warnings.push(
@@ -1662,39 +1687,54 @@ export class BoxBootFailedError extends Error {
1662
1687
  }
1663
1688
  }
1664
1689
 
1665
- export interface BoxDisplayGrant {
1666
- url: string
1667
- authToken: string
1668
- expectedPeer: unknown
1669
- }
1670
-
1671
1690
  // how long `peach remote` waits for a fresh box's page to attach its simulator
1672
1691
  // before giving up on the watch url. the page boots the bundle after the
1673
1692
  // browser is up, and a large bundle takes a while to parse.
1674
1693
  const BOX_DISPLAY_GRANT_WAIT_MS = 180_000
1675
1694
  const BOX_DISPLAY_GRANT_RETRY_MS = 2_000
1676
1695
 
1677
- function readBoxDisplayGrant(value: unknown): BoxDisplayGrant {
1678
- if (!isRecord(value) || !isRecord(value.grant)) {
1679
- throw new Error('box display-grant returned an invalid grant')
1680
- }
1681
- const grant = value.grant
1682
- if (
1683
- typeof grant.url !== 'string' ||
1684
- !grant.url ||
1685
- typeof grant.authToken !== 'string' ||
1686
- !grant.authToken ||
1687
- !isRecord(grant.expectedPeer)
1688
- ) {
1689
- throw new Error('box display-grant returned an invalid grant')
1696
+ // waits for a fresh box's page to load its app or report that the app threw
1697
+ // while booting, reading the same runtime receipts the page records. a boot
1698
+ // error throws; a slow boot that reports neither within the wait is not an
1699
+ // error, so the caller still gets its watch url.
1700
+ export async function awaitBoxBoot(options: {
1701
+ apiOrigin: string
1702
+ boxId: string
1703
+ boxToken: string
1704
+ }): Promise<void> {
1705
+ const started = Date.now()
1706
+ while (Date.now() - started < BOX_DISPLAY_GRANT_WAIT_MS) {
1707
+ const response = await fetch(
1708
+ `${options.apiOrigin}/v1/boxes/${encodeURIComponent(options.boxId)}/lifecycle`,
1709
+ { headers: { authorization: `Bearer ${options.boxToken}` } },
1710
+ )
1711
+ const text = await response.text()
1712
+ if (!response.ok) throw responseError(response.status, text)
1713
+ const status: unknown = JSON.parse(text)
1714
+ const generation =
1715
+ isRecord(status) && isRecord(status.browser)
1716
+ ? status.browser.browserGeneration
1717
+ : null
1718
+ const receipts =
1719
+ isRecord(status) && Array.isArray(status.runtimeReceipts)
1720
+ ? status.runtimeReceipts
1721
+ : []
1722
+ for (const receipt of receipts.toReversed()) {
1723
+ if (!isRecord(receipt) || receipt.browserGeneration !== generation) continue
1724
+ if (receipt.kind === 'boot-failed' && typeof receipt.error === 'string') {
1725
+ throw new BoxBootFailedError(options.boxId, receipt.error)
1726
+ }
1727
+ if (receipt.kind === 'loaded' || receipt.kind === 'painted') return
1728
+ }
1729
+ await new Promise((resolve) => setTimeout(resolve, BOX_DISPLAY_GRANT_RETRY_MS))
1690
1730
  }
1691
- return { url: grant.url, authToken: grant.authToken, expectedPeer: grant.expectedPeer }
1692
1731
  }
1693
1732
 
1694
- // mints a watch-only display grant from the box and builds the bare shell
1695
- // watch url for it: the same /simulator#remoteEngine fragment the nano service
1696
- // mints, pointed at the box's display socket instead. the box token stays in
1697
- // this process; only the grant travels in the fragment.
1733
+ // mints a watch-only display grant from the box and returns the watch url the
1734
+ // box service built for it. the service puts it on the deploy that serves the
1735
+ // box page, so the viewer runs the same engine build, and display wire, as the
1736
+ // page hosting the simulator. the box token stays in this process; only the
1737
+ // grant travels, in the url's fragment.
1698
1738
  export async function requestBoxWatchUrl(options: {
1699
1739
  apiOrigin: string
1700
1740
  boxId: string
@@ -1722,17 +1762,6 @@ export async function requestBoxWatchUrl(options: {
1722
1762
  }`,
1723
1763
  )
1724
1764
  }
1725
- if (response.status === 422) {
1726
- const text = await response.text()
1727
- let failure: unknown = null
1728
- try {
1729
- failure = JSON.parse(text)
1730
- } catch {}
1731
- if (isRecord(failure) && failure.code === 'boot-failed') {
1732
- throw new BoxBootFailedError(options.boxId, String(failure.error))
1733
- }
1734
- throw responseError(response.status, text)
1735
- }
1736
1765
  if (response.status === 409) {
1737
1766
  // the page has not attached its simulator yet. this is the normal state
1738
1767
  // right after a create, so wait rather than refusing.
@@ -1752,27 +1781,9 @@ export async function requestBoxWatchUrl(options: {
1752
1781
  } catch {
1753
1782
  throw new Error('box display-grant returned unreadable JSON')
1754
1783
  }
1755
- const grant = readBoxDisplayGrant(parsed)
1756
- const watch = new URL(PEACH_SIMULATOR_PATH, rnxPublicBrand.origin)
1757
- // the shell boots its stage at its own default device, while the box page
1758
- // hosts its simulator at the grant's device profile. without ?device= the
1759
- // tenant refuses the host hello on dimensions and paints nothing — the
1760
- // same param SimPage and BoxHostPage already set on their stage urls.
1761
- if (
1762
- isRecord(grant.expectedPeer) &&
1763
- typeof grant.expectedPeer.deviceProfile === 'string' &&
1764
- grant.expectedPeer.deviceProfile
1765
- ) {
1766
- watch.searchParams.set('device', grant.expectedPeer.deviceProfile)
1767
- }
1768
- watch.hash = `remoteEngine=${encodeURIComponent(
1769
- JSON.stringify({
1770
- url: grant.url,
1771
- authToken: grant.authToken,
1772
- expectedPeer: grant.expectedPeer,
1773
- }),
1774
- )}`
1775
- return watch.toString()
1784
+ if (!isRecord(parsed) || typeof parsed.watchUrl !== 'string' || !parsed.watchUrl)
1785
+ throw new Error('box display-grant returned no watch url')
1786
+ return parsed.watchUrl
1776
1787
  }
1777
1788
  }
1778
1789
 
@@ -128,6 +128,7 @@ const VALUE_FLAGS = new Set([
128
128
  '--timeout',
129
129
  '--device',
130
130
  '--posture',
131
+ '--render-resolution',
131
132
  '--os-version',
132
133
  '--ios-target',
133
134
  '--output',
@@ -34,6 +34,7 @@
34
34
  import { spawn } from 'node:child_process'
35
35
  import { defineCommand, type CustomCommand } from '@peach/box/shell'
36
36
  import { PROJECT_MOUNT } from '@peach/box/shell'
37
+ import { terminateProcessGroup } from '../../../src/process-group'
37
38
 
38
39
  // generous next to a 414ms p95: this bounds a wedged child, it does not cut off
39
40
  // a large search.
@@ -63,9 +64,9 @@ function runGitGrep(args: string[], cwd: string) {
63
64
  clearTimeout(timer)
64
65
  resolve({ stdout, stderr, exitCode })
65
66
  }
66
- const timer = setTimeout(() => {
67
+ const timer = setTimeout(async () => {
67
68
  try {
68
- child.kill('SIGKILL')
69
+ await terminateProcessGroup(child, 'SIGKILL')
69
70
  } catch {}
70
71
  stderr += `rg: search timed out after ${SEARCH_DEADLINE_MS}ms\n`
71
72
  finish(2)
@@ -52,9 +52,6 @@ interface BoxCommandOptions {
52
52
  verbose?: boolean
53
53
  }
54
54
 
55
- // no default. a cloud box service has to be deployed before one can exist, and
56
- // defaulting to an origin that answers nothing would advertise a capability
57
- // that always fails.
58
55
  const ENDPOINT_ENV = 'PEACH_BOX_ENDPOINT'
59
56
  const API_ORIGIN_ENV = 'PEACH_API_ORIGIN'
60
57
  const BOX_TEMPLATES = new BoxTemplateRegistry([
@@ -212,23 +209,16 @@ async function runCloudBoxUse(args: string[]): Promise<number> {
212
209
  }
213
210
  }
214
211
 
215
- function requireEndpoint(args: string[], label: string): string | null {
216
- return cloudBoxEndpoint(args, `box ${label}`)
217
- }
218
-
219
212
  /**
220
- * the cloud box service this command talks to, or null with the reason
221
- * printed. exported because `peach box send` creates a box too, and a second
222
- * spelling of where boxes live is a second place to configure.
213
+ * the cloud box service for every CLI command that selects a service. existing
214
+ * box references carry their own endpoint from the account service.
223
215
  */
224
- export function cloudBoxEndpoint(args: string[], command: string): string | null {
225
- const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
226
- if (endpoint) return endpoint
227
- console.error(
228
- ` ${rnxPublicBrand.commandName} ${command}: no cloud box service to talk to.\n` +
229
- ` set ${ENDPOINT_ENV} or pass --endpoint=<url>.`,
216
+ export function cloudBoxEndpoint(args: string[]): string {
217
+ return (
218
+ flag(args, 'endpoint')?.trim() ||
219
+ process.env[ENDPOINT_ENV]?.trim() ||
220
+ rnxPublicBrand.boxOrigin
230
221
  )
231
- return null
232
222
  }
233
223
 
234
224
  /** the api origin a cloud box bills against. */
@@ -278,8 +268,7 @@ async function runBoxSend(args: string[], opts: BoxCommandOptions): Promise<numb
278
268
  async function runCloudBoxStop(args: string[], purge: boolean): Promise<number> {
279
269
  const peach = rnxPublicBrand.commandName
280
270
  const label = purge ? 'delete' : 'stop'
281
- const endpoint = requireEndpoint(args, label)
282
- if (!endpoint) return 1
271
+ const endpoint = cloudBoxEndpoint(args)
283
272
  const name = boxTarget(args, label)
284
273
  if (!name) return 1
285
274
  const { auth, header } = authHeaderOrExit(`box ${label}`)
@@ -321,7 +310,7 @@ async function runCloudBoxList(args: string[]): Promise<number> {
321
310
  const accountId = cloudAccountIdOrExit(auth)
322
311
  const client = createBoxClient(
323
312
  createCloudBoxProvider({
324
- endpoint: flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim() ?? '',
313
+ endpoint: cloudBoxEndpoint(args),
325
314
  authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
326
315
  }),
327
316
  )
@@ -589,15 +578,7 @@ async function runCloudBoxCreate(
589
578
  ): Promise<number> {
590
579
  const peach = rnxPublicBrand.commandName
591
580
  const json = args.includes('--json')
592
- const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
593
- if (!endpoint) {
594
- console.error(
595
- ` ${peach} box create: no cloud box service to create a box in.\n` +
596
- ` set ${ENDPOINT_ENV} or pass --endpoint=<url>. \`${peach} box\` runs the\n` +
597
- ` same shell locally over this checkout.`,
598
- )
599
- return 1
600
- }
581
+ const endpoint = cloudBoxEndpoint(args)
601
582
 
602
583
  // --bundle IS the template choice: the box runs an app that is already
603
584
  // built, so naming a second template would ask for two different boxes.