peachsim 0.1.626 → 0.1.628
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/LICENSE +1 -1
- package/README.md +3 -2
- package/cli/bin.ts +6 -2
- package/cli/bridge-flow-runner.ts +79 -72
- package/cli/commands/box/bundle-plane.ts +7 -2
- package/cli/commands/box.ts +9 -2
- package/cli/commands/cleanup.ts +6 -6
- package/cli/commands/control.ts +36 -6
- package/cli/commands/detox.ts +25 -11
- package/cli/commands/flow.ts +20 -8
- package/cli/commands/inspect.ts +22 -37
- package/cli/commands/maestro-generate.ts +5 -12
- package/cli/commands/maestro.ts +16 -6
- package/cli/commands/mode.ts +11 -7
- package/cli/commands/platform.ts +1 -0
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/setup.ts +16 -11
- package/cli/commands/show-touches-mode.ts +33 -0
- package/cli/commands/skills.ts +16 -0
- package/cli/commands/test.ts +22 -1
- package/cli/commands/upload.ts +18 -8
- package/cli/commands/welcome.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +16 -8
- package/cli/drivers/playwright.ts +8 -5
- package/cli/help-core.ts +9 -4
- package/cli/internal-child.ts +14 -2
- package/cli/levenshtein.ts +27 -0
- package/cli/main.ts +28 -18
- package/cli/run-rnx.ts +8 -4
- package/cli/ws-bridge.ts +3 -2
- package/detox/index.ts +234 -7
- package/dist-lib/agent-daemon-client.cjs +2 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- 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 +4 -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/capture-contract.cjs +18 -8
- package/dist-lib/capture-contract.mjs +18 -8
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -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 +1 -1
- package/dist-lib/detox/index.cjs +170 -13
- package/dist-lib/dev-bundle-resolution.cjs +2 -2
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +280 -138
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +62 -12
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +7 -1
- package/dist-lib/menu.mjs +7 -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 +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +43 -4
- 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/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +215 -28
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +431 -276
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +14512 -14208
- package/dist-lib/vite-kotlin.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +44 -4
- package/install.mjs +16 -0
- package/install.sh +199 -0
- package/package.json +6 -1
- package/scripts/dev-server-scanner.ts +77 -46
- package/skills/contrast/SKILL.md +1 -1
- package/src/bridge-constants.ts +11 -0
- package/src/capture-contract.ts +40 -7
- package/src/dev-bundle-resolution.ts +1 -1
- package/src/host/bridge-host.ts +26 -137
- package/src/menu.ts +7 -0
- package/src/native-seam-manifest.ts +4 -16
- package/src/runtime-assets.ts +59 -2
- package/src/vite-plugin.ts +723 -25
package/dist-lib/vite-kotlin.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.628 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/dist-lib/vite-swift.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.628 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.628 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -47,6 +47,7 @@ var import_ws = require("ws");
|
|
|
47
47
|
// src/bridge-constants.ts
|
|
48
48
|
var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
|
|
49
49
|
var DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`;
|
|
50
|
+
var AUTOMATION_SIM_IDLE_REAP_TTL_MS = 5 * 6e4;
|
|
50
51
|
|
|
51
52
|
// src/dev-server-open.ts
|
|
52
53
|
var import_node_child_process = require("node:child_process");
|
|
@@ -660,7 +661,11 @@ var MIME_TYPES = {
|
|
|
660
661
|
".woff": "font/woff",
|
|
661
662
|
".woff2": "font/woff2",
|
|
662
663
|
".mp3": "audio/mpeg",
|
|
663
|
-
".wav": "audio/wav"
|
|
664
|
+
".wav": "audio/wav",
|
|
665
|
+
".mp4": "video/mp4",
|
|
666
|
+
".m4v": "video/mp4",
|
|
667
|
+
".mov": "video/quicktime",
|
|
668
|
+
".webm": "video/webm"
|
|
664
669
|
};
|
|
665
670
|
var ROOT_RUNTIME_PATHS = [
|
|
666
671
|
"/assets/",
|
|
@@ -674,7 +679,7 @@ var ROOT_RUNTIME_PATHS = [
|
|
|
674
679
|
"/spike/",
|
|
675
680
|
"/test-wallpaper.jpg",
|
|
676
681
|
"/preview-sw.js",
|
|
677
|
-
"/
|
|
682
|
+
"/peach.svg",
|
|
678
683
|
"/bedrock-sqlite.wasm",
|
|
679
684
|
"/bedrock-sqlite.js",
|
|
680
685
|
"/sootsim/"
|
|
@@ -758,7 +763,42 @@ function serveRuntimeFile(req, res, fullPath) {
|
|
|
758
763
|
res.end();
|
|
759
764
|
return;
|
|
760
765
|
}
|
|
761
|
-
|
|
766
|
+
sendFileWithRanges(req, res, fullPath, stat.size, {});
|
|
767
|
+
}
|
|
768
|
+
function sendFileWithRanges(req, res, filePath, total, headers) {
|
|
769
|
+
const head = (req?.method || "GET").toUpperCase() === "HEAD";
|
|
770
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(req?.headers?.range || "");
|
|
771
|
+
const invalid = range && range[1] && range[2] && Number(range[2]) < Number(range[1]);
|
|
772
|
+
if (range && (range[1] || range[2]) && !invalid) {
|
|
773
|
+
const [, startRaw, endRaw] = range;
|
|
774
|
+
const start = startRaw ? Number(startRaw) : Math.max(0, total - Number(endRaw));
|
|
775
|
+
const end = startRaw && endRaw ? Math.min(Number(endRaw), total - 1) : total - 1;
|
|
776
|
+
if (start > end || start >= total) {
|
|
777
|
+
res.writeHead(416, { ...headers, "Content-Range": `bytes */${total}` });
|
|
778
|
+
res.end();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
res.writeHead(206, {
|
|
782
|
+
...headers,
|
|
783
|
+
"Content-Range": `bytes ${start}-${end}/${total}`,
|
|
784
|
+
"Accept-Ranges": "bytes",
|
|
785
|
+
"Content-Length": String(end - start + 1)
|
|
786
|
+
});
|
|
787
|
+
if (head) res.end();
|
|
788
|
+
else streamFile(res, import_fs.default.createReadStream(filePath, { start, end }));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
res.writeHead(200, {
|
|
792
|
+
...headers,
|
|
793
|
+
"Accept-Ranges": "bytes",
|
|
794
|
+
"Content-Length": String(total)
|
|
795
|
+
});
|
|
796
|
+
if (head || total === 0) res.end();
|
|
797
|
+
else streamFile(res, import_fs.default.createReadStream(filePath, { start: 0, end: total - 1 }));
|
|
798
|
+
}
|
|
799
|
+
function streamFile(res, stream) {
|
|
800
|
+
stream.on("error", (error) => res.destroy(error));
|
|
801
|
+
stream.pipe(res);
|
|
762
802
|
}
|
|
763
803
|
|
|
764
804
|
// src/vite-plugin-one.ts
|
package/install.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx peachsim` runs the same guided install as
|
|
3
|
+
// `curl -fsSL https://peachsim.com/install.sh | sh`: this package ships that
|
|
4
|
+
// script, and it installs the standalone peach executable, then hands off to
|
|
5
|
+
// `peach setup`.
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const result = spawnSync('sh', [fileURLToPath(new URL('./install.sh', import.meta.url))], {
|
|
10
|
+
stdio: 'inherit',
|
|
11
|
+
})
|
|
12
|
+
if (result.error) {
|
|
13
|
+
process.stderr.write(`error: could not run the peach installer: ${result.error.message}\n`)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
process.exit(result.status ?? 1)
|
package/install.sh
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
# peach installer
|
|
5
|
+
#
|
|
6
|
+
# this installs the standalone peach executable and hands machine and repository
|
|
7
|
+
# decisions to `peach setup`. it does not require node, bun, npm, or sudo.
|
|
8
|
+
#
|
|
9
|
+
# inspect this entire script without running it:
|
|
10
|
+
#
|
|
11
|
+
# curl -fsSL https://peachsim.com/install.sh | less
|
|
12
|
+
#
|
|
13
|
+
# install:
|
|
14
|
+
#
|
|
15
|
+
# curl -fsSL https://peachsim.com/install.sh | sh
|
|
16
|
+
#
|
|
17
|
+
# options:
|
|
18
|
+
# PEACH_VERSION=0.1.377 install a specific cli version
|
|
19
|
+
# PEACH_CLI_PREFIX=... choose the user-writable cli prefix
|
|
20
|
+
# PEACH_NO_MODIFY_PATH=1 do not update your shell profile
|
|
21
|
+
|
|
22
|
+
PEACH_VERSION="${PEACH_VERSION:-latest}"
|
|
23
|
+
CLI_PREFIX="${PEACH_CLI_PREFIX:-$HOME/.local}"
|
|
24
|
+
CLI="$CLI_PREFIX/bin/peach"
|
|
25
|
+
RELEASE_ORIGIN="https://r2.rnxsim.com/rnx-cli"
|
|
26
|
+
|
|
27
|
+
say() {
|
|
28
|
+
printf '%s\n' "$*"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fail() {
|
|
32
|
+
printf 'error: %s\n' "$*" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
command_exists() {
|
|
37
|
+
command -v "$1" >/dev/null 2>&1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
command_exists curl || fail "curl is required to install peach"
|
|
41
|
+
|
|
42
|
+
case "$(uname -s)" in
|
|
43
|
+
Darwin)
|
|
44
|
+
PLATFORM="darwin"
|
|
45
|
+
PLATFORM_LABEL="macOS"
|
|
46
|
+
;;
|
|
47
|
+
Linux)
|
|
48
|
+
PLATFORM="linux"
|
|
49
|
+
PLATFORM_LABEL="Linux"
|
|
50
|
+
;;
|
|
51
|
+
*)
|
|
52
|
+
fail "this installer currently supports macOS and Linux"
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
case "$(uname -m)" in
|
|
57
|
+
arm64 | aarch64)
|
|
58
|
+
ARCH="arm64"
|
|
59
|
+
;;
|
|
60
|
+
x86_64 | amd64)
|
|
61
|
+
ARCH="x64"
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
fail "peach does not support this processor architecture"
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
if [ "$PEACH_VERSION" = "latest" ]; then
|
|
69
|
+
PEACH_VERSION="$(curl -fsSL "$RELEASE_ORIGIN/channels/stable")" ||
|
|
70
|
+
fail "could not resolve the current peach version"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
printf '%s' "$PEACH_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' ||
|
|
74
|
+
fail "PEACH_VERSION must be 'latest' or a semantic version"
|
|
75
|
+
|
|
76
|
+
ARTIFACT="peach-$PLATFORM-$ARCH"
|
|
77
|
+
ARTIFACT_URL="$RELEASE_ORIGIN/$PEACH_VERSION/$ARTIFACT"
|
|
78
|
+
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/peach-install.XXXXXX")"
|
|
79
|
+
TMP_BIN="$TMP_DIR/peach"
|
|
80
|
+
TMP_SHA="$TMP_DIR/peach.sha256"
|
|
81
|
+
cleanup() {
|
|
82
|
+
rm -rf "$TMP_DIR"
|
|
83
|
+
}
|
|
84
|
+
trap cleanup EXIT HUP INT TERM
|
|
85
|
+
|
|
86
|
+
say ""
|
|
87
|
+
say "peach installer"
|
|
88
|
+
say ""
|
|
89
|
+
say " platform: $PLATFORM_LABEL $ARCH"
|
|
90
|
+
say " version: $PEACH_VERSION"
|
|
91
|
+
say " command: $CLI"
|
|
92
|
+
say ""
|
|
93
|
+
say "Downloading the standalone peach executable..."
|
|
94
|
+
curl -fsSL "$ARTIFACT_URL" -o "$TMP_BIN" || fail "could not download peach"
|
|
95
|
+
curl -fsSL "$ARTIFACT_URL.sha256" -o "$TMP_SHA" ||
|
|
96
|
+
fail "could not download the peach checksum"
|
|
97
|
+
|
|
98
|
+
EXPECTED_SHA="$(tr -d '[:space:]' <"$TMP_SHA")"
|
|
99
|
+
printf '%s' "$EXPECTED_SHA" | grep -Eq '^[a-f0-9]{64}$' ||
|
|
100
|
+
fail "the peach checksum is invalid"
|
|
101
|
+
if [ "$PLATFORM" = "darwin" ]; then
|
|
102
|
+
ACTUAL_SHA="$(shasum -a 256 "$TMP_BIN" | awk '{print $1}')"
|
|
103
|
+
else
|
|
104
|
+
ACTUAL_SHA="$(sha256sum "$TMP_BIN" | awk '{print $1}')"
|
|
105
|
+
fi
|
|
106
|
+
[ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] || fail "the peach executable failed verification"
|
|
107
|
+
chmod 755 "$TMP_BIN"
|
|
108
|
+
VERSION_OUTPUT="$("$TMP_BIN" --version 2>/dev/null)" ||
|
|
109
|
+
fail "the downloaded peach executable did not start"
|
|
110
|
+
REPORTED_VERSION="$(
|
|
111
|
+
printf '%s\n' "$VERSION_OUTPUT" |
|
|
112
|
+
sed -n '1s/^peach v\([^[:space:]]*\).*$/\1/p'
|
|
113
|
+
)"
|
|
114
|
+
[ "$REPORTED_VERSION" = "$PEACH_VERSION" ] ||
|
|
115
|
+
fail "the downloaded peach executable reported the wrong version"
|
|
116
|
+
|
|
117
|
+
mkdir -p "$CLI_PREFIX/bin"
|
|
118
|
+
NEXT_CLI="$CLI.next.$$"
|
|
119
|
+
cp "$TMP_BIN" "$NEXT_CLI"
|
|
120
|
+
mv -f "$NEXT_CLI" "$CLI"
|
|
121
|
+
|
|
122
|
+
update_shell_profile() {
|
|
123
|
+
profile="$1"
|
|
124
|
+
shell_name="$2"
|
|
125
|
+
path_line="export PATH=\"$CLI_PREFIX/bin:\$PATH\""
|
|
126
|
+
integration=""
|
|
127
|
+
if [ -n "$shell_name" ]; then
|
|
128
|
+
integration="eval \"\$(command peach __shell-init $shell_name)\""
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
add_path=0
|
|
132
|
+
if ! { [ -f "$profile" ] && grep -F "$path_line" "$profile" >/dev/null 2>&1; }; then
|
|
133
|
+
add_path=1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
add_integration=0
|
|
137
|
+
if [ -n "$integration" ] &&
|
|
138
|
+
! { [ -f "$profile" ] && grep -F "$integration" "$profile" >/dev/null 2>&1; }; then
|
|
139
|
+
add_integration=1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if [ "$add_path" = 0 ] && [ "$add_integration" = 0 ]; then
|
|
143
|
+
return 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
printf '\n'
|
|
148
|
+
printf '# added by the peach installer; delete this block to remove peach from your shell\n'
|
|
149
|
+
if [ "$add_path" = 1 ]; then
|
|
150
|
+
printf '%s\n' "$path_line"
|
|
151
|
+
fi
|
|
152
|
+
if [ "$add_integration" = 1 ]; then
|
|
153
|
+
printf '%s\n' "$integration"
|
|
154
|
+
fi
|
|
155
|
+
} >>"$profile"
|
|
156
|
+
|
|
157
|
+
if [ "$add_path" = 1 ]; then
|
|
158
|
+
say "Added $CLI_PREFIX/bin to PATH in $profile"
|
|
159
|
+
fi
|
|
160
|
+
if [ "$add_integration" = 1 ]; then
|
|
161
|
+
say "Added Peach shell integration to $profile"
|
|
162
|
+
fi
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if [ "${PEACH_NO_MODIFY_PATH:-0}" = "1" ]; then
|
|
166
|
+
case ":$PATH:" in
|
|
167
|
+
*":$CLI_PREFIX/bin:"*) ;;
|
|
168
|
+
*)
|
|
169
|
+
say ""
|
|
170
|
+
say "$CLI_PREFIX/bin is not currently on PATH. Add it with:"
|
|
171
|
+
say " export PATH=\"$CLI_PREFIX/bin:\$PATH\""
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
case "${SHELL:-}" in
|
|
175
|
+
*/zsh) say " eval \"\$(command peach __shell-init zsh)\"" ;;
|
|
176
|
+
*/bash) say " eval \"\$(command peach __shell-init bash)\"" ;;
|
|
177
|
+
esac
|
|
178
|
+
else
|
|
179
|
+
case "${SHELL:-}" in
|
|
180
|
+
*/zsh) update_shell_profile "$HOME/.zshrc" zsh ;;
|
|
181
|
+
*/bash) update_shell_profile "$HOME/.bashrc" bash ;;
|
|
182
|
+
*) update_shell_profile "$HOME/.profile" "" ;;
|
|
183
|
+
esac
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
PATH="$CLI_PREFIX/bin:$PATH"
|
|
187
|
+
export PATH
|
|
188
|
+
|
|
189
|
+
say ""
|
|
190
|
+
say "peach is installed."
|
|
191
|
+
say ""
|
|
192
|
+
if [ -t 1 ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then
|
|
193
|
+
say "Continuing with guided setup..."
|
|
194
|
+
"$CLI" setup </dev/tty
|
|
195
|
+
else
|
|
196
|
+
say "Run this deterministic setup command in CI:"
|
|
197
|
+
say ""
|
|
198
|
+
say " peach setup --ci --yes --json"
|
|
199
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peachsim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.628",
|
|
4
4
|
"description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
|
|
5
5
|
"author": "Tamagui LLC",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"peachsim": "./install.mjs"
|
|
18
|
+
},
|
|
16
19
|
"types": "./src/index.ts",
|
|
17
20
|
"exports": {
|
|
18
21
|
".": {
|
|
@@ -267,6 +270,8 @@
|
|
|
267
270
|
"./package.json": "./package.json"
|
|
268
271
|
},
|
|
269
272
|
"files": [
|
|
273
|
+
"install.mjs",
|
|
274
|
+
"install.sh",
|
|
270
275
|
"dist-lib/",
|
|
271
276
|
"src/",
|
|
272
277
|
"cli/",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// zero HTTP requests; probes only fire when a port appears, disappears, or its
|
|
14
14
|
// pid changes. before any HTTP, a TCP connect gate skips unreachable ports.
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { spawn } from 'child_process'
|
|
17
|
+
import { terminateProcessGroup } from '../../contrast/src/runtime/process-group'
|
|
17
18
|
import { readlink } from 'fs/promises'
|
|
18
19
|
import http from 'http'
|
|
19
20
|
import net from 'net'
|
|
@@ -37,52 +38,66 @@ import {
|
|
|
37
38
|
import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
|
|
38
39
|
import type { AppSettingsSpecifier } from '@peach/globals'
|
|
39
40
|
|
|
40
|
-
// how long past a command's own budget we wait before deciding its exit event is
|
|
41
|
-
// never coming. generous, because this only fires when the child is already
|
|
42
|
-
// overdue and we are choosing between a wrong answer and a hang.
|
|
43
|
-
const EXEC_EXIT_GRACE_MS = 2_000
|
|
44
|
-
|
|
45
41
|
/** every subprocess in this file goes through here.
|
|
46
42
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
* with `killed`, which callers already read as could-not-look. */
|
|
57
|
-
async function execGuarded(
|
|
43
|
+
* the command runs under `/bin/sh` as the leader of its own process group, and
|
|
44
|
+
* a timeout kills the whole group. `exec`'s own `timeout` kills only the shell:
|
|
45
|
+
* for `lsof | grep` that orphans `lsof` to launchd, where it keeps walking every
|
|
46
|
+
* open fd on the machine, and a tray rescanning every few seconds on a busy
|
|
47
|
+
* host piled up hundreds of them. the deadline is our own timer, so a lost
|
|
48
|
+
* 'close' event cannot leave the promise unsettled either. a timeout rejects
|
|
49
|
+
* with `killed`, which callers read as could-not-look; a non-zero exit rejects
|
|
50
|
+
* as `exec` did. */
|
|
51
|
+
function execGuarded(
|
|
58
52
|
command: string,
|
|
59
53
|
timeout: number,
|
|
60
54
|
extra: { maxBuffer?: number } = {},
|
|
61
55
|
): Promise<{ stdout: string }> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
child =
|
|
56
|
+
const maxBuffer = extra.maxBuffer ?? 1024 * 1024
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn('/bin/sh', ['-c', command], {
|
|
59
|
+
detached: true,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
})
|
|
62
|
+
let stdout = ''
|
|
63
|
+
let settled = false
|
|
64
|
+
let killing = false
|
|
65
|
+
const settle = (err: Error | null) => {
|
|
66
|
+
if (settled) return
|
|
67
|
+
settled = true
|
|
68
|
+
clearTimeout(deadline)
|
|
65
69
|
if (err) reject(err)
|
|
66
70
|
else resolve({ stdout })
|
|
71
|
+
}
|
|
72
|
+
// a timeout or an overflow settles only once the whole group is gone.
|
|
73
|
+
const killThenSettle = (err: Error) => {
|
|
74
|
+
if (killing) return
|
|
75
|
+
killing = true
|
|
76
|
+
clearTimeout(deadline)
|
|
77
|
+
terminateProcessGroup(child, 'SIGKILL', 0).finally(() => settle(err))
|
|
78
|
+
}
|
|
79
|
+
const deadline = setTimeout(() => {
|
|
80
|
+
killThenSettle(
|
|
81
|
+
Object.assign(new Error(`\`${command}\` did not exit within ${timeout}ms`), {
|
|
82
|
+
killed: true,
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
}, timeout)
|
|
86
|
+
child.stdout.setEncoding('utf8')
|
|
87
|
+
child.stdout.on('data', (chunk: string) => {
|
|
88
|
+
stdout += chunk
|
|
89
|
+
if (stdout.length > maxBuffer) {
|
|
90
|
+
killThenSettle(new Error(`\`${command}\` exceeded maxBuffer ${maxBuffer}`))
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
child.on('error', (err) => {
|
|
94
|
+
if (!killing) settle(err)
|
|
95
|
+
})
|
|
96
|
+
child.on('close', (code) => {
|
|
97
|
+
if (killing) return
|
|
98
|
+
settle(code === 0 ? null : new Error(`\`${command}\` exited with ${code}`))
|
|
67
99
|
})
|
|
68
100
|
})
|
|
69
|
-
let deadlineTimer: ReturnType<typeof setTimeout> | undefined
|
|
70
|
-
const deadline = new Promise<never>((_resolve, reject) => {
|
|
71
|
-
deadlineTimer = setTimeout(() => {
|
|
72
|
-
child?.kill('SIGKILL')
|
|
73
|
-
const err = new Error(`\`${command}\` did not exit within ${timeout}ms`)
|
|
74
|
-
// matches the shape `wasKilledByTimeout` reads off a real exec timeout, so
|
|
75
|
-
// a lost exit event and a genuine timeout reach callers as the same fact.
|
|
76
|
-
Object.assign(err, { killed: true })
|
|
77
|
-
reject(err)
|
|
78
|
-
}, timeout + EXEC_EXIT_GRACE_MS)
|
|
79
|
-
})
|
|
80
|
-
try {
|
|
81
|
-
return await Promise.race([ran, deadline])
|
|
82
|
-
} finally {
|
|
83
|
-
// the race is settled, so drop the deadline rather than leaving it pending.
|
|
84
|
-
clearTimeout(deadlineTimer)
|
|
85
|
-
}
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
export interface DiscoveredServer {
|
|
@@ -198,6 +213,7 @@ function httpGet(
|
|
|
198
213
|
// first answer wins: the response, the deadline, and the connection closing
|
|
199
214
|
// all race, and every one of them can arrive after another has decided.
|
|
200
215
|
let settled = false
|
|
216
|
+
let killing = false
|
|
201
217
|
const settle = (value: HttpResult | null) => {
|
|
202
218
|
if (settled) return
|
|
203
219
|
settled = true
|
|
@@ -341,11 +357,32 @@ export async function scanListeningProcesses(
|
|
|
341
357
|
excludePorts: number[] = [],
|
|
342
358
|
): Promise<ListeningProcessScan> {
|
|
343
359
|
const excluded = new Set(excludePorts)
|
|
360
|
+
const listing = await listListenersShared()
|
|
361
|
+
if (listing.state === 'could-not-look') return listing
|
|
362
|
+
return {
|
|
363
|
+
state: 'looked',
|
|
364
|
+
processes: listing.processes.filter((p) => acceptPort(p.port, excluded)),
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// every caller (the tray's interval, the gateway, the dev middleware, `peach
|
|
369
|
+
// open`) shares one listing while it runs. a scan slower than the tray's
|
|
370
|
+
// interval would otherwise start a second `lsof` on top of the first.
|
|
371
|
+
let inflightListing: Promise<ListeningProcessScan> | null = null
|
|
372
|
+
|
|
373
|
+
function listListenersShared(): Promise<ListeningProcessScan> {
|
|
374
|
+
inflightListing ??= listListeners().finally(() => {
|
|
375
|
+
inflightListing = null
|
|
376
|
+
})
|
|
377
|
+
return inflightListing
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** every listening node/bun process, one pid per port, before any port filter. */
|
|
381
|
+
async function listListeners(): Promise<ListeningProcessScan> {
|
|
344
382
|
let timedOut = false
|
|
345
383
|
|
|
346
384
|
// lsof layout: COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
|
|
347
385
|
// NAME looks like "*:8081" or "127.0.0.1:8081" for a LISTEN line.
|
|
348
|
-
// async exec — sync blocked CrBrowserMain for ~600ms/scan on macOS.
|
|
349
386
|
try {
|
|
350
387
|
const { stdout } = await execGuarded(
|
|
351
388
|
`lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun)'`,
|
|
@@ -356,11 +393,9 @@ export async function scanListeningProcesses(
|
|
|
356
393
|
const parts = line.trim().split(/\s+/)
|
|
357
394
|
if (parts.length < 9) continue
|
|
358
395
|
const pid = Number(parts[1])
|
|
359
|
-
const
|
|
360
|
-
const m = addr.match(/:(\d+)$/)
|
|
396
|
+
const m = parts[8].match(/:(\d+)$/)
|
|
361
397
|
if (!m) continue
|
|
362
398
|
const port = Number(m[1])
|
|
363
|
-
if (!acceptPort(port, excluded)) continue
|
|
364
399
|
if (!seen.has(port)) seen.set(port, pid)
|
|
365
400
|
}
|
|
366
401
|
// lsof ran. an empty result is a real answer: nothing is listening.
|
|
@@ -385,7 +420,6 @@ export async function scanListeningProcesses(
|
|
|
385
420
|
if (!portMatch) continue
|
|
386
421
|
const port = Number(portMatch[1])
|
|
387
422
|
const pid = pidMatch ? Number(pidMatch[1]) : 0
|
|
388
|
-
if (!acceptPort(port, excluded)) continue
|
|
389
423
|
if (!seen.has(port)) seen.set(port, pid)
|
|
390
424
|
}
|
|
391
425
|
return {
|
|
@@ -402,10 +436,7 @@ export async function scanListeningProcesses(
|
|
|
402
436
|
|
|
403
437
|
// neither tool exists here, so probe the conventional bundler ports directly.
|
|
404
438
|
// that is a real (if narrow) look, not a substitute for one.
|
|
405
|
-
return {
|
|
406
|
-
state: 'looked',
|
|
407
|
-
processes: FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded)),
|
|
408
|
-
}
|
|
439
|
+
return { state: 'looked', processes: FALLBACK_PORTS }
|
|
409
440
|
}
|
|
410
441
|
|
|
411
442
|
// kept for backwards compatibility with any external callers that only need
|
package/skills/contrast/SKILL.md
CHANGED
|
@@ -35,7 +35,7 @@ automation, inspecting the render tree, and managing sims.
|
|
|
35
35
|
| `peach do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
|
|
36
36
|
| `peach wait` | block until the runtime reaches a known state |
|
|
37
37
|
| `peach shell` | drive shell state and simulator chrome |
|
|
38
|
-
| `peach mode` | toggle screenshot and 3D display modes |
|
|
38
|
+
| `peach mode` | toggle screenshot, touch indicators, and 3D display modes |
|
|
39
39
|
| `peach device` | inspect or change the live device preset for a sim |
|
|
40
40
|
| `peach network` | inspect live network traffic from the running peach worker |
|
|
41
41
|
| `peach logs` | inspect live console output from the running peach worker |
|
package/src/bridge-constants.ts
CHANGED
|
@@ -173,3 +173,14 @@ export function isSootsimBridgeHealth(value: unknown, expectedPort: number): boo
|
|
|
173
173
|
// loop; playwright-owned sims use it to close their hosting context.
|
|
174
174
|
export const SOOTSIM_BRIDGE_SIM_CLOSE_CODE = 4001
|
|
175
175
|
export const SOOTSIM_BRIDGE_SIM_CLOSE_REASON = 'sootsim close'
|
|
176
|
+
|
|
177
|
+
// automation sims (spawned by the playwright driver: `peach open --driver
|
|
178
|
+
// playwright`, the factory/screenshot capture path, `meta.sootsimHostDriver
|
|
179
|
+
// === 'playwright'`) are unattended browsers, never a human's tab, and an idle
|
|
180
|
+
// one still renders at full rate (the host disables background throttling so
|
|
181
|
+
// captures see real motion). only a cli proves one is in use: attached, which
|
|
182
|
+
// covers a long in-flight command such as a recording, or a command within
|
|
183
|
+
// this window. past it the bridge reaper closes the sim socket with the code
|
|
184
|
+
// above, the page asks its host to close, and the host exits with its chrome
|
|
185
|
+
// tree.
|
|
186
|
+
export const AUTOMATION_SIM_IDLE_REAP_TTL_MS = 5 * 60_000
|
package/src/capture-contract.ts
CHANGED
|
@@ -164,6 +164,7 @@ export interface RnxRasterRegionTree {
|
|
|
164
164
|
nodeId: number
|
|
165
165
|
parentNodeId: number | null
|
|
166
166
|
type: string
|
|
167
|
+
props?: Record<string, unknown>
|
|
167
168
|
text?: string | null
|
|
168
169
|
style: Record<string, unknown>
|
|
169
170
|
layout: RnxCaptureRect
|
|
@@ -173,14 +174,33 @@ export interface RnxRasterRegionTree {
|
|
|
173
174
|
}[]
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
function isLiveRasterSource(node: RnxRasterRegionTree['nodes'][number]): boolean {
|
|
178
|
+
const source = node.props?.source
|
|
179
|
+
return (
|
|
180
|
+
node.type === 'image' &&
|
|
181
|
+
!!source &&
|
|
182
|
+
typeof source === 'object' &&
|
|
183
|
+
'uri' in source &&
|
|
184
|
+
typeof source.uri === 'string' &&
|
|
185
|
+
source.uri.startsWith('live:')
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
|
|
177
190
|
const nodesById = new Map(tree.nodes.map((node) => [node.nodeId, node]))
|
|
178
191
|
const unsupported = new Set<number>()
|
|
179
192
|
for (const node of tree.nodes) {
|
|
180
|
-
const
|
|
193
|
+
const liveVideo = isLiveRasterSource(node)
|
|
194
|
+
const visibleFrame =
|
|
195
|
+
liveVideo &&
|
|
196
|
+
node.visibleFrame &&
|
|
197
|
+
(node.visibleFrame.width <= 0 || node.visibleFrame.height <= 0)
|
|
198
|
+
? node.frame
|
|
199
|
+
: (node.visibleFrame ?? node.frame)
|
|
181
200
|
if (!visibleFrame || visibleFrame.width <= 0 || visibleFrame.height <= 0) continue
|
|
182
201
|
if (
|
|
183
202
|
node.type === 'masked-view' ||
|
|
203
|
+
liveVideo ||
|
|
184
204
|
(node.type === 'text' &&
|
|
185
205
|
typeof node.text === 'string' &&
|
|
186
206
|
(/\p{Emoji_Presentation}/u.test(node.text) ||
|
|
@@ -208,21 +228,33 @@ export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
|
|
|
208
228
|
|
|
209
229
|
const screenArea = tree.width * tree.height
|
|
210
230
|
const maxLayerArea = screenArea * 0.25
|
|
211
|
-
const maxAtlasArea = screenArea * 0.5
|
|
212
231
|
const candidates = [...unsupported]
|
|
213
232
|
.map((nodeId) => {
|
|
214
233
|
const node = nodesById.get(nodeId)
|
|
215
|
-
|
|
216
|
-
|
|
234
|
+
if (!node) return null
|
|
235
|
+
const liveVideo = isLiveRasterSource(node)
|
|
236
|
+
const frame =
|
|
237
|
+
liveVideo &&
|
|
238
|
+
node.visibleFrame &&
|
|
239
|
+
(node.visibleFrame.width <= 0 || node.visibleFrame.height <= 0)
|
|
240
|
+
? node.frame
|
|
241
|
+
: (node.visibleFrame ?? node.frame)
|
|
242
|
+
return frame ? { nodeId, area: frame.width * frame.height, liveVideo } : null
|
|
217
243
|
})
|
|
218
244
|
.filter(
|
|
219
|
-
(candidate): candidate is { nodeId: number; area: number } =>
|
|
220
|
-
candidate !== null && candidate.area <= maxLayerArea,
|
|
245
|
+
(candidate): candidate is { nodeId: number; area: number; liveVideo: boolean } =>
|
|
246
|
+
candidate !== null && (candidate.liveVideo || candidate.area <= maxLayerArea),
|
|
247
|
+
)
|
|
248
|
+
.sort(
|
|
249
|
+
(left, right) =>
|
|
250
|
+
Number(right.liveVideo) - Number(left.liveVideo) ||
|
|
251
|
+
left.area - right.area ||
|
|
252
|
+
left.nodeId - right.nodeId,
|
|
221
253
|
)
|
|
222
|
-
.sort((left, right) => left.area - right.area || left.nodeId - right.nodeId)
|
|
223
254
|
const selected = new Set<number>()
|
|
224
255
|
let atlasArea = 0
|
|
225
256
|
for (const candidate of candidates) {
|
|
257
|
+
const maxAtlasArea = screenArea * (candidate.liveVideo ? 1 : 0.5)
|
|
226
258
|
if (atlasArea + candidate.area > maxAtlasArea) continue
|
|
227
259
|
selected.add(candidate.nodeId)
|
|
228
260
|
atlasArea += candidate.area
|
|
@@ -345,6 +377,7 @@ export function rnxCaptureRasterRegions(
|
|
|
345
377
|
nodeId: node.nodeId,
|
|
346
378
|
parentNodeId: node.parentNodeId,
|
|
347
379
|
type: node.type,
|
|
380
|
+
props: node.props,
|
|
348
381
|
text: node.text,
|
|
349
382
|
style: node.style.resolved,
|
|
350
383
|
layout: node.layout.local,
|
|
@@ -20,7 +20,7 @@ export interface ResolvedDevBundle {
|
|
|
20
20
|
|
|
21
21
|
function buildUnresolvedTargetError(targetLabel: string): Error {
|
|
22
22
|
return new Error(
|
|
23
|
-
`could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`,
|
|
23
|
+
`could not resolve a native bundle for ${targetLabel}. start the app's dev server (for example npx expo start), pass an explicit bundle URL, or open Connect and choose the app there.`,
|
|
24
24
|
)
|
|
25
25
|
}
|
|
26
26
|
|