peachsim 0.1.919 → 0.1.921

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 (86) hide show
  1. package/cli/commands/compat.ts +51 -9
  2. package/cli/commands/daemon.ts +35 -14
  3. package/cli/commands/flow.ts +25 -3
  4. package/cli/commands/maestro.ts +10 -2
  5. package/cli/commands/report-issue.ts +32 -16
  6. package/cli/commands/skills.ts +1 -1
  7. package/cli/commands/support.ts +3 -3
  8. package/cli/main.ts +5 -0
  9. package/cli/runtime-compat.ts +127 -0
  10. package/cli/ws-bridge.ts +14 -4
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/bundle-channel.cjs +1 -1
  26. package/dist-lib/capture-contract.cjs +1 -1
  27. package/dist-lib/capture-contract.mjs +1 -1
  28. package/dist-lib/cli-constants.cjs +1 -1
  29. package/dist-lib/cloud-contract.cjs +1 -1
  30. package/dist-lib/cloud-contract.mjs +1 -1
  31. package/dist-lib/cloud-test-args.cjs +1 -1
  32. package/dist-lib/cloud-test-args.mjs +1 -1
  33. package/dist-lib/cloud.cjs +1 -1
  34. package/dist-lib/cloud.mjs +1 -1
  35. package/dist-lib/config.cjs +1 -1
  36. package/dist-lib/detox/index.cjs +11 -3
  37. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  38. package/dist-lib/expo-swift-bundle.cjs +1 -1
  39. package/dist-lib/expo-swift-metadata.cjs +1 -1
  40. package/dist-lib/expo-swift-metadata.mjs +1 -1
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +1 -1
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +11 -3
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/kotlin-host.cjs +1 -1
  52. package/dist-lib/menu.cjs +1 -1
  53. package/dist-lib/menu.mjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  56. package/dist-lib/metro-production-bundle.cjs +1 -1
  57. package/dist-lib/metro-production-bundle.mjs +1 -1
  58. package/dist-lib/metro.cjs +1 -1
  59. package/dist-lib/native-app-identity.cjs +1 -1
  60. package/dist-lib/native-app-identity.mjs +1 -1
  61. package/dist-lib/native-firebase-metadata.cjs +1 -1
  62. package/dist-lib/native-firebase-metadata.mjs +1 -1
  63. package/dist-lib/plist.cjs +1 -1
  64. package/dist-lib/plist.mjs +1 -1
  65. package/dist-lib/profiles.cjs +1 -1
  66. package/dist-lib/public-brand.cjs +1 -1
  67. package/dist-lib/react-native-host-modules.cjs +1 -1
  68. package/dist-lib/react-native-host-modules.mjs +1 -1
  69. package/dist-lib/render-mode.cjs +1 -1
  70. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  71. package/dist-lib/sdk.cjs +1 -1
  72. package/dist-lib/sdk.mjs +1 -1
  73. package/dist-lib/skills.cjs +29 -4
  74. package/dist-lib/swift-compile.cjs +1 -1
  75. package/dist-lib/swift-compile.mjs +1 -1
  76. package/dist-lib/swift-package.cjs +1 -1
  77. package/dist-lib/swift-package.mjs +1 -1
  78. package/dist-lib/swift-submit.cjs +1 -1
  79. package/dist-lib/swift-submit.mjs +1 -1
  80. package/dist-lib/swift-toolchain.cjs +1 -1
  81. package/dist-lib/swift.cjs +1 -1
  82. package/dist-lib/vite-swift.cjs +1 -1
  83. package/dist-lib/vite.cjs +1 -1
  84. package/dist-lib/worklet-executable.cjs +1 -1
  85. package/dist-lib/worklet-executable.mjs +1 -1
  86. package/package.json +1 -1
@@ -14,11 +14,13 @@ import {
14
14
  COMPAT_RESULT_CAVEAT,
15
15
  formatDetailedScanLines,
16
16
  formatScanCounts,
17
- scanDeps,
17
+ scanDepsWithData,
18
+ type CompatScanData,
18
19
  type ScanResult,
19
20
  } from '@peach/compat/web'
20
21
  import { rnxPublicBrand } from '../../src/public-brand'
21
22
  import { resolveRNXAppConfig } from '../app-config'
23
+ import { readRuntimeCompatibility } from '../runtime-compat'
22
24
  import {
23
25
  findProjectRoot,
24
26
  scanRepository,
@@ -59,14 +61,34 @@ flags:
59
61
  return
60
62
  }
61
63
 
64
+ const { config } = await resolveRNXAppConfig(appDir)
65
+ const runtime = readRuntimeCompatibility(config?.runtimeVersion)
66
+ if (!runtime.data) {
67
+ if (args.includes('--json'))
68
+ console.log(
69
+ JSON.stringify({
70
+ runtimeVersion: runtime.runtimeVersion,
71
+ overallScore: null,
72
+ error: runtime.reason,
73
+ }),
74
+ )
75
+ else console.error(` ${runtime.reason}`)
76
+ process.exitCode = 1
77
+ return
78
+ }
79
+ const data = runtime.data
80
+ const scanDeps = (manifest: ProjectManifest) => scanDepsWithData(manifest, data)
81
+ const reportRuntime = (result: ScanResult, local: LocalNative[] | null) =>
82
+ report(args, result, local, runtime.runtimeVersion)
83
+
62
84
  if (packageName) {
63
- report(args, scanOnePackage(appDir, pkg, packageName), null)
85
+ reportRuntime(scanOnePackage(appDir, pkg, packageName, runtime.data), null)
64
86
  return
65
87
  }
66
88
 
67
89
  const here = scanDeps(installedManifest(appDir, pkg))
68
90
  if (here.packages.length > 0 || appIndex >= 0) {
69
- report(args, here, await scanLocalNative(appDir, pkg))
91
+ reportRuntime(here, await scanLocalNative(appDir, pkg))
70
92
  return
71
93
  }
72
94
 
@@ -84,8 +106,9 @@ flags:
84
106
 
85
107
  if (elsewhere.length === 1) {
86
108
  const only = elsewhere[0]
87
- console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
88
- report(args, only.result, await scanLocalNative(resolve(only.dir), only.manifest))
109
+ if (!args.includes('--json'))
110
+ console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
111
+ reportRuntime(only.result, await scanLocalNative(resolve(only.dir), only.manifest))
89
112
  return
90
113
  }
91
114
  if (elsewhere.length > 1) {
@@ -96,13 +119,31 @@ flags:
96
119
  console.log()
97
120
  return
98
121
  }
99
- report(args, here, await scanLocalNative(appDir, pkg))
122
+ reportRuntime(here, await scanLocalNative(appDir, pkg))
100
123
  }
101
124
 
102
- function report(args: string[], result: ScanResult, local: LocalNative[] | null): void {
125
+ function report(
126
+ args: string[],
127
+ result: ScanResult,
128
+ local: LocalNative[] | null,
129
+ runtimeVersion: string | null,
130
+ ): void {
103
131
  if (args.includes('--json')) {
132
+ const overallScore = result.scoreSummary.assessed === 0 ? null : result.overallScore
104
133
  console.log(
105
- JSON.stringify(local ? { ...result, localNative: local } : result, null, 2),
134
+ JSON.stringify(
135
+ {
136
+ ...result,
137
+ runtimeVersion,
138
+ overallScore,
139
+ scoreSummary: { ...result.scoreSummary, compatibility: overallScore },
140
+ implementationEstimate:
141
+ result.scoreSummary.assessed === 0 ? result.overallScore : null,
142
+ ...(local ? { localNative: local } : {}),
143
+ },
144
+ null,
145
+ 2,
146
+ ),
106
147
  )
107
148
  return
108
149
  }
@@ -343,11 +384,12 @@ function scanOnePackage(
343
384
  appDir: string,
344
385
  pkg: ProjectManifest,
345
386
  packageName: string,
387
+ data: CompatScanData,
346
388
  ): ScanResult {
347
389
  const installed = installedManifest(appDir, pkg)
348
390
  const version =
349
391
  installed.dependencies?.[packageName] ??
350
392
  installed.devDependencies?.[packageName] ??
351
393
  '*'
352
- return scanDeps({ dependencies: { [packageName]: version } })
394
+ return scanDepsWithData({ dependencies: { [packageName]: version } }, data)
353
395
  }
@@ -223,7 +223,9 @@ export async function daemonInstall({ port, force }: { port: number; force: bool
223
223
  const path = macPlistPath()
224
224
  if (existsSync(path) && !force) {
225
225
  console.log(` already installed at ${path}`)
226
- console.log(` pass --force to overwrite, or use 'peach daemon restart' to reload`)
226
+ const pid = await startMacDaemon(path)
227
+ console.log(` registered (state: running, pid ${pid})`)
228
+ console.log(` pass --force to overwrite the service configuration`)
227
229
  return
228
230
  }
229
231
  await stopMacDaemonBridge(invocation)
@@ -321,8 +323,10 @@ function macServiceTarget(): string {
321
323
  return `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`
322
324
  }
323
325
 
324
- function readMacDaemonState(): { state: string; pid: number | null } | null {
325
- const result = spawnSync('launchctl', ['print', macServiceTarget()], {
326
+ function readMacDaemonState(
327
+ target = macServiceTarget(),
328
+ ): { state: string; pid: number | null } | null {
329
+ const result = spawnSync('launchctl', ['print', target], {
326
330
  encoding: 'utf8',
327
331
  })
328
332
  if (result.status !== 0) return null
@@ -358,8 +362,11 @@ function macBridgeHasListener(pid: number, port: number): boolean {
358
362
  return result.stdout.trim() === String(pid)
359
363
  }
360
364
 
361
- async function stopMacDaemonBridge(invocation: RnxSelfInvocation): Promise<void> {
362
- const service = readMacDaemonState()
365
+ export async function stopMacDaemonBridge(
366
+ invocation: RnxSelfInvocation,
367
+ target = macServiceTarget(),
368
+ ): Promise<void> {
369
+ const service = readMacDaemonState(target)
363
370
  const lock = readDaemonLockfile()
364
371
  if (isDaemonLockfileFresh(lock) && lock.pid !== service?.pid) {
365
372
  // a lockfile alone cannot authorize a signal after pid reuse.
@@ -380,7 +387,7 @@ async function stopMacDaemonBridge(invocation: RnxSelfInvocation): Promise<void>
380
387
  }
381
388
  process.kill(lock.pid, 'SIGTERM')
382
389
  }
383
- if (service) runLaunchctl(['bootout', macServiceTarget()])
390
+ if (service) runLaunchctl(['bootout', target])
384
391
  if (!lock) return
385
392
  const deadline = Date.now() + 4000
386
393
  while (Date.now() < deadline) {
@@ -390,22 +397,36 @@ async function stopMacDaemonBridge(invocation: RnxSelfInvocation): Promise<void>
390
397
  throw new Error(`bridge pid ${lock.pid} did not release port ${lock.bridgePort}`)
391
398
  }
392
399
 
393
- async function startMacDaemon(path: string): Promise<number> {
394
- if (readMacDaemonState()) {
395
- runLaunchctl(['kickstart', macServiceTarget()])
396
- } else {
397
- runLaunchctl(['bootstrap', `gui/${process.getuid!()}`, path])
398
- }
400
+ export async function startMacDaemon(
401
+ path: string,
402
+ invocation = resolveDaemonInvocation(),
403
+ target = macServiceTarget(),
404
+ ): Promise<number> {
405
+ const service = readMacDaemonState(target)
406
+ const lock = readDaemonLockfile()
407
+ if (
408
+ service?.state === 'running' &&
409
+ service.pid !== null &&
410
+ isDaemonLockfileFresh(lock) &&
411
+ lock.pid === service.pid &&
412
+ macBridgeHasListener(lock.pid, lock.bridgePort)
413
+ )
414
+ return service.pid
415
+ // reconcile an unloaded service or a bare bridge before launchd can create
416
+ // another process. start is idempotent when the service already owns the bridge.
417
+ await stopMacDaemonBridge(invocation, target)
418
+ runLaunchctl(['bootstrap', target.slice(0, target.lastIndexOf('/')), path])
399
419
  // registration and an xpcproxy pid precede the actual bridge bind.
400
420
  const deadline = Date.now() + 4000
401
421
  while (Date.now() < deadline) {
402
- const service = readMacDaemonState()
422
+ const service = readMacDaemonState(target)
403
423
  const lock = readDaemonLockfile()
404
424
  if (
405
425
  service?.state === 'running' &&
406
426
  service.pid !== null &&
407
427
  isDaemonLockfileFresh(lock) &&
408
- lock.pid === service.pid
428
+ lock.pid === service.pid &&
429
+ macBridgeHasListener(lock.pid, lock.bridgePort)
409
430
  )
410
431
  return service.pid
411
432
  await new Promise((resolve) => setTimeout(resolve, 100))
@@ -26,6 +26,8 @@ import {
26
26
  BRIDGE_VALUE_FLAGS,
27
27
  createBridge,
28
28
  createBridgeFromParsed,
29
+ DisconnectedSimError,
30
+ disconnectedSavedSimMessage,
29
31
  parseBridgeCliArgs,
30
32
  resolveBridgePortForPin,
31
33
  } from '../ws-bridge'
@@ -480,6 +482,28 @@ flow extension:
480
482
  rnxExit(1)
481
483
  }
482
484
 
485
+ const effectiveOpenInNewSim =
486
+ openInNewSim || (wantElectron && parsedBridgeArgs.simIdSource !== 'flag')
487
+ const requestedSimId = parsedBridgeArgs.simId
488
+ // use the resolved playback bridge so an explicit shell stays in its world.
489
+ // a disconnected pin is a session failure, not a failed assertion in every flow.
490
+ if (requestedSimId && !effectiveOpenInNewSim) {
491
+ const probe = createBridgeFromParsed(parsedBridgeArgs)
492
+ try {
493
+ const sims = await probe.listSims()
494
+ if (!sims.some((sim) => sim.id === requestedSimId && sim.readyState === 'open')) {
495
+ throw new DisconnectedSimError(
496
+ parsedBridgeArgs.simIdSource === 'saved'
497
+ ? disconnectedSavedSimMessage(requestedSimId)
498
+ : `sim ${requestedSimId} requested by --sim is not connected; ` +
499
+ 'run `peach list` and retry with a connected sim, or open a fresh sim',
500
+ )
501
+ }
502
+ } finally {
503
+ probe.close()
504
+ }
505
+ }
506
+
483
507
  const mode = wantElectron ? 'desktop companion' : 'bridge'
484
508
  const appLabel = targetApp ? ` | target: ${targetApp}` : ''
485
509
  console.log(`
@@ -488,8 +512,6 @@ flow extension:
488
512
  `)
489
513
 
490
514
  let openTarget = url || targetApp || ''
491
- const effectiveOpenInNewSim =
492
- openInNewSim || (wantElectron && parsedBridgeArgs.simIdSource !== 'flag')
493
515
  const driverFlag = getFlag('--driver') || ''
494
516
  let closeFlowOwnedSimAfterRun = false
495
517
  const ownedSimIds = new Set<string>()
@@ -571,7 +593,7 @@ flow extension:
571
593
  // (getStableOwnerPid -> findTopAncestor), which is the shell, agent
572
594
  // session, or runner daemon, not the run. that pid can live for days, so
573
595
  // a launched sim nobody closes stays open and keeps its flow lock.
574
- closeFlowOwnedSimAfterRun = opened?.launched === true || !!driverFlag
596
+ closeFlowOwnedSimAfterRun = opened?.launched === true
575
597
  if (closeFlowOwnedSimAfterRun && opened) ownedSimIds.add(opened.simId)
576
598
  }
577
599
 
@@ -30,6 +30,7 @@ import {
30
30
  } from '../maestro-workspace'
31
31
  import { registerRun, shouldRegisterRun, stepSummaryFromTrace } from '../run-registry'
32
32
  import { RnxExit } from '../run-rnx'
33
+ import { DisconnectedSimError } from '../ws-bridge'
33
34
  import {
34
35
  findUnexpectedFlowArgs,
35
36
  FLOW_ARG_VALUE_FLAGS,
@@ -478,7 +479,6 @@ export async function runMaestro(
478
479
  frontmatter: FlowFrontmatter
479
480
  }> = []
480
481
  for (const [flowIndex, file] of files.entries()) {
481
- console.log(`\n ▶ ${path.relative(cwd, file)}`)
482
482
  const playbackArgs = [file, ...forwardArgs]
483
483
  let flowTestDir: string | null = null
484
484
  if (testOutputRoot) {
@@ -512,8 +512,16 @@ export async function runMaestro(
512
512
  let exit = 0
513
513
  const startedAt = Date.now()
514
514
  try {
515
- exit = await runFlowPlayback(playbackArgs)
515
+ exit = await runFlowPlayback(playbackArgs, {
516
+ beforeSteps: async () => {
517
+ console.log(`\n ▶ ${path.relative(cwd, file)}`)
518
+ },
519
+ })
516
520
  } catch (err) {
521
+ if (err instanceof DisconnectedSimError) {
522
+ console.error(` error: ${err.message}`)
523
+ return 1
524
+ }
517
525
  // rnxExit is a deliberate exit that already printed its own reason.
518
526
  // reporting the sentinel's own text on top of it reads like a second,
519
527
  // unrelated failure.
@@ -3,9 +3,9 @@
3
3
  import { existsSync } from 'node:fs'
4
4
  import { resolve } from 'node:path'
5
5
  import { getCliVersion } from '../../src/cli-version'
6
- import { readActiveRuntime } from '../../src/home-paths'
7
6
  import { authHeaderValue, resolveCliAuth } from '../auth'
8
7
  import { confirm } from '../prompt'
8
+ import { readRuntimeCompatibility } from '../runtime-compat'
9
9
  import { installedManifest, readProjectManifest } from './detect'
10
10
  import {
11
11
  createProjectReport,
@@ -41,6 +41,7 @@ export type ReportIssuePayload = {
41
41
  platform: NodeJS.Platform
42
42
  arch: string
43
43
  compatibility: ScanResult | null
44
+ compatibilityUnavailable: string | null
44
45
  projectReport: ProjectReport
45
46
  }
46
47
  }
@@ -120,6 +121,7 @@ export function parseReportIssueOptions(
120
121
  export async function toolchainContext(appDir: string) {
121
122
  const manifest = readProjectManifest(appDir)
122
123
  const installed = manifest ? installedManifest(appDir, manifest) : null
124
+ const runtime = readRuntimeCompatibility()
123
125
  const safeVersions = (deps: Record<string, string> | undefined) =>
124
126
  deps &&
125
127
  Object.fromEntries(
@@ -128,17 +130,20 @@ export async function toolchainContext(appDir: string) {
128
130
  sanitizeProjectReportVersion(version),
129
131
  ]),
130
132
  )
131
- const compatibility = installed
132
- ? (await import('@peach/compat/web')).scanDeps({
133
- name: installed.name ? sanitizeProjectReportName(installed.name) : undefined,
134
- dependencies: safeVersions(installed.dependencies),
135
- devDependencies: safeVersions(installed.devDependencies),
136
- })
137
- : null
133
+ const compatibility =
134
+ installed && runtime.data
135
+ ? (await import('@peach/compat/web')).scanDepsWithData(
136
+ {
137
+ name: installed.name ? sanitizeProjectReportName(installed.name) : undefined,
138
+ dependencies: safeVersions(installed.dependencies),
139
+ devDependencies: safeVersions(installed.devDependencies),
140
+ },
141
+ runtime.data,
142
+ )
143
+ : null
138
144
  const cliVersion = sanitizeProjectReportVersion(getCliVersion())
139
- const activeRuntime = readActiveRuntime()
140
- const runtimeVersion = activeRuntime
141
- ? sanitizeProjectReportVersion(activeRuntime)
145
+ const runtimeVersion = runtime.runtimeVersion
146
+ ? sanitizeProjectReportVersion(runtime.runtimeVersion)
142
147
  : null
143
148
  return {
144
149
  activeApp:
@@ -151,6 +156,7 @@ export async function toolchainContext(appDir: string) {
151
156
  platform: process.platform,
152
157
  arch: process.arch,
153
158
  compatibility,
159
+ compatibilityUnavailable: runtime.reason,
154
160
  }
155
161
  }
156
162
 
@@ -174,7 +180,7 @@ export async function createReportIssuePayload(
174
180
 
175
181
  // where support requests go and who they come from: the login's origin and
176
182
  // bearer, and the account a `peach login --account` sign-in chose. a report
177
- // may go anonymously; an ask or a status read needs the sign-in.
183
+ // needs a sign-in so the account can read its replies later.
178
184
  export function supportEndpoint(): {
179
185
  origin: string
180
186
  headers: Record<string, string>
@@ -193,8 +199,11 @@ export function supportEndpoint(): {
193
199
  return { origin, headers, signedIn: true }
194
200
  }
195
201
 
196
- function compatibilitySummary(scan: ScanResult | null): string {
197
- if (!scan) return 'no package.json found; no compatibility scan attached'
202
+ function compatibilitySummary(
203
+ scan: ScanResult | null,
204
+ unavailable: string | null,
205
+ ): string {
206
+ if (!scan) return unavailable ?? 'no package.json found; no compatibility scan attached'
198
207
  const attention = scan.packages.filter(
199
208
  (item) => item.status !== 'full' && item.status !== 'not-relevant',
200
209
  ).length
@@ -227,6 +236,14 @@ export async function runReportIssue(
227
236
  return 1
228
237
  }
229
238
 
239
+ const endpoint = supportEndpoint()
240
+ if (!options.dryRun && !endpoint.signedIn) {
241
+ dependencies.stderr(
242
+ ' sign in first with `peach login`, or set PEACH_API_KEY. Nothing was sent.\n',
243
+ )
244
+ return 1
245
+ }
246
+
230
247
  let payload: ReportIssuePayload
231
248
  try {
232
249
  payload = await createReportIssuePayload(options, dependencies.now())
@@ -242,7 +259,7 @@ export async function runReportIssue(
242
259
  dependencies.stdout(
243
260
  `\n peach issue report preview\n\n` +
244
261
  ` description: ${payload.note}\n` +
245
- ` compatibility: ${compatibilitySummary(payload.bundle.compatibility)}\n` +
262
+ ` compatibility: ${compatibilitySummary(payload.bundle.compatibility, payload.bundle.compatibilityUnavailable)}\n` +
246
263
  ` environment: CLI ${payload.bundle.cliVersion}, runtime ${
247
264
  payload.bundle.runtimeVersion ?? 'not installed'
248
265
  }, ${payload.bundle.platform}/${payload.bundle.arch}\n\n` +
@@ -273,7 +290,6 @@ export async function runReportIssue(
273
290
  }
274
291
  }
275
292
 
276
- const endpoint = supportEndpoint()
277
293
  const headers = { ...endpoint.headers, 'content-type': 'application/json' }
278
294
 
279
295
  let response: Response
@@ -598,7 +598,7 @@ async function runInstall(args: string[]) {
598
598
  )
599
599
  rnxExit(1)
600
600
  }
601
- console.log(' done. restart the agent session to load newly installed skills.\n')
601
+ console.log(' done. skills are installed and available to your agent.\n')
602
602
  }
603
603
 
604
604
  function runShow(args: string[]) {
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // peach support ask "<question>" [--no-wait] [--wait <minutes>] [--json]
6
6
  // peach support report [--app <dir>] [--dry-run] [--yes] "<description>"
7
- // peach support status [--new] [--json]
7
+ // peach support status [--id <id>] [--new] [--json]
8
8
  // peach support fund [--amount <usd>]
9
9
 
10
10
  import { resolve } from 'node:path'
@@ -41,7 +41,7 @@ const USAGE =
41
41
  ' usage:\n' +
42
42
  ' peach support ask "<question>" [--no-wait] [--wait <minutes>] [--json]\n' +
43
43
  ' peach support report [--app <dir>] [--dry-run] [--yes] "<description>"\n' +
44
- ' peach support status [--new] [--json]\n' +
44
+ ' peach support status [--id <id>] [--new] [--json]\n' +
45
45
  ' peach support fund [--amount <usd>]\n'
46
46
 
47
47
  function signInHint(): number {
@@ -341,7 +341,7 @@ async function runStatus(args: string[]): Promise<number> {
341
341
  if (!supportEndpoint().signedIn) return signInHint()
342
342
  let items: Item[]
343
343
  try {
344
- items = await fetchItems()
344
+ items = await fetchItems(flagValue(args, '--id') ?? undefined)
345
345
  } catch (error) {
346
346
  process.stderr.write(
347
347
  ` status failed: ${error instanceof Error ? error.message : error}\n`,
package/cli/main.ts CHANGED
@@ -556,6 +556,11 @@ try {
556
556
  }
557
557
 
558
558
  case 'remote': {
559
+ if (parsed.commandArgs.length === 0) {
560
+ const { printCommandHelp } = await import('./help')
561
+ printCommandHelp('remote')
562
+ await exitWithFlush(0)
563
+ }
559
564
  if (globalSimTarget) {
560
565
  console.error(
561
566
  ` ${rnxPublicBrand.commandName} remote creates a new simulator and cannot target --sim`,
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { readActiveRuntime, runtimeDir } from '../src/home-paths'
4
+ import type { CompatScanData } from '@peach/compat/web'
5
+
6
+ type Outcomes = NonNullable<
7
+ CompatScanData['registry'][string]['versions'][number]['outcomes']
8
+ >
9
+
10
+ function record(value: unknown): value is Record<string, unknown> {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
12
+ }
13
+ function finite(value: unknown): value is number {
14
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
15
+ }
16
+ function measure(value: unknown): boolean {
17
+ return (
18
+ record(value) &&
19
+ [
20
+ 'passedWeight',
21
+ 'failedWeight',
22
+ 'unassessedWeight',
23
+ 'totalWeight',
24
+ 'assessedWeight',
25
+ 'assessed',
26
+ ].every((key) => finite(value[key])) &&
27
+ (value.compatibility === null ||
28
+ (finite(value.compatibility) && value.compatibility <= 1))
29
+ )
30
+ }
31
+ function platforms(value: unknown): boolean {
32
+ return record(value) && measure(value.ios) && measure(value.android)
33
+ }
34
+ function weightings(value: unknown): boolean {
35
+ return record(value) && platforms(value.strict) && platforms(value.usage)
36
+ }
37
+ function outcomes(value: unknown): value is Outcomes {
38
+ if (
39
+ !record(value) ||
40
+ typeof value.exactTestedVersion !== 'string' ||
41
+ typeof value.supportedVersionRange !== 'string' ||
42
+ !finite(value.unmatchedReferenceCount) ||
43
+ !platforms(value.byPlatform) ||
44
+ !weightings(value.byWeighting)
45
+ )
46
+ return false
47
+ const last = value.lastMeasured
48
+ return (
49
+ last === null ||
50
+ (record(last) &&
51
+ weightings(last.byWeighting) &&
52
+ (last.sourceCommit === null || typeof last.sourceCommit === 'string') &&
53
+ typeof last.capturedAt === 'string' &&
54
+ Array.isArray(last.receipts) &&
55
+ last.receipts.every((id) => typeof id === 'string'))
56
+ )
57
+ }
58
+ function scanData(value: unknown): value is CompatScanData {
59
+ if (
60
+ !record(value) ||
61
+ !record(value.registry) ||
62
+ !Array.isArray(value.unsupported) ||
63
+ !value.unsupported.every((name) => typeof name === 'string')
64
+ )
65
+ return false
66
+ return Object.values(value.registry).every(
67
+ (entry) =>
68
+ record(entry) &&
69
+ (entry.stubType === 'native' || entry.stubType === 'build-only') &&
70
+ Array.isArray(entry.versions) &&
71
+ entry.versions.every(
72
+ (version) =>
73
+ record(version) &&
74
+ typeof version.range === 'string' &&
75
+ typeof version.note === 'string' &&
76
+ (version.coverage === null ||
77
+ (finite(version.coverage) && version.coverage <= 1)) &&
78
+ (version.estimate === undefined ||
79
+ (finite(version.estimate) && version.estimate <= 1)) &&
80
+ (version.coverageSource === undefined ||
81
+ version.coverageSource === 'estimated' ||
82
+ (version.coverageSource === 'measured' && outcomes(version.outcomes))) &&
83
+ (version.outcomes === undefined || outcomes(version.outcomes)) &&
84
+ ['working', 'missing'].every(
85
+ (key) => version[key] === undefined || typeof version[key] === 'string',
86
+ ),
87
+ ),
88
+ )
89
+ }
90
+
91
+ // only the selected runtime can describe its compatibility. older runtimes
92
+ // without a snapshot remain explicitly unavailable rather than borrowing the CLI's data.
93
+ export function readRuntimeCompatibility(runtimeVersion = readActiveRuntime()): {
94
+ runtimeVersion: string | null
95
+ data: CompatScanData | null
96
+ reason: string | null
97
+ } {
98
+ if (!runtimeVersion)
99
+ return {
100
+ runtimeVersion,
101
+ data: null,
102
+ reason: 'no runtime selected; run `peach runtime install`',
103
+ }
104
+ try {
105
+ const snapshot: unknown = JSON.parse(
106
+ readFileSync(join(runtimeDir(runtimeVersion), 'compatibility.json'), 'utf8'),
107
+ )
108
+ if (
109
+ !record(snapshot) ||
110
+ snapshot.runtimeVersion !== runtimeVersion ||
111
+ !scanData(snapshot.data)
112
+ ) {
113
+ return {
114
+ runtimeVersion,
115
+ data: null,
116
+ reason: 'runtime compatibility snapshot is invalid or belongs to another runtime',
117
+ }
118
+ }
119
+ return { runtimeVersion, data: snapshot.data, reason: null }
120
+ } catch {
121
+ return {
122
+ runtimeVersion,
123
+ data: null,
124
+ reason: 'runtime compatibility snapshot is unavailable; run `peach upgrade`',
125
+ }
126
+ }
127
+ }
package/cli/ws-bridge.ts CHANGED
@@ -418,6 +418,15 @@ export interface BridgeSimInfo {
418
418
  meta?: Record<string, unknown>
419
419
  }
420
420
 
421
+ export class DisconnectedSimError extends Error {}
422
+
423
+ export function disconnectedSavedSimMessage(simId: string, detail = ''): string {
424
+ return (
425
+ `saved sim ${simId} is not connected${detail}; commands resume when it reconnects, ` +
426
+ 'or run `peach list` and `peach use <sim>`, or open a fresh sim'
427
+ )
428
+ }
429
+
421
430
  // a lock owner is either a friendly cli label ("active user") or a raw cli
422
431
  // identity key ("CLAUDE_CODE_SESSION_ID:<uuid>", "gppid-1234"). keep the list
423
432
  // readable by collapsing a "<source>:<value>" identity to a short form that
@@ -846,10 +855,11 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
846
855
  // is how agents end up recording the wrong app. the pin stays: a
847
856
  // page whose socket dropped reconnects under the same id, and
848
857
  // clearing the pin then would hand every later command to primary.
849
- throw new Error(
850
- `saved sim ${sentSimId} is not connected${message.slice(missingSimMessage.length)}; ` +
851
- 'commands resume when it reconnects, or run `peach list` and ' +
852
- `\`peach use <sim>\`, or open a fresh sim`,
858
+ throw new DisconnectedSimError(
859
+ disconnectedSavedSimMessage(
860
+ sentSimId,
861
+ message.slice(missingSimMessage.length),
862
+ ),
853
863
  )
854
864
  }
855
865
  throw error
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.919 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.921 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.919 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.921 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.919 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.921 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.919 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.921 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.919 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.921 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.919 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.921 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;