peachsim 0.1.1594 → 0.1.1619
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.
- package/README.md +34 -11
- package/cli/app-catalog.ts +7 -5
- package/cli/app-config.ts +6 -2
- package/cli/app-fonts.ts +74 -35
- package/cli/app-stubs.ts +173 -42
- package/cli/bin.ts +9 -0
- package/cli/bridge-flow-runner.ts +189 -84
- package/cli/cloud-client.ts +26 -25
- package/cli/commands/agent-indicator-mode.ts +39 -0
- package/cli/commands/box.ts +13 -3
- package/cli/commands/control.ts +132 -108
- package/cli/commands/daemon.ts +99 -24
- package/cli/commands/debug.ts +29 -1
- package/cli/commands/detect.ts +1 -1
- package/cli/commands/device.ts +6 -8
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/doctor.ts +97 -34
- package/cli/commands/flow.ts +22 -12
- package/cli/commands/inspect/actions.ts +2 -0
- package/cli/commands/inspect/core.ts +106 -40
- package/cli/commands/inspect/list.ts +9 -3
- package/cli/commands/inspect/memory.ts +9 -0
- package/cli/commands/inspect/settling.ts +167 -49
- package/cli/commands/inspect/shared.ts +53 -12
- package/cli/commands/inspect/wait-event.ts +0 -1
- package/cli/commands/inspect/wait.ts +2 -2
- package/cli/commands/inspect.ts +19 -4
- package/cli/commands/kotlin.ts +5 -7
- package/cli/commands/maestro-compare.ts +25 -2
- package/cli/commands/mode.ts +13 -1
- package/cli/commands/perf-record.ts +296 -0
- package/cli/commands/perf.ts +6 -1
- package/cli/commands/platform.ts +5 -4
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +28 -5
- package/cli/commands/runtime.ts +9 -11
- package/cli/commands/screenshots.ts +4 -3
- package/cli/commands/setup.ts +17 -5
- package/cli/commands/skills.ts +16 -43
- package/cli/commands/swift.ts +18 -3
- package/cli/commands/upgrade.ts +7 -6
- package/cli/commands/upload.ts +27 -51
- package/cli/commands/window.ts +449 -0
- package/cli/desktop-companion.ts +84 -10
- package/cli/drivers/desktop.ts +2 -0
- package/cli/flow-file.ts +1 -0
- package/cli/maestro-compare.ts +19 -3
- package/cli/main.ts +22 -1
- package/cli/material-symbol-fonts.ts +171 -0
- package/cli/outbound-endpoints.ts +27 -13
- package/cli/preview-capture.ts +2 -2
- package/cli/run-registry.ts +5 -2
- package/cli/telemetry.ts +6 -3
- package/cli/ws-bridge.ts +15 -19
- package/detox/attempt-events.cjs +63 -7
- package/detox/attempt-events.d.cts +7 -0
- package/detox/gestures.ts +115 -2
- package/detox/index.ts +313 -411
- package/detox/jest-environment.cjs +86 -0
- package/detox/jest-preset.cjs +3 -1
- package/detox/jest-setup-after-env.cjs +1 -0
- package/detox/matchers.ts +3 -6
- package/detox/motion-change.cjs +9 -1
- package/detox/proof-frame.cjs +83 -9
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +15 -5
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +4000 -1362
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +6 -1
- package/dist-lib/cloud-contract.mjs +5 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +26 -7
- package/dist-lib/config.mjs +26 -7
- package/dist-lib/detox/index.cjs +1908 -539
- package/dist-lib/detox/jest-environment.cjs +86 -0
- package/dist-lib/detox/jest-preset.cjs +3 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3612 -1016
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +4755 -1825
- package/dist-lib/host/fetch-proxy-handler.cjs +59 -9
- package/dist-lib/host/fetch-proxy-overrides.cjs +62 -2
- package/dist-lib/host/fetch-proxy-overrides.mjs +56 -2
- package/dist-lib/host/replacement-module-handler.cjs +8 -8
- package/dist-lib/host/websocket-proxy.cjs +22 -6
- package/dist-lib/index.cjs +603 -184
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +33 -4
- package/dist-lib/kotlin-host.cjs +262 -121
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +101 -7
- package/dist-lib/metro-production-bundle.mjs +106 -7
- package/dist-lib/metro.cjs +308 -87
- package/dist-lib/native-app-identity-project.cjs +99 -26
- package/dist-lib/native-app-identity-project.mjs +99 -26
- package/dist-lib/native-app-identity.cjs +25 -7
- package/dist-lib/native-app-identity.mjs +25 -7
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +30 -9
- package/dist-lib/sdk.cjs +236 -78
- package/dist-lib/sdk.mjs +236 -78
- package/dist-lib/skills.cjs +6713 -2597
- package/dist-lib/swift-compile.cjs +1632 -299
- package/dist-lib/swift-compile.mjs +1553 -219
- package/dist-lib/swift-package.cjs +25 -3
- package/dist-lib/swift-package.mjs +22 -2
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +3670 -1051
- package/dist-lib/vite.cjs +142 -20
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/install.sh +1 -1
- package/kotlin/AppSources/android/view/Window.kt +4 -1
- package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
- package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
- package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
- package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
- package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
- package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +27 -20
- package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
- package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
- package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
- package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
- package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
- package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
- package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
- package/kotlin/README.md +18 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +30 -40
- package/skills/contrast/SKILL.md +1 -1
- package/skills/peach-perf/SKILL.md +25 -0
- package/skills/peach-setup/SKILL.md +25 -1
- package/src/assetCatalogContract.ts +31 -3
- package/src/auth/shared-session.ts +3 -2
- package/src/bridge-contract.ts +5 -0
- package/src/bundle-channel.mjs +1 -1
- package/src/cloud-contract.ts +11 -0
- package/src/config.ts +16 -8
- package/src/expo-kotlin-host.ts +7 -2
- package/src/guest-bundle.ts +15 -8
- package/src/host/agent-host.ts +2 -4
- package/src/host/bridge-host.ts +52 -54
- package/src/host/fetch-proxy-handler.ts +94 -31
- package/src/host/fetch-proxy-overrides.ts +91 -0
- package/src/host/replacement-module-handler.ts +11 -10
- package/src/host/server-scan-handler.ts +1 -1
- package/src/host/staged-host-files.ts +92 -0
- package/src/host/websocket-proxy.ts +27 -6
- package/src/installed-package-versions.ts +1 -0
- package/src/jump-to-source-native.ts +3 -3
- package/src/kotlin-host.ts +143 -26
- package/src/kotlin-module.ts +52 -0
- package/src/metro-layout-parser.ts +78 -0
- package/src/metro-production-bundle.ts +59 -10
- package/src/native-app-identity-project.ts +65 -3
- package/src/native-app-identity.ts +31 -0
- package/src/native-firebase-config.ts +3 -2
- package/src/native-seam-manifest.ts +0 -44
- package/src/nativeAppIconContract.ts +11 -6
- package/src/nativeViewSourceContract.ts +49 -0
- package/src/nativeViewWebContract.ts +41 -0
- package/src/nitro-source.ts +3 -7
- package/src/one-widget-metadata.ts +23 -3
- package/src/oneWidgetContract.ts +22 -0
- package/src/process-group.ts +306 -0
- package/src/process-liveness.ts +24 -0
- package/src/process-output.ts +21 -2
- package/src/serverDiscoveryContract.ts +41 -0
- package/src/swift-compile.ts +1057 -265
- package/src/swift-design-time.ts +552 -0
- package/src/swift-distribution.lock.json +12 -11
- package/src/swift-host.ts +133 -25
- package/src/swift-objc-sdk.ts +726 -0
- package/src/swift-objc-standin.ts +441 -0
- package/src/swift-package.ts +25 -0
- package/src/swift-packages.ts +25 -10
- package/src/swift-standin-config.ts +94 -0
- package/src/swift-standin-emit.ts +5 -3
- package/src/swift-standins.ts +225 -81
- package/src/swiftContract.ts +4 -0
- package/src/target-matcher.ts +67 -0
- package/src/visit-parsed-program.ts +21 -0
- package/src/vite-plugin-one.ts +58 -16
- package/src/vite-plugin-swift.ts +1 -0
- package/src/vite-plugin.ts +105 -41
- package/src/worker-system-chunks.ts +86 -9
- package/src/worklets-babel.ts +85 -13
- package/src/xcode-project.ts +40 -15
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
package dev.peach.jni
|
|
2
|
+
|
|
3
|
+
private data class IncomingKey(val owner: String, val name: String, val descriptor: String, val isStatic: Boolean)
|
|
4
|
+
private data class IncomingClass(val parents: Array<String>, val accepts: (Any?) -> Boolean)
|
|
5
|
+
private data class IncomingMethod(
|
|
6
|
+
val key: IncomingKey,
|
|
7
|
+
val arguments: Int,
|
|
8
|
+
val virtual: (Any?, Array<Any?>) -> Any?,
|
|
9
|
+
val nonvirtual: ((Any?, Array<Any?>) -> Any?)?,
|
|
10
|
+
)
|
|
11
|
+
private data class IncomingField(val key: IncomingKey, val read: (Any?) -> Any?, val write: ((Any?, Any?) -> Unit)?)
|
|
12
|
+
private val incomingClasses = mutableMapOf<String, IncomingClass>()
|
|
13
|
+
private val incomingMethods = mutableListOf<IncomingMethod>()
|
|
14
|
+
private val incomingMethodIds = mutableMapOf<IncomingKey, Int>()
|
|
15
|
+
private val incomingFields = mutableListOf<IncomingField>()
|
|
16
|
+
private val incomingFieldIds = mutableMapOf<IncomingKey, Int>()
|
|
17
|
+
private var incomingInitialized = false
|
|
18
|
+
private var incomingRetired = false
|
|
19
|
+
|
|
20
|
+
// the compiler replaces this body with registrations of original declarations.
|
|
21
|
+
internal fun initializeIncomingBindings() {
|
|
22
|
+
error("JNI incoming bindings require the compiler plugin")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private fun requireIncomingBindings() {
|
|
26
|
+
check(!incomingRetired) { "native binding runtime retired" }
|
|
27
|
+
if (!incomingInitialized) {
|
|
28
|
+
initializeIncomingBindings()
|
|
29
|
+
incomingInitialized = true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
internal fun registerIncomingClass(owner: String, parents: Array<String>, accepts: (Any?) -> Boolean) {
|
|
34
|
+
check(incomingClasses.put(owner, IncomingClass(parents, accepts)) == null) { "duplicate incoming class: $owner" }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
internal fun registerIncomingMethod(
|
|
38
|
+
owner: String, name: String, descriptor: String, isStatic: Boolean, arguments: Int,
|
|
39
|
+
virtual: (Any?, Array<Any?>) -> Any?, nonvirtual: ((Any?, Array<Any?>) -> Any?)?,
|
|
40
|
+
) {
|
|
41
|
+
val key = IncomingKey(owner, name, descriptor, isStatic)
|
|
42
|
+
check(key !in incomingMethodIds) { "duplicate incoming method: $owner.$name$descriptor" }
|
|
43
|
+
incomingMethods += IncomingMethod(key, arguments, virtual, nonvirtual)
|
|
44
|
+
incomingMethodIds[key] = incomingMethods.size
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
internal fun registerIncomingField(owner: String, name: String, descriptor: String, isStatic: Boolean, read: (Any?) -> Any?, write: ((Any?, Any?) -> Unit)?) {
|
|
48
|
+
val key = IncomingKey(owner, name, descriptor, isStatic)
|
|
49
|
+
check(key !in incomingFieldIds) { "duplicate incoming field: $owner.$name:$descriptor" }
|
|
50
|
+
incomingFields += IncomingField(key, read, write)
|
|
51
|
+
incomingFieldIds[key] = incomingFields.size
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun requireIncomingClass(owner: String) {
|
|
55
|
+
requireIncomingBindings()
|
|
56
|
+
if (owner !in incomingClasses) throw ClassNotFoundException(owner)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun isIncomingInstance(owner: String, receiver: Any?): Boolean {
|
|
60
|
+
requireIncomingClass(owner)
|
|
61
|
+
return incomingClasses.getValue(owner).accepts(receiver)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun isIncomingAssignableFrom(target: String, source: String): Boolean {
|
|
65
|
+
requireIncomingClass(target)
|
|
66
|
+
requireIncomingClass(source)
|
|
67
|
+
fun inherits(type: String): Boolean = type == target || incomingClasses[type]?.parents.orEmpty().any(::inherits)
|
|
68
|
+
return inherits(source)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun incomingClassOf(receiver: Any): String {
|
|
72
|
+
requireIncomingBindings()
|
|
73
|
+
val matches = incomingClasses.filterValues { it.accepts(receiver) }.keys
|
|
74
|
+
return matches.firstOrNull { candidate -> matches.none { it != candidate && isIncomingAssignableFrom(candidate, it) } }
|
|
75
|
+
?: throw ClassNotFoundException("receiver has no registered class")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun lookupIncoming(owner: String, name: String, descriptor: String, isStatic: Boolean, ids: Map<IncomingKey, Int>): Int? {
|
|
79
|
+
val key = IncomingKey(owner, name, descriptor, isStatic)
|
|
80
|
+
ids[key]?.let { return it }
|
|
81
|
+
if (name == "<init>") return null
|
|
82
|
+
for (parent in incomingClasses[owner]?.parents.orEmpty()) {
|
|
83
|
+
lookupIncoming(parent, name, descriptor, isStatic, ids)?.let { return it }
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ids describe declarations; a cached base id remains valid on derived instances.
|
|
89
|
+
fun lookupIncomingMethod(owner: String, name: String, descriptor: String, isStatic: Boolean = false): Int {
|
|
90
|
+
requireIncomingClass(owner)
|
|
91
|
+
return lookupIncoming(owner, name, descriptor, isStatic, incomingMethodIds)
|
|
92
|
+
?: throw NoSuchMethodError("$owner.$name$descriptor")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun lookupIncomingField(owner: String, name: String, descriptor: String, isStatic: Boolean = false): Int {
|
|
96
|
+
requireIncomingClass(owner)
|
|
97
|
+
return lookupIncoming(owner, name, descriptor, isStatic, incomingFieldIds)
|
|
98
|
+
?: throw NoSuchFieldError("$owner.$name:$descriptor")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private fun requireIncomingReceiver(key: IncomingKey, receiver: Any?) {
|
|
102
|
+
if (!key.isStatic && key.name != "<init>") {
|
|
103
|
+
if (receiver == null) throw NullPointerException("null receiver for ${key.owner}.${key.name}")
|
|
104
|
+
if (!incomingClasses.getValue(key.owner).accepts(receiver))
|
|
105
|
+
throw IllegalArgumentException("receiver is not an instance of ${key.owner}")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun invokeIncoming(id: Int, receiver: Any?, arguments: Array<Any?>, nonvirtual: Boolean = false): Any? {
|
|
110
|
+
requireIncomingBindings()
|
|
111
|
+
val method = incomingMethods.getOrNull(id - 1) ?: throw NoSuchMethodError("invalid incoming method id: $id")
|
|
112
|
+
require(method.key.name != "<init>") { "constructors require constructIncoming" }
|
|
113
|
+
requireIncomingReceiver(method.key, receiver)
|
|
114
|
+
require(arguments.size == method.arguments) { "wrong argument count for ${method.key.owner}.${method.key.name}${method.key.descriptor}" }
|
|
115
|
+
val body = if (nonvirtual) method.nonvirtual
|
|
116
|
+
?: throw AbstractMethodError("${method.key.owner}.${method.key.name}${method.key.descriptor}")
|
|
117
|
+
else method.virtual
|
|
118
|
+
return body(receiver, arguments)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fun constructIncoming(id: Int, arguments: Array<Any?>): Any {
|
|
122
|
+
requireIncomingBindings()
|
|
123
|
+
val method = incomingMethods.getOrNull(id - 1) ?: throw NoSuchMethodError("invalid incoming constructor id: $id")
|
|
124
|
+
require(method.key.name == "<init>" && !method.key.isStatic) { "method is not a constructor" }
|
|
125
|
+
require(arguments.size == method.arguments) { "wrong constructor argument count for ${method.key.owner}${method.key.descriptor}" }
|
|
126
|
+
return checkNotNull(method.virtual(null, arguments))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fun readIncomingField(id: Int, receiver: Any?): Any? {
|
|
130
|
+
requireIncomingBindings()
|
|
131
|
+
val field = incomingFields.getOrNull(id - 1) ?: throw NoSuchFieldError("invalid incoming field id: $id")
|
|
132
|
+
requireIncomingReceiver(field.key, receiver)
|
|
133
|
+
return field.read(receiver)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fun writeIncomingField(id: Int, receiver: Any?, value: Any?) {
|
|
137
|
+
requireIncomingBindings()
|
|
138
|
+
val field = incomingFields.getOrNull(id - 1) ?: throw NoSuchFieldError("invalid incoming field id: $id")
|
|
139
|
+
requireIncomingReceiver(field.key, receiver)
|
|
140
|
+
val write = field.write ?: throw IllegalAccessError("final field: ${field.key.owner}.${field.key.name}")
|
|
141
|
+
write(receiver, value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
internal fun retireIncomingBindings() {
|
|
145
|
+
incomingRetired = true
|
|
146
|
+
incomingClasses.clear()
|
|
147
|
+
incomingMethods.clear()
|
|
148
|
+
incomingMethodIds.clear()
|
|
149
|
+
incomingFields.clear()
|
|
150
|
+
incomingFieldIds.clear()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class ClassNotFoundException(message: String) : Exception(message)
|
|
154
|
+
class NoSuchMethodError(message: String) : Error(message)
|
|
155
|
+
class NoSuchFieldError(message: String) : Error(message)
|
|
156
|
+
class AbstractMethodError(message: String) : Error(message)
|
|
157
|
+
class IllegalAccessError(message: String) : Error(message)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class)
|
|
2
|
+
|
|
3
|
+
package dev.peach.jni
|
|
4
|
+
|
|
5
|
+
import kotlin.js.*
|
|
6
|
+
|
|
7
|
+
private data class Method(val owner: String, val name: String, val descriptor: String)
|
|
8
|
+
private val methods = mutableMapOf<Method, (Any?, Array<Any?>) -> Any?>()
|
|
9
|
+
private var nativeRuntime: NativeRuntime? = null
|
|
10
|
+
private var retired = false
|
|
11
|
+
private var retiring = false
|
|
12
|
+
private val retirementActions = mutableListOf<() -> Unit>()
|
|
13
|
+
private fun assertActive() = check(!retired) { "native binding runtime retired" }
|
|
14
|
+
|
|
15
|
+
fun isNativeRuntimeActive(): Boolean = !retired && !retiring
|
|
16
|
+
|
|
17
|
+
fun registerNativeRetirement(action: () -> Unit) {
|
|
18
|
+
check(isNativeRuntimeActive()) { "native binding runtime retired" }
|
|
19
|
+
retirementActions += action
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// registration glue supplies native entrypoints; package method bodies stay unchanged.
|
|
23
|
+
fun registerNative(owner: String, method: String, descriptor: String, body: (Any?, Array<Any?>) -> Any?) {
|
|
24
|
+
check(isNativeRuntimeActive()) { "native binding runtime retired" }
|
|
25
|
+
val key = Method(owner, method, descriptor)
|
|
26
|
+
check(key !in methods) { "native method already bound: $owner.$method$descriptor" }
|
|
27
|
+
methods[key] = body
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun invoke(receiver: Any?, owner: String, method: String, descriptor: String, arguments: Array<Any?>): Any? {
|
|
31
|
+
assertActive()
|
|
32
|
+
val body = methods[Method(owner, method, descriptor)]
|
|
33
|
+
?: throw UnsatisfiedLinkError("unbound native method: $owner.$method$descriptor")
|
|
34
|
+
return body(receiver, arguments)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class UnsatisfiedLinkError(message: String) : Error(message)
|
|
38
|
+
|
|
39
|
+
external interface NativePeer : JsAny {
|
|
40
|
+
fun reset()
|
|
41
|
+
fun isValid(): Boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
external interface NativeCallback : JsAny {
|
|
45
|
+
fun invoke(arguments: JsReference<Array<Any?>>): JsReference<Any>?
|
|
46
|
+
fun release()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
external interface NativeRuntime : JsAny {
|
|
50
|
+
fun create(owner: String, receiver: JsReference<Any>): NativePeer
|
|
51
|
+
fun attach(peer: NativePeer, owner: JsReference<Any>)
|
|
52
|
+
fun capture(peer: NativePeer, receiver: JsReference<Any>, dispatch: (JsReference<Any>, JsReference<Array<Any?>>) -> JsReference<Any>?): NativeCallback
|
|
53
|
+
fun retire()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fun installNativeRuntime(runtime: NativeRuntime) {
|
|
57
|
+
check(isNativeRuntimeActive()) { "native binding runtime retired" }
|
|
58
|
+
check(nativeRuntime == null) { "native binding runtime is already installed" }
|
|
59
|
+
nativeRuntime = runtime
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fun retireNativeRuntime() {
|
|
63
|
+
if (retired || retiring) return
|
|
64
|
+
retiring = true
|
|
65
|
+
var failure: Throwable? = null
|
|
66
|
+
try {
|
|
67
|
+
for (action in retirementActions) {
|
|
68
|
+
try { action() } catch (error: Throwable) { if (failure == null) failure = error }
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
retirementActions.clear()
|
|
72
|
+
retired = true
|
|
73
|
+
retireIncomingBindings()
|
|
74
|
+
nativeRuntime?.retire()
|
|
75
|
+
methods.clear()
|
|
76
|
+
}
|
|
77
|
+
failure?.let { throw it }
|
|
78
|
+
// retirement is terminal for this Kotlin process.
|
|
79
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
@file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class)
|
|
2
|
+
|
|
3
|
+
package dev.peach.jni.integration
|
|
4
|
+
|
|
5
|
+
import dev.peach.jni.*
|
|
6
|
+
import kotlin.js.*
|
|
7
|
+
|
|
8
|
+
external interface JniTransportReply : JsAny {
|
|
9
|
+
val value: JsReference<Any>?
|
|
10
|
+
val error: JsReference<Throwable>?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private fun transportReply(value: JsReference<Any>?, error: JsReference<Throwable>?): JniTransportReply =
|
|
14
|
+
js("({ value, error })")
|
|
15
|
+
|
|
16
|
+
private inline fun transport(body: () -> Any?): JniTransportReply = try {
|
|
17
|
+
transportReply(body()?.toJsReference(), null)
|
|
18
|
+
} catch (error: Throwable) {
|
|
19
|
+
transportReply(null, error.toJsReference())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@JsExport fun jniMethod(owner: String, name: String, descriptor: String, isStatic: Boolean): JniTransportReply =
|
|
23
|
+
transport { lookupIncomingMethod(owner, name, descriptor, isStatic) }
|
|
24
|
+
|
|
25
|
+
@JsExport fun jniField(owner: String, name: String, descriptor: String, isStatic: Boolean): JniTransportReply =
|
|
26
|
+
transport { lookupIncomingField(owner, name, descriptor, isStatic) }
|
|
27
|
+
|
|
28
|
+
@JsExport fun jniInvoke(id: Int, receiver: JsReference<Any>?, arguments: JsReference<Array<Any?>>, nonvirtual: Boolean): JniTransportReply =
|
|
29
|
+
transport { invokeIncoming(id, receiver?.get(), arguments.get(), nonvirtual) }
|
|
30
|
+
|
|
31
|
+
@JsExport fun jniConstruct(id: Int, arguments: JsReference<Array<Any?>>): JniTransportReply =
|
|
32
|
+
transport { constructIncoming(id, arguments.get()) }
|
|
33
|
+
|
|
34
|
+
@JsExport fun jniRead(id: Int, receiver: JsReference<Any>?): JniTransportReply =
|
|
35
|
+
transport { readIncomingField(id, receiver?.get()) }
|
|
36
|
+
|
|
37
|
+
@JsExport fun jniWrite(id: Int, receiver: JsReference<Any>?, value: JsReference<Any>?): JniTransportReply =
|
|
38
|
+
transport { writeIncomingField(id, receiver?.get(), value?.get()); null }
|
|
39
|
+
|
|
40
|
+
@JsExport fun jniRequireClass(owner: String): JniTransportReply = transport { requireIncomingClass(owner); null }
|
|
41
|
+
@JsExport fun jniClassOf(receiver: JsReference<Any>): JniTransportReply = transport { incomingClassOf(receiver.get()) }
|
|
42
|
+
@JsExport fun jniInstanceOf(owner: String, receiver: JsReference<Any>?): JniTransportReply = transport { isIncomingInstance(owner, receiver?.get()) }
|
|
43
|
+
@JsExport fun jniAssignable(target: String, source: String): JniTransportReply = transport { isIncomingAssignableFrom(target, source) }
|
|
44
|
+
@JsExport fun jniSame(a: JsReference<Any>?, b: JsReference<Any>?): Boolean = a?.get() === b?.get()
|
|
45
|
+
|
|
46
|
+
@JsExport fun jniArguments(count: Int): JsReference<Array<Any?>> = arrayOfNulls<Any>(count).toJsReference()
|
|
47
|
+
@JsExport fun jniArgument(arguments: JsReference<Array<Any?>>, index: Int, value: JsReference<Any>?) { arguments.get()[index] = value?.get() }
|
|
48
|
+
@JsExport fun jniArgumentAt(arguments: JsReference<Array<Any?>>, index: Int): JsReference<Any>? = arguments.get()[index]?.toJsReference()
|
|
49
|
+
@JsExport fun jniArgumentCount(arguments: JsReference<Array<Any?>>): Int = arguments.get().size
|
|
50
|
+
@JsExport fun jniInt(value: JsReference<Any>): Int = when (val scalar = value.get()) {
|
|
51
|
+
is Int -> scalar
|
|
52
|
+
is Byte -> scalar.toInt()
|
|
53
|
+
is Short -> scalar.toInt()
|
|
54
|
+
is Char -> scalar.code
|
|
55
|
+
else -> error("JNI value is not an integer scalar")
|
|
56
|
+
}
|
|
57
|
+
@JsExport fun jniLong(value: JsReference<Any>): String = (value.get() as Long).toString()
|
|
58
|
+
@JsExport fun jniString(value: JsReference<Any>): String = value.get() as String
|
|
59
|
+
@JsExport fun jniBoolean(value: JsReference<Any>): Boolean = value.get() as Boolean
|
|
60
|
+
@JsExport fun jniNumber(value: JsReference<Any>): Double = when (val scalar = value.get()) {
|
|
61
|
+
is Float -> scalar.toDouble()
|
|
62
|
+
is Double -> scalar
|
|
63
|
+
else -> error("JNI value is not a floating-point scalar")
|
|
64
|
+
}
|
|
65
|
+
@JsExport fun jniPrimitive(descriptor: String, integer: String, number: Double, text: String): JniTransportReply = transport {
|
|
66
|
+
when (descriptor) {
|
|
67
|
+
"Z" -> when (integer) { "0" -> false; "1" -> true; else -> error("invalid JNI boolean") }
|
|
68
|
+
"B" -> integer.toByte()
|
|
69
|
+
"C" -> integer.toInt().also { require(it in 0..65535) }.toChar()
|
|
70
|
+
"S" -> integer.toShort()
|
|
71
|
+
"I" -> integer.toInt()
|
|
72
|
+
"J" -> integer.toLong()
|
|
73
|
+
"F" -> number.toFloat()
|
|
74
|
+
"D" -> number
|
|
75
|
+
"Ljava/lang/String;" -> text
|
|
76
|
+
else -> error("unsupported JNI scalar descriptor: $descriptor")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
@JsExport fun jniErrorClass(error: JsReference<Throwable>): String? = error.get()::class.simpleName
|
|
80
|
+
@JsExport fun jniErrorMessage(error: JsReference<Throwable>): String? = error.get().message
|
|
81
|
+
|
|
82
|
+
@JsExport fun jniBind(owner: String, name: String, descriptor: String,
|
|
83
|
+
callback: (JsReference<Any>?, JsReference<Array<Any?>>) -> JniTransportReply): JniTransportReply = transport {
|
|
84
|
+
registerNative(owner, name, descriptor) { receiver, arguments ->
|
|
85
|
+
val result = callback(receiver?.toJsReference(), arguments.toJsReference())
|
|
86
|
+
result.error?.let { throw it.get() }
|
|
87
|
+
result.value?.get()
|
|
88
|
+
}
|
|
89
|
+
null
|
|
90
|
+
}
|
|
91
|
+
@JsExport fun jniRetire(): JniTransportReply = transport { retireNativeRuntime(); null }
|
package/kotlin/README.md
CHANGED
|
@@ -31,6 +31,24 @@ traffic in `scripts/debug/peach-swift-lazy-network.sh`.
|
|
|
31
31
|
- `ExpoSources/`: the expo-modules-core Kotlin DSL port and `PeachExpoRuntime`
|
|
32
32
|
that local Expo Android modules compile against. Owner spec:
|
|
33
33
|
[`specs/peach-kotlin-expo-modules.md`](../../../specs/peach-kotlin-expo-modules.md).
|
|
34
|
+
- `CompilerSources/`: the native member compiler plugin and its JVM service
|
|
35
|
+
registration. The shared host compiles it into a classes directory cached by
|
|
36
|
+
the pinned compiler and every plugin source byte. Completed directories publish
|
|
37
|
+
atomically under `~/.peach/kotlin/compiler-plugins/`; all Kotlin entry paths
|
|
38
|
+
load that plugin. `JniPlatformSources/` and `JniSources/` supply the shared
|
|
39
|
+
platform declarations and native invocation runtime for imported modules,
|
|
40
|
+
pure Kotlin apps and Expo modules.
|
|
41
|
+
Incoming JNI lookup has stable method and field IDs, declaration-backed private
|
|
42
|
+
and virtual calls, actual constructors and backing-field access. Native static
|
|
43
|
+
companion calls preserve the enclosing JVM owner and null receiver. The
|
|
44
|
+
[binding contract](../../../docs/research/peach/kotlin-native-bindings.md)
|
|
45
|
+
owns the APIs, holder lifetime and unsupported forms.
|
|
46
|
+
|
|
47
|
+
App artifact hashes include compiler and library pins, plugin sources, platform
|
|
48
|
+
and runtime sources, app sources and generated glue. The host compiles byte
|
|
49
|
+
snapshots of the hashed app, platform and plugin inputs, preserving their
|
|
50
|
+
original paths in compiler diagnostics. A cached artifact needs no compiler
|
|
51
|
+
process; another app reuses the compiled plugin without rebuilding it.
|
|
34
52
|
|
|
35
53
|
The dev server compiles every `.kt` source of the app, One's glue for each and
|
|
36
54
|
a generated entry into one Kotlin/Wasm module, held to the Kotlin language
|
package/package.json
CHANGED
|
@@ -44,8 +44,9 @@ import {
|
|
|
44
44
|
parseSwiftHostDiscovery,
|
|
45
45
|
} from '../src/swiftDiscoveryContract'
|
|
46
46
|
import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
|
|
47
|
-
import type {
|
|
48
|
-
|
|
47
|
+
import type { DiscoveredServer } from '../src/serverDiscoveryContract'
|
|
48
|
+
|
|
49
|
+
export type { DiscoveredServer } from '../src/serverDiscoveryContract'
|
|
49
50
|
|
|
50
51
|
// one machine-wide lock. lsof walks every open fd, so overlapping scans from
|
|
51
52
|
// every peach process are what drives the load average through the roof.
|
|
@@ -297,37 +298,6 @@ function spawnReaped(
|
|
|
297
298
|
)
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
export interface DiscoveredServer {
|
|
301
|
-
port: number
|
|
302
|
-
framework: 'metro' | 'expo' | 'vxrn' | 'one' | 'swift' | 'kotlin' | 'unknown'
|
|
303
|
-
status?: 'compiling' | 'ready' | 'error'
|
|
304
|
-
projectName?: string
|
|
305
|
-
bundleUrl: string
|
|
306
|
-
hmrUrl?: string
|
|
307
|
-
lastSeen: number
|
|
308
|
-
iconUrl?: string
|
|
309
|
-
iconPath?: string
|
|
310
|
-
/** the app's own brand colour from its manifest, used as the home tile's
|
|
311
|
-
* background so the tile reads as that app before its icon decodes and
|
|
312
|
-
* when the icon is missing entirely. */
|
|
313
|
-
primaryColor?: string
|
|
314
|
-
bundleId?: string
|
|
315
|
-
widget?: ConfiguredOneWidget | null
|
|
316
|
-
settingsBundle?: readonly AppSettingsSpecifier[]
|
|
317
|
-
settingsStorageScopeUrl?: string
|
|
318
|
-
patched?: boolean
|
|
319
|
-
/** true when `bundleUrl` was NOT derived from the server's own manifest —
|
|
320
|
-
* the manifest probe got no answer, so this is the generic `/index.bundle`
|
|
321
|
-
* guess. good enough to LIST the server; never good enough to open it. see
|
|
322
|
-
* `manifestState` below for why the two are different answers. */
|
|
323
|
-
bundleUrlProvisional?: boolean
|
|
324
|
-
/** absolute cwd of the owning node/bun process — resolved from `lsof -d cwd`
|
|
325
|
-
* when the scanner can see the pid. used to auto-attach the project for
|
|
326
|
-
* agent sessions without requiring a manual `sootsim agent attach`. */
|
|
327
|
-
cwd?: string
|
|
328
|
-
pid?: number
|
|
329
|
-
}
|
|
330
|
-
|
|
331
301
|
/** how the manifest probe (`GET /` with expo-platform) actually went. the same
|
|
332
302
|
* three answers `dev-bundle-resolution.ts` uses, deliberately spelled the same
|
|
333
303
|
* way: a second vocabulary for one question is how the two resolvers drift.
|
|
@@ -876,14 +846,18 @@ function makeResult(
|
|
|
876
846
|
}
|
|
877
847
|
|
|
878
848
|
function withRuntimeConfig(port: number, bundleUrl: string, projectCwd?: string): string {
|
|
849
|
+
// bun's realpathSync opens the directory, and another process's cwd can sit on a volume
|
|
850
|
+
// whose privacy grant blocks that open behind a pending prompt, so only resolve it when
|
|
851
|
+
// the internal registry has an app to match. a published install has none.
|
|
852
|
+
const demoApps = optionalDemoAppsSync()
|
|
879
853
|
let physicalProjectCwd: string | undefined
|
|
880
|
-
if (projectCwd) {
|
|
854
|
+
if (projectCwd && demoApps.length > 0) {
|
|
881
855
|
try {
|
|
882
856
|
physicalProjectCwd = realpathSync(resolve(projectCwd))
|
|
883
857
|
} catch {}
|
|
884
858
|
}
|
|
885
859
|
const knownApps = physicalProjectCwd
|
|
886
|
-
?
|
|
860
|
+
? demoApps.filter((app) => {
|
|
887
861
|
try {
|
|
888
862
|
return realpathSync(resolve(app.dir)) === physicalProjectCwd
|
|
889
863
|
} catch {
|
|
@@ -982,13 +956,28 @@ function applyManifest(
|
|
|
982
956
|
rawLaunchUrl,
|
|
983
957
|
buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port),
|
|
984
958
|
)
|
|
985
|
-
:
|
|
959
|
+
: undefined
|
|
986
960
|
if (launchUrl && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
|
|
987
961
|
result.bundleUrl = withRuntimeConfig(result.port, launchUrl, result.cwd)
|
|
988
962
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const
|
|
963
|
+
// expo's withIosIcons.ts selects its base icon as light || dark || tinted.
|
|
964
|
+
// normalize before publishing iconPath, which local asset probes consume.
|
|
965
|
+
const iosIcon: unknown = client.ios?.icon
|
|
966
|
+
const defaultIosIcon =
|
|
967
|
+
iosIcon && typeof iosIcon === 'object'
|
|
968
|
+
? ('light' in iosIcon && iosIcon.light) ||
|
|
969
|
+
('dark' in iosIcon && iosIcon.dark) ||
|
|
970
|
+
('tinted' in iosIcon && iosIcon.tinted)
|
|
971
|
+
: iosIcon
|
|
972
|
+
const icons: unknown[] = [
|
|
973
|
+
defaultIosIcon,
|
|
974
|
+
client.icon,
|
|
975
|
+
client.ios?.iconUrl,
|
|
976
|
+
client.iconUrl,
|
|
977
|
+
]
|
|
978
|
+
const rawIcon = icons.find(
|
|
979
|
+
(icon): icon is string => typeof icon === 'string' && icon.trim().length > 0,
|
|
980
|
+
)
|
|
992
981
|
if (rawIcon) {
|
|
993
982
|
result.iconPath = rawIcon
|
|
994
983
|
let resolvedIconUrl: string | undefined
|
|
@@ -1483,7 +1472,8 @@ export async function probePort(
|
|
|
1483
1472
|
oneRes &&
|
|
1484
1473
|
oneRes.statusCode > 0 &&
|
|
1485
1474
|
oneRes.statusCode < 400 &&
|
|
1486
|
-
/application
|
|
1475
|
+
// one's native dev engine serves text/javascript; metro uses application/javascript.
|
|
1476
|
+
/^(?:application|text)\/javascript(?:\s*;|$)/i.test(oneRes.contentType?.trim() || '')
|
|
1487
1477
|
) {
|
|
1488
1478
|
knownNonPatched.add(port)
|
|
1489
1479
|
knownOne.add(port)
|
package/skills/contrast/SKILL.md
CHANGED
|
@@ -39,7 +39,7 @@ automation, inspecting the render tree, and managing sims.
|
|
|
39
39
|
| `peach do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
|
|
40
40
|
| `peach wait` | block until the runtime reaches a known state |
|
|
41
41
|
| `peach shell` | drive shell state and simulator chrome |
|
|
42
|
-
| `peach mode` | toggle screenshot, touch indicators, and 3D display modes |
|
|
42
|
+
| `peach mode` | toggle screenshot, touch indicators, the agent indicator, and 3D display modes |
|
|
43
43
|
| `peach device` | inspect or change the live device preset for a sim |
|
|
44
44
|
| `peach network` | inspect live network traffic from the running peach worker |
|
|
45
45
|
| `peach logs` | inspect live console output from the running peach worker |
|
|
@@ -62,6 +62,31 @@ indict, remeasure — and every wasted week came from skipping a step.
|
|
|
62
62
|
|
|
63
63
|
## measuring
|
|
64
64
|
|
|
65
|
+
### record a Performance timeline
|
|
66
|
+
|
|
67
|
+
`peach perf record` and the devtools Performance tab share one recording.
|
|
68
|
+
Start from either, interact with the app, then Stop from either. The saved
|
|
69
|
+
category choice is used unless the command supplies a preset or category list:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
peach perf record start --preset default
|
|
73
|
+
peach perf record status --json
|
|
74
|
+
peach perf record stop -o /tmp/app.profile.jsonl.gz
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Default records compositor frames, Animated and Reanimated animation runs,
|
|
78
|
+
interactions, network and logs. `--categories frames,animations,react` selects
|
|
79
|
+
specific categories. React attribution and memory cost more than Default;
|
|
80
|
+
Paint Detail changes the renderer's path, so do not compare those frame
|
|
81
|
+
times with a Default run. A recording retains its last five minutes and
|
|
82
|
+
reports gaps when frame coverage is lost. Stop releases its profiling resources.
|
|
83
|
+
Save writes the preview timeline’s gzip JSONL format. Open reads it, plain
|
|
84
|
+
JSONL, or a CLI JSON profile; a `.json` CLI output keeps the JSON form. Both
|
|
85
|
+
forms include frame columns, animation spans, discrete events, missed display
|
|
86
|
+
ticks and optional value samples. Upload shares the profile alongside the
|
|
87
|
+
currently loaded app through the preview uploader. This is a diagnostic capture; use the independent
|
|
88
|
+
presentation evidence below to make smoothness claims.
|
|
89
|
+
|
|
65
90
|
### presentation proof must stay outside the simulator
|
|
66
91
|
|
|
67
92
|
Performance acceptance must not use `peach record`, `liveComposite`,
|
|
@@ -74,6 +74,28 @@ debugging or `/peach-test` for writing tests.
|
|
|
74
74
|
saved sim to reuse. verify the development server and Peach runtime, then
|
|
75
75
|
retry once after fixing the cause.
|
|
76
76
|
|
|
77
|
+
## choose the runtime
|
|
78
|
+
|
|
79
|
+
on Apple silicon macOS and Linux x64, recommend trying the optional native
|
|
80
|
+
runtime for faster simulation. it is beta, so the user may run into more issues
|
|
81
|
+
than in the browser. keep the current default unless the user chooses native:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
peach desktop install
|
|
85
|
+
peach open 8081 --driver desktop
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
use the app's actual development server port. the Linux desktop app needs a
|
|
89
|
+
desktop session; its `.deb` installer uses `sudo apt-get install`.
|
|
90
|
+
`peach desktop install --native` on Linux installs the standalone headless host
|
|
91
|
+
instead of the desktop app. Intel Mac and Linux arm64 use the browser.
|
|
92
|
+
`peach open 8081 --driver playwright --headed` explicitly opens a browser window.
|
|
93
|
+
pass the selected driver when opening the app in the steps below; installing
|
|
94
|
+
the desktop app alone does not change the saved window preference.
|
|
95
|
+
|
|
96
|
+
the native runtime renders the simulator. the background service below keeps
|
|
97
|
+
the bridge ready for either runtime and is a separate optional choice.
|
|
98
|
+
|
|
77
99
|
## first move
|
|
78
100
|
|
|
79
101
|
```sh
|
|
@@ -97,11 +119,13 @@ scripts should use `peach open <port-or-url>` so the target is explicit.
|
|
|
97
119
|
|
|
98
120
|
if you do not know the port, run `peach open` with no target and pick from
|
|
99
121
|
the detected dev servers. for hosted or unusual targets, pass the URL
|
|
100
|
-
explicitly
|
|
122
|
+
explicitly. a built bundle file needs no dev server; `peach help open` shows
|
|
123
|
+
the build command for One, Expo and React Native:
|
|
101
124
|
|
|
102
125
|
```sh
|
|
103
126
|
peach open 8081 # load a metro dev server
|
|
104
127
|
peach open https://my-app.local # load a hosted bundle
|
|
128
|
+
peach open ./dist/ios.js # load a built production bundle, no dev server
|
|
105
129
|
```
|
|
106
130
|
|
|
107
131
|
## you're done when
|
|
@@ -54,18 +54,46 @@ export interface SfSymbolShaderImage {
|
|
|
54
54
|
readonly pixels: Uint8Array
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// UIImage's ordered monochrome layers and unrounded content bearings.
|
|
57
|
+
// UIImage's ordered monochrome layers and unrounded content bearings. a layer
|
|
58
|
+
// with a variable threshold turns off below it; one without never does.
|
|
58
59
|
export interface SfSymbolImageDrawing {
|
|
59
60
|
referencePointSize: number
|
|
60
61
|
basePointSize: number
|
|
61
62
|
baselineOffsetUnrounded: number
|
|
62
63
|
referenceCanvasSize: readonly [number, number]
|
|
63
64
|
insets: readonly [number, number, number, number]
|
|
64
|
-
layers: readonly { d: string; opacity: number; eraser: boolean }[]
|
|
65
|
+
layers: readonly { d: string; opacity: number; eraser: boolean; threshold?: number }[]
|
|
66
|
+
// an image view reflects a flippable drawing under right to left
|
|
67
|
+
flippable?: boolean
|
|
68
|
+
// automatic variable mode draws the value along the paths rather than
|
|
69
|
+
// turning layers off by threshold
|
|
70
|
+
variableDraw?: boolean
|
|
71
|
+
// coreui's own right to left rendition, which replaces the drawing there
|
|
72
|
+
rightToLeft?: SfSymbolImageDrawing
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
// a symbol's palette layers, bottom first, in its monochrome outline's space.
|
|
76
|
+
// level is the palette slot a layer paints in (0 primary, 1 secondary, 2
|
|
77
|
+
// tertiary); a layer with a variable threshold turns off below it, as its
|
|
78
|
+
// image layer does.
|
|
79
|
+
export interface SfSymbolPalette {
|
|
80
|
+
layers: readonly { d: string; level: number; threshold?: number }[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SfSymbolImageMetadata extends Omit<
|
|
84
|
+
SfSymbolImageDrawing,
|
|
85
|
+
'layers' | 'rightToLeft'
|
|
86
|
+
> {
|
|
68
87
|
layers: readonly { opacity: number; eraser: boolean }[]
|
|
88
|
+
rightToLeft?: SfSymbolImageMetadata
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// the drawing a symbol image view shows in its laid out direction
|
|
92
|
+
export function sfSymbolImageForDirection(
|
|
93
|
+
drawing: SfSymbolImageMetadata,
|
|
94
|
+
rightToLeft: boolean,
|
|
95
|
+
): SfSymbolImageMetadata {
|
|
96
|
+
return rightToLeft && drawing.rightToLeft ? drawing.rightToLeft : drawing
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
export function sfSymbolImageDimensions(
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'node:fs'
|
|
12
12
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
13
13
|
import { electronUserDataDir } from '../home-paths.ts'
|
|
14
|
+
import { peachPublicBrand } from '../public-brand.ts'
|
|
14
15
|
|
|
15
16
|
export type SharedDesktopAuthTeamSummary = {
|
|
16
17
|
id: string
|
|
@@ -61,7 +62,7 @@ export function sharedDesktopAuthAccounts(
|
|
|
61
62
|
|
|
62
63
|
const SESSION_VERSION = 1 as const
|
|
63
64
|
const SESSION_FILE_ENV = 'SOOTSIM_SHARED_AUTH_FILE'
|
|
64
|
-
const DEFAULT_ORIGIN =
|
|
65
|
+
const DEFAULT_ORIGIN = peachPublicBrand.origin
|
|
65
66
|
|
|
66
67
|
function getBaseDir() {
|
|
67
68
|
const explicit = process.env[SESSION_FILE_ENV]
|
|
@@ -186,7 +187,7 @@ export function writeSharedDesktopAuthSession(
|
|
|
186
187
|
version: SESSION_VERSION,
|
|
187
188
|
token,
|
|
188
189
|
user: session.user ? normalizeUser(session.user) : null,
|
|
189
|
-
origin: session.origin?.trim() || DEFAULT_ORIGIN,
|
|
190
|
+
origin: session.origin?.trim() || same?.origin || DEFAULT_ORIGIN,
|
|
190
191
|
source: same?.source ?? session.source,
|
|
191
192
|
updatedAt: session.updatedAt || new Date().toISOString(),
|
|
192
193
|
validatedAt: session.validatedAt ?? same?.validatedAt,
|
package/src/bridge-contract.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
SootSimCaptureFilter,
|
|
15
15
|
SootSimExternalAppLifecycleOptions,
|
|
16
16
|
SootSimStorageResetStrategy,
|
|
17
|
+
TextInputEditCommand,
|
|
17
18
|
} from '@peach/globals'
|
|
18
19
|
|
|
19
20
|
export type BridgeCommandType =
|
|
@@ -67,6 +68,9 @@ export interface WsCommand {
|
|
|
67
68
|
acquireLock?: boolean
|
|
68
69
|
// bridge:claim — take the lease even when another cli holds it.
|
|
69
70
|
force?: boolean
|
|
71
|
+
// bridge:list-sims — also report the retained disconnected sims (readyState
|
|
72
|
+
// closed with disconnectCause) after the live ones. read in bridge-host.
|
|
73
|
+
includeRetained?: boolean
|
|
70
74
|
// flowStatus — live flow-runner progress payload (see flow-run-store)
|
|
71
75
|
status?: Record<string, unknown>
|
|
72
76
|
code?: string
|
|
@@ -79,6 +83,7 @@ export interface WsCommand {
|
|
|
79
83
|
target?: SootSimAgentActionDetail['target']
|
|
80
84
|
action?: string
|
|
81
85
|
text?: string
|
|
86
|
+
edit?: TextInputEditCommand
|
|
82
87
|
appearance?: 'light' | 'dark'
|
|
83
88
|
url?: string
|
|
84
89
|
includeVisual?: boolean
|