peachsim 0.1.627 → 0.1.629

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 (94) hide show
  1. package/LICENSE +1 -1
  2. package/cli/bin.ts +6 -2
  3. package/cli/bridge-flow-runner.ts +79 -72
  4. package/cli/commands/cleanup.ts +6 -6
  5. package/cli/commands/control.ts +4 -5
  6. package/cli/commands/detox.ts +25 -11
  7. package/cli/commands/flow.ts +3 -2
  8. package/cli/commands/inspect/core.ts +5 -0
  9. package/cli/commands/inspect.ts +43 -49
  10. package/cli/commands/mode.ts +11 -7
  11. package/cli/commands/report-issue.ts +1 -1
  12. package/cli/commands/setup.ts +16 -11
  13. package/cli/commands/show-touches-mode.ts +33 -0
  14. package/cli/commands/skills.ts +16 -0
  15. package/cli/commands/test.ts +8 -1
  16. package/cli/commands/welcome.ts +3 -3
  17. package/cli/drivers/playwright-sim-host.ts +6 -5
  18. package/cli/help-core.ts +9 -4
  19. package/cli/internal-child.ts +14 -2
  20. package/cli/levenshtein.ts +27 -0
  21. package/cli/main.ts +28 -18
  22. package/cli/ws-bridge.ts +3 -2
  23. package/detox/index.ts +234 -7
  24. package/dist-lib/agent-daemon-client.cjs +1 -1
  25. package/dist-lib/agent-events.cjs +1 -1
  26. package/dist-lib/agent-identity.cjs +1 -1
  27. package/dist-lib/agent-sessions.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +1 -1
  30. package/dist-lib/backend-origin.cjs +1 -1
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/capture-contract.cjs +1 -1
  39. package/dist-lib/capture-contract.mjs +1 -1
  40. package/dist-lib/cli-constants.cjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud-test-args.cjs +1 -1
  44. package/dist-lib/cloud-test-args.mjs +1 -1
  45. package/dist-lib/cloud.cjs +1 -1
  46. package/dist-lib/cloud.mjs +1 -1
  47. package/dist-lib/config.cjs +1 -1
  48. package/dist-lib/detox/index.cjs +170 -13
  49. package/dist-lib/dev-bundle-resolution.cjs +2 -2
  50. package/dist-lib/home-paths.cjs +1 -1
  51. package/dist-lib/host/bridge-host.cjs +57 -97
  52. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  53. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  54. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  55. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  56. package/dist-lib/host/websocket-proxy.cjs +1 -1
  57. package/dist-lib/index.cjs +44 -5
  58. package/dist-lib/jump-to-source-babel.cjs +1 -1
  59. package/dist-lib/jump-to-source-native.cjs +1 -1
  60. package/dist-lib/menu.cjs +7 -1
  61. package/dist-lib/menu.mjs +7 -1
  62. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  63. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  64. package/dist-lib/metro-production-bundle.cjs +1 -1
  65. package/dist-lib/metro-production-bundle.mjs +1 -1
  66. package/dist-lib/metro.cjs +43 -4
  67. package/dist-lib/profiles.cjs +1 -1
  68. package/dist-lib/public-brand.cjs +1 -1
  69. package/dist-lib/react-native-host-modules.cjs +1 -1
  70. package/dist-lib/react-native-host-modules.mjs +1 -1
  71. package/dist-lib/render-mode.cjs +1 -1
  72. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  73. package/dist-lib/sdk.cjs +1 -1
  74. package/dist-lib/sdk.mjs +1 -1
  75. package/dist-lib/skills.cjs +159 -210
  76. package/dist-lib/swift-compile.cjs +1 -1
  77. package/dist-lib/swift-compile.mjs +1 -1
  78. package/dist-lib/swift-package.cjs +1 -1
  79. package/dist-lib/swift-package.mjs +1 -1
  80. package/dist-lib/swift-submit.cjs +1 -1
  81. package/dist-lib/swift-submit.mjs +1 -1
  82. package/dist-lib/swift.cjs +7276 -7131
  83. package/dist-lib/vite-kotlin.cjs +1 -1
  84. package/dist-lib/vite-swift.cjs +1 -1
  85. package/dist-lib/vite.cjs +43 -4
  86. package/package.json +1 -1
  87. package/skills/contrast/SKILL.md +1 -1
  88. package/src/dev-bundle-resolution.ts +1 -1
  89. package/src/host/bridge-host.ts +2 -34
  90. package/src/menu.ts +7 -0
  91. package/src/native-seam-manifest.ts +4 -16
  92. package/src/runtime-assets.ts +59 -2
  93. package/src/screenshots/frame-compose.ts +4 -1
  94. package/src/vite-plugin.ts +723 -25
@@ -54,6 +54,7 @@ interface SetupOptions {
54
54
 
55
55
  interface SetupRunOptions {
56
56
  launchConnectRN?: boolean
57
+ port?: number
57
58
  }
58
59
 
59
60
  export interface SetupResult {
@@ -120,6 +121,7 @@ export async function runSetup(
120
121
  channel: 'stable',
121
122
  setActive: true,
122
123
  protectVersions: activeRuntime ? [activeRuntime] : [],
124
+ quiet: !options.json,
123
125
  })
124
126
  const result = options.json
125
127
  ? await install()
@@ -130,10 +132,16 @@ export async function runSetup(
130
132
  let serviceInstalled = serviceStatus.installed
131
133
  if (service && !serviceStatus.installed && !options.dryRun) {
132
134
  if (options.json) {
133
- await daemonInstall({ port: DEFAULT_SOOTSIM_BRIDGE_PORT, force: false })
135
+ await daemonInstall({
136
+ port: runOptions.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT,
137
+ force: false,
138
+ })
134
139
  } else {
135
140
  console.log(`\n ${accent('◆')} ${bold('Enabling the background service')}`)
136
- await daemonInstall({ port: DEFAULT_SOOTSIM_BRIDGE_PORT, force: false })
141
+ await daemonInstall({
142
+ port: runOptions.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT,
143
+ force: false,
144
+ })
137
145
  }
138
146
  serviceInstalled = true
139
147
  }
@@ -144,13 +152,10 @@ export async function runSetup(
144
152
 
145
153
  if (launchConnectRN) {
146
154
  console.log(`\n ${accent('◆')} Opening ConnectRN`)
147
- await runOpenCommand([
148
- 'ConnectRN',
149
- '--driver',
150
- simulatorDriver,
151
- '--no-describe',
152
- '--quiet',
153
- ])
155
+ await runOpenCommand(
156
+ ['ConnectRN', '--driver', simulatorDriver, '--no-describe', '--quiet'],
157
+ { port: runOptions.port },
158
+ )
154
159
  }
155
160
 
156
161
  const productAnalytics = await decidePrivacy(options, privacy)
@@ -324,7 +329,7 @@ async function decideSimulatorDriver(
324
329
  desktopDescription,
325
330
  'The Desktop app opens in a dedicated native window.',
326
331
  ])
327
- const choice = await select('Where should peach open??', [
332
+ const choice = await select('Where should peach open?', [
328
333
  'Browser (recommended) No additional download',
329
334
  desktopChoice,
330
335
  ])
@@ -343,7 +348,7 @@ async function decidePrivacy(
343
348
  'Reports can contain package compatibility or an error class.',
344
349
  'They never contain your code or app data.',
345
350
  ])
346
- return confirm('Share bounded diagnostics to improve peach??', false)
351
+ return confirm('Share bounded diagnostics to improve peach?', false)
347
352
  }
348
353
 
349
354
  async function decideRepository(
@@ -0,0 +1,33 @@
1
+ // peach mode show-touches — toggle OS touch indicators (Show Single Touches / Show taps)
2
+ // in the running peach sim.
3
+
4
+ import { rnxExit } from '../run-rnx'
5
+ import { runShellBooleanMode, type ShellBooleanModeOptions } from './shell-boolean-mode'
6
+
7
+ export async function runShowTouchesMode(args: string[], opts: ShellBooleanModeOptions) {
8
+ if (args.includes('--help') || args.includes('-h')) {
9
+ console.log(`
10
+ peach mode show-touches — toggle touch indicator overlays
11
+
12
+ usage:
13
+ peach mode show-touches [on|off|toggle]
14
+
15
+ arguments:
16
+ on enable touch indicators
17
+ off disable touch indicators
18
+ toggle flip the current state (default)
19
+
20
+ examples:
21
+ peach mode show-touches
22
+ peach mode show-touches on
23
+ peach mode show-touches off
24
+ `)
25
+ rnxExit(0)
26
+ }
27
+
28
+ await runShellBooleanMode(args, opts, {
29
+ modeKey: 'showTouches',
30
+ displayName: 'show-touches',
31
+ actionId: 'toggle-touches',
32
+ })
33
+ }
@@ -9,10 +9,26 @@ import * as fs from 'fs'
9
9
  import * as os from 'os'
10
10
  import * as path from 'path'
11
11
  import { fileURLToPath } from 'url'
12
+ import { getCliVersion } from '../../src/cli-version'
13
+ import { rnxHomeDir } from '../../src/home-paths'
12
14
  import { rnxPublicBrand } from '../../src/public-brand'
13
15
  import { rnxExit } from '../run-rnx'
14
16
 
17
+ declare const __PEACH_BUNDLED_SKILLS__: Record<string, string>
18
+
15
19
  function getSkillsDir(): string {
20
+ // the standalone binary carries its skills instead of a package directory;
21
+ // write them once per cli version so installs can link to real files.
22
+ if (typeof __PEACH_BUNDLED_SKILLS__ !== 'undefined') {
23
+ const dir = path.join(rnxHomeDir(), 'skills', getCliVersion())
24
+ for (const [file, body] of Object.entries(__PEACH_BUNDLED_SKILLS__)) {
25
+ const target = path.join(dir, file)
26
+ if (fs.existsSync(target)) continue
27
+ fs.mkdirSync(path.dirname(target), { recursive: true })
28
+ fs.writeFileSync(target, body)
29
+ }
30
+ return dir
31
+ }
16
32
  try {
17
33
  const pkgPath = fileURLToPath(
18
34
  import.meta.resolve(`${rnxPublicBrand.packageName}/package.json`),
@@ -984,7 +984,14 @@ export async function runTest(
984
984
  return runCloudTest(args, opts)
985
985
  }
986
986
 
987
- const selected = resolveTestCommand(args)
987
+ // a missing or unknown target is the caller's mistake: one line, no stack.
988
+ let selected: ReturnType<typeof resolveTestCommand>
989
+ try {
990
+ selected = resolveTestCommand(args)
991
+ } catch (error) {
992
+ console.error(` error: ${error instanceof Error ? error.message : String(error)}`)
993
+ return 1
994
+ }
988
995
  const app = appFromArgs(args)
989
996
  const appTarget = app
990
997
  ? await resolveTestAppTarget(app)
@@ -4,14 +4,14 @@ import { printBrand } from '../ui'
4
4
  import { runOpenCommand } from './control'
5
5
  import { runSetup } from './setup'
6
6
 
7
- export async function runWelcome(): Promise<number> {
7
+ export async function runWelcome(port: number | undefined): Promise<number> {
8
8
  if (!isOnboardingComplete() && canPromptForPrivacyChoice()) {
9
- await runSetup([], { launchConnectRN: true })
9
+ await runSetup([], { launchConnectRN: true, port })
10
10
  return 0
11
11
  }
12
12
 
13
13
  printBrand('Run any React Native app in a local simulator.')
14
14
  console.log(`\n Opening ConnectRN`)
15
- await runOpenCommand(['ConnectRN', '--no-describe', '--quiet'])
15
+ await runOpenCommand(['ConnectRN', '--no-describe', '--quiet'], { port })
16
16
  return 0
17
17
  }
@@ -446,16 +446,17 @@ const cleanupProfile = () => {
446
446
  const request = readBrowserCompositeCaptureRequest(value);
447
447
  const session = await source.page.context().newCDPSession(source.page);
448
448
  try {
449
+ const { clip, originX, originY } = resolveBrowserCompositeCdpClip(
450
+ request,
451
+ await session.send('Page.getLayoutMetrics'),
452
+ );
449
453
  const result = await session.send('Page.captureScreenshot', {
450
454
  format: 'png',
451
455
  fromSurface: true,
452
456
  captureBeyondViewport: true,
453
- clip: resolveBrowserCompositeCdpClip(
454
- request,
455
- await session.send('Page.getLayoutMetrics'),
456
- ),
457
+ clip,
457
458
  });
458
- return 'data:image/png;base64,' + result.data;
459
+ return { dataUrl: 'data:image/png;base64,' + result.data, originX, originY };
459
460
  } finally {
460
461
  await session.detach();
461
462
  }
package/cli/help-core.ts CHANGED
@@ -96,8 +96,13 @@ Run \`peach <command> --help\` for leaf availability and usage.`
96
96
  export function renderRnxHelp(options: RnxHelpOptions): string {
97
97
  const command = options.command ?? null
98
98
  const commandArgs = options.commandArgs ?? []
99
+ // the preview selectors and backend lists describe the factory's browser
100
+ // backend; the public cli runs the node backend and never sees them.
101
+ const factory = options.backend === 'browser'
99
102
  if (!command) {
100
- return `${renderCliIndexHelp(HELP_CONTEXT, formatFlagHelp()).trimEnd()}
103
+ const index = renderCliIndexHelp(HELP_CONTEXT, formatFlagHelp()).trimEnd()
104
+ if (!factory) return index
105
+ return `${index}
101
106
 
102
107
  browser preview selectors:
103
108
  peach web select the existing Factory web preview
@@ -108,7 +113,7 @@ ${renderRootAvailability(options.backend)}`
108
113
  }
109
114
 
110
115
  let body: string | null
111
- if (command === 'web' || command === 'ios' || command === 'android') {
116
+ if (factory && (command === 'web' || command === 'ios' || command === 'android')) {
112
117
  body = renderPreviewHelp(command)
113
118
  } else if (options.group === command && !options.prefer) {
114
119
  body = renderCliGroupHelp(command, HELP_CONTEXT, formatFlagHelp())
@@ -130,11 +135,11 @@ ${renderRootAvailability(options.backend)}`
130
135
  const registration = getRnxCommandRegistration(command)
131
136
  if (!registration) return body
132
137
  const current = getRnxCommandAvailability(command, commandArgs, options.backend)
138
+ if (!factory && current.available) return body
133
139
  const currentLine = current.available
134
140
  ? `${options.backend} backend: available`
135
141
  : `${options.backend} backend: ${current.reason}`
136
- const factoryLeaves =
137
- options.backend === 'browser' ? renderFactoryBrowserLeaves(command) : null
142
+ const factoryLeaves = factory ? renderFactoryBrowserLeaves(command) : null
138
143
  return `${body.trimEnd()}
139
144
 
140
145
  ${factoryLeaves ? `${factoryLeaves}\n\n` : ''}${backendLabel(command, commandArgs)}
@@ -18,6 +18,12 @@ import * as vm from 'node:vm'
18
18
 
19
19
  export const RNX_INTERNAL_COMMAND = '__internal'
20
20
 
21
+ // playwright's installer forks its downloader script with process.execPath,
22
+ // which here is this binary. the installer child sets this for its whole
23
+ // process tree, and bin.ts then runs a forked `.js` argument as a script
24
+ // instead of reading it as a peach command.
25
+ export const RNX_FORKED_SCRIPT_ENV = 'PEACH_RUNS_FORKED_SCRIPTS'
26
+
21
27
  export const RNX_INTERNAL_CHILDREN = {
22
28
  playwrightProbe: 'playwright-probe',
23
29
  playwrightInstall: 'playwright-install',
@@ -113,8 +119,14 @@ function playwrightInstall(args: string[]): void {
113
119
  if (!cliPath) {
114
120
  throw new Error('internal playwright-install needs the package CLI path')
115
121
  }
116
- process.argv = [process.argv[0], cliPath, ...cliArgs]
117
- requireFromCwd()(cliPath)
122
+ process.env[RNX_FORKED_SCRIPT_ENV] = '1'
123
+ runScript(cliPath, cliArgs)
124
+ }
125
+
126
+ /** run a javascript file as node would, with argv shaped for its own parser. */
127
+ export function runScript(scriptPath: string, args: string[]): void {
128
+ process.argv = [process.argv[0], scriptPath, ...args]
129
+ createRequire(scriptPath)(scriptPath)
118
130
  }
119
131
 
120
132
  /** the detached browser host. its source is CJS that resolves playwright from
@@ -0,0 +1,27 @@
1
+ export function levenshtein(a: string, b: string): number {
2
+ if (a === b) return 0
3
+ if (!a.length) return b.length
4
+ if (!b.length) return a.length
5
+ // standard two-row dp: prev[] is the previous row, cur[] is the row being
6
+ // computed; swap pointers per outer iteration. the earlier in-place rolling
7
+ // version overwrote prev[j-1] twice per cell and never set prev[0]=i, so
8
+ // distance(`kitten`,`sitting`) returned 8 instead of 3 and the fuzzy-suggest
9
+ // ranking was effectively random.
10
+ let prev = new Array<number>(b.length + 1)
11
+ let cur = new Array<number>(b.length + 1)
12
+ for (let j = 0; j <= b.length; j++) prev[j] = j
13
+ for (let i = 1; i <= a.length; i++) {
14
+ cur[0] = i
15
+ for (let j = 1; j <= b.length; j++) {
16
+ cur[j] = Math.min(
17
+ prev[j] + 1,
18
+ cur[j - 1] + 1,
19
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
20
+ )
21
+ }
22
+ const tmp = prev
23
+ prev = cur
24
+ cur = tmp
25
+ }
26
+ return prev[b.length]
27
+ }
package/cli/main.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  // when none of its hidden entry points claimed the process.
4
4
 
5
5
  import { rnxPublicBrand } from '../src/public-brand'
6
- import { parseArgs, TOP_LEVEL_RUNTIME_COMMANDS } from './parse-args'
6
+ import { levenshtein } from './levenshtein'
7
+ import { COMMANDS, parseArgs, TOP_LEVEL_RUNTIME_COMMANDS } from './parse-args'
7
8
  import { RnxExit } from './run-rnx'
8
9
  import { rnxSelfInvocation } from './self-invocation'
9
10
  import { IS_STANDALONE } from './standalone'
@@ -37,18 +38,29 @@ function handleTopLevelError(err: unknown): never {
37
38
  msg.startsWith('peach bridge exited before becoming ready') ||
38
39
  msg.startsWith(`${rnxPublicBrand.commandName} open: requested bridge port`) ||
39
40
  msg.startsWith('background daemon setup is unavailable') ||
40
- msg.startsWith('peach bridge did not start within')
41
+ msg.startsWith('peach bridge did not start within') ||
42
+ msg.startsWith('could not resolve a native bundle for ')
41
43
  if (isKnownCliFailure && !process.env.SOOTSIM_VERBOSE) {
42
44
  process.stderr.write(` ${msg}\n`)
43
45
  process.exit(1)
44
46
  }
45
- if (err instanceof Error && err.stack) {
46
- process.stderr.write(`${err.stack}\n`)
47
- } else {
48
- process.stderr.write(`${msg}\n`)
49
- }
47
+ // bun can render an async-thrown error's stack with a bare `Error` header, so
48
+ // the message always goes first.
49
+ process.stderr.write(` ${msg}\n`)
50
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}\n`)
50
51
  process.exit(1)
51
52
  }
53
+ function printUnknownCommand(attempted: string) {
54
+ const closest = [...COMMANDS]
55
+ .map((name) => ({ name, distance: levenshtein(attempted, name) }))
56
+ .sort((a, b) => a.distance - b.distance)[0]
57
+ console.error(` unknown command: ${attempted}`)
58
+ if (closest && closest.distance <= 2) {
59
+ console.error(` did you mean \`${rnxPublicBrand.commandName} ${closest.name}\`?`)
60
+ }
61
+ console.error(` run \`${rnxPublicBrand.commandName} --help\` to see the full surface.`)
62
+ }
63
+
52
64
  process.on('unhandledRejection', handleTopLevelError)
53
65
  process.on('uncaughtException', handleTopLevelError)
54
66
 
@@ -73,7 +85,11 @@ const isCliUpdateWorker =
73
85
  parsed.commandArgs[0] === '--check-worker'
74
86
  const isBareWelcome = !parsed.command && parsed.commandArgs.length === 0
75
87
  const isReadOnlyInvocation =
76
- parsed.version || parsed.help || parsed.commandArgs.includes('--dry-run')
88
+ parsed.version ||
89
+ parsed.help ||
90
+ parsed.commandArgs.includes('--help') ||
91
+ parsed.commandArgs.includes('-h') ||
92
+ parsed.commandArgs.includes('--dry-run')
77
93
 
78
94
  if (isCliUpdateWorker) {
79
95
  if (IS_STANDALONE) {
@@ -264,7 +280,7 @@ if (device || theme) {
264
280
  // returning runs open ConnectRN directly; app discovery belongs inside it.
265
281
  if (!parsed.command && parsed.commandArgs.length === 0) {
266
282
  const { runWelcome } = await import('./commands/welcome')
267
- await exitWithFlush(await runWelcome())
283
+ await exitWithFlush(await runWelcome(port))
268
284
  }
269
285
 
270
286
  // route to command. an empty command at this point only happens if the
@@ -280,10 +296,7 @@ if (!command) {
280
296
  if (parsed.commandArgs.length > 0) {
281
297
  const attempted =
282
298
  parsed.commandArgs.find((a) => !a.startsWith('-')) ?? parsed.commandArgs[0]
283
- console.error(` unknown command: ${attempted}`)
284
- console.error(
285
- ` run \`${rnxPublicBrand.commandName} --help\` to see the full surface.`,
286
- )
299
+ printUnknownCommand(attempted)
287
300
  await exitWithFlush(1)
288
301
  }
289
302
  const { printHelp } = await import('./help')
@@ -608,7 +621,7 @@ try {
608
621
 
609
622
  case 'setup': {
610
623
  const { runSetup } = await import('./commands/setup')
611
- await runSetup(parsed.commandArgs)
624
+ await runSetup(parsed.commandArgs, { port })
612
625
  break
613
626
  }
614
627
 
@@ -685,10 +698,7 @@ try {
685
698
  }
686
699
 
687
700
  default: {
688
- console.error(` unknown command: ${command}`)
689
- console.error(
690
- ` run \`${rnxPublicBrand.commandName} --help\` to see the full surface.`,
691
- )
701
+ printUnknownCommand(command)
692
702
  await exitWithFlush(1)
693
703
  }
694
704
  }
package/cli/ws-bridge.ts CHANGED
@@ -145,9 +145,10 @@ function resolveDefaultDevBridgeLockfile(): DevBridgeLockfile | null {
145
145
  }
146
146
  }
147
147
 
148
+ // another checkout's bridge on its own offset belongs to that checkout's
149
+ // world; only the canonical bridge is shared, and past it the daemon serves.
148
150
  return (
149
- lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ??
150
- lockfiles[0]
151
+ lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? null
151
152
  )
152
153
  }
153
154