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/cli/commands/inspect.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { getCliIdentityKey } from '../current-sim'
|
|
|
17
17
|
import { rememberFlowCandidate } from '../flow-session'
|
|
18
18
|
import { maybeHint } from '../hints'
|
|
19
19
|
import { shouldPrintInspectNotice } from '../inspect-notice-state'
|
|
20
|
+
import { levenshtein } from '../levenshtein'
|
|
20
21
|
import { TOP_LEVEL_RUNTIME_COMMANDS } from '../parse-args'
|
|
21
22
|
import { rethrowIfExit, rnxExit } from '../run-rnx'
|
|
22
23
|
import {
|
|
@@ -243,34 +244,6 @@ function scoreIdSimilarity(needle: string, candidate: string): number {
|
|
|
243
244
|
return levenshtein(needle, candidate) - prefix
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
function levenshtein(a: string, b: string): number {
|
|
247
|
-
if (a === b) return 0
|
|
248
|
-
if (!a.length) return b.length
|
|
249
|
-
if (!b.length) return a.length
|
|
250
|
-
// standard two-row dp: prev[] is the previous row, cur[] is the row being
|
|
251
|
-
// computed; swap pointers per outer iteration. the earlier in-place rolling
|
|
252
|
-
// version overwrote prev[j-1] twice per cell and never set prev[0]=i, so
|
|
253
|
-
// distance(`kitten`,`sitting`) returned 8 instead of 3 and the fuzzy-suggest
|
|
254
|
-
// ranking was effectively random.
|
|
255
|
-
let prev = new Array<number>(b.length + 1)
|
|
256
|
-
let cur = new Array<number>(b.length + 1)
|
|
257
|
-
for (let j = 0; j <= b.length; j++) prev[j] = j
|
|
258
|
-
for (let i = 1; i <= a.length; i++) {
|
|
259
|
-
cur[0] = i
|
|
260
|
-
for (let j = 1; j <= b.length; j++) {
|
|
261
|
-
cur[j] = Math.min(
|
|
262
|
-
prev[j] + 1,
|
|
263
|
-
cur[j - 1] + 1,
|
|
264
|
-
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
const tmp = prev
|
|
268
|
-
prev = cur
|
|
269
|
-
cur = tmp
|
|
270
|
-
}
|
|
271
|
-
return prev[b.length]
|
|
272
|
-
}
|
|
273
|
-
|
|
274
247
|
function formatNetworkStatus(e: NetworkEntryPayload): string {
|
|
275
248
|
if (e.error) return 'err'
|
|
276
249
|
if (e.status == null) return ' … '
|
|
@@ -3539,7 +3512,16 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
3539
3512
|
if (reloadResult && reloadResult.engineStale) {
|
|
3540
3513
|
console.log(' engine build changed since page load — full page reload')
|
|
3541
3514
|
}
|
|
3542
|
-
} catch {
|
|
3515
|
+
} catch (error) {
|
|
3516
|
+
// no sim at all is not a reload in flight: report it rather than
|
|
3517
|
+
// waiting out the reconnect budget.
|
|
3518
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
3519
|
+
if (
|
|
3520
|
+
message === 'no sim connected' ||
|
|
3521
|
+
message.startsWith('could not connect to ws://')
|
|
3522
|
+
) {
|
|
3523
|
+
throw error
|
|
3524
|
+
}
|
|
3543
3525
|
// the sim may already be in the middle of reloading; fall through to reconnect wait
|
|
3544
3526
|
}
|
|
3545
3527
|
console.log(' reloading...')
|
|
@@ -3623,7 +3605,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
3623
3605
|
// ignore errors checking for errors
|
|
3624
3606
|
}
|
|
3625
3607
|
}
|
|
3626
|
-
if (
|
|
3608
|
+
if (!readyResult?.ready) {
|
|
3627
3609
|
rnxExit(1)
|
|
3628
3610
|
}
|
|
3629
3611
|
break
|
|
@@ -4655,15 +4637,18 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4655
4637
|
// single 15s timeout into ~60s of apparent hang (QA F19-1). there is
|
|
4656
4638
|
// nothing useful to fetch from a sim that just timed out, so skip them.
|
|
4657
4639
|
const timedOut = /^command timed out after (\d+)s$/.exec(message)
|
|
4658
|
-
// a disconnected
|
|
4659
|
-
//
|
|
4640
|
+
// a disconnected bridge is genuinely gone; recover is right and probing
|
|
4641
|
+
// it would just re-incur a full timeout. no bridge at all means there is
|
|
4642
|
+
// no sim to recover, only one to open.
|
|
4660
4643
|
const disconnected =
|
|
4661
4644
|
message.startsWith('sim disconnected:') ||
|
|
4662
|
-
message.startsWith('bridge never reconnected')
|
|
4663
|
-
message.startsWith('could not connect to ws://')
|
|
4645
|
+
message.startsWith('bridge never reconnected')
|
|
4664
4646
|
if (unknownSim) {
|
|
4665
4647
|
await printUnknownSimHint(bridge, wsPort, unknownSim[1])
|
|
4666
|
-
} else if (
|
|
4648
|
+
} else if (
|
|
4649
|
+
/^no sim connected$/.test(message) ||
|
|
4650
|
+
message.startsWith('could not connect to ws://')
|
|
4651
|
+
) {
|
|
4667
4652
|
printMissingSimHint(wsPort)
|
|
4668
4653
|
} else if (timedOut) {
|
|
4669
4654
|
// a command timeout alone does not mean a wedged sim — a large screen
|
|
@@ -4677,9 +4662,9 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4677
4662
|
process.stderr.write(
|
|
4678
4663
|
` the sim is still responsive — '${cmd}' just exceeded the ${timedOut[1]}s command budget.\n` +
|
|
4679
4664
|
` the screen's node tree is large; narrow the query or raise the budget:\n` +
|
|
4680
|
-
` peach
|
|
4665
|
+
` peach ${cmd} --subtree <id> # scope to one subtree\n` +
|
|
4681
4666
|
` peach find --testid <id> # targeted single-node lookup\n` +
|
|
4682
|
-
` peach
|
|
4667
|
+
` peach ${cmd} --timeout 60000 # raise per-command budget (ms)\n`,
|
|
4683
4668
|
)
|
|
4684
4669
|
} else {
|
|
4685
4670
|
printRecoverHint()
|
|
@@ -4,6 +4,7 @@ import { mkdtempSync, writeFileSync } from 'fs'
|
|
|
4
4
|
import { tmpdir } from 'os'
|
|
5
5
|
import { join } from 'path'
|
|
6
6
|
import yaml from 'yaml'
|
|
7
|
+
import { isRecord } from '../../src/is-record'
|
|
7
8
|
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
8
9
|
import { registerRun, stepSummaryFromTrace } from '../run-registry'
|
|
9
10
|
import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
} from './flow'
|
|
19
20
|
import { inspectDescribe } from './inspect/core'
|
|
20
21
|
import { resolveDefaultUploadOrigin } from './upload'
|
|
21
|
-
import { isRecord } from '../../src/is-record'
|
|
22
22
|
|
|
23
23
|
const DEFAULT_TEST_MODEL = process.env.PEACH_TEST_MODEL || 'deepseek-v4-flash'
|
|
24
24
|
const DESCRIBE_CONTEXT_LIMIT = 18_000
|
|
@@ -239,14 +239,7 @@ export function buildFlowArgs(
|
|
|
239
239
|
flowPath: string,
|
|
240
240
|
origin: string,
|
|
241
241
|
) {
|
|
242
|
-
const out = [
|
|
243
|
-
flowPath,
|
|
244
|
-
'--preview',
|
|
245
|
-
'--preview-origin',
|
|
246
|
-
origin,
|
|
247
|
-
'--billing-kind',
|
|
248
|
-
'test_run',
|
|
249
|
-
]
|
|
242
|
+
const out = [flowPath, '--preview', '--preview-origin', origin]
|
|
250
243
|
for (let i = 0; i < args.length; i++) {
|
|
251
244
|
if (i === promptIndex) continue
|
|
252
245
|
const arg = args[i]
|
|
@@ -285,10 +278,10 @@ export function findUnexpectedGenerateArgs(
|
|
|
285
278
|
if (i === promptIndex) continue
|
|
286
279
|
const arg = args[i]
|
|
287
280
|
if (!arg) continue
|
|
288
|
-
//
|
|
289
|
-
//
|
|
281
|
+
// a generated flow's recording is an ordinary preview upload; the only
|
|
282
|
+
// billing kind is a hosted test job's, which the recorder sets.
|
|
290
283
|
if (arg === '--billing-kind') {
|
|
291
|
-
problems.push({ arg, message: `${arg} is
|
|
284
|
+
problems.push({ arg, message: `${arg} is only for hosted test jobs` })
|
|
292
285
|
i += 1
|
|
293
286
|
continue
|
|
294
287
|
}
|
package/cli/commands/maestro.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { RnxExit } from '../run-rnx'
|
|
|
26
26
|
import {
|
|
27
27
|
findUnexpectedFlowArgs,
|
|
28
28
|
FLOW_ARG_VALUE_FLAGS,
|
|
29
|
+
getLastFlowPreviewUploadError,
|
|
29
30
|
getLastFlowPreviewUploadResult,
|
|
30
31
|
getLastFlowTraceSteps,
|
|
31
32
|
hoistLeadingSimFlag,
|
|
@@ -728,6 +729,10 @@ export async function runMaestro(
|
|
|
728
729
|
console.error(` error: ${target} not found`)
|
|
729
730
|
return 1
|
|
730
731
|
}
|
|
732
|
+
// cwd is the real path, so name every flow from the target's real path too:
|
|
733
|
+
// a target under a symlink (macOS /tmp, /var/folders) otherwise reports as
|
|
734
|
+
// ../../../var/folders/... instead of flows/login.yaml.
|
|
735
|
+
target = fs.realpathSync(target)
|
|
731
736
|
|
|
732
737
|
let files: string[]
|
|
733
738
|
let workspaceConfig: MaestroWorkspaceConfig = {}
|
|
@@ -886,24 +891,29 @@ export async function runMaestro(
|
|
|
886
891
|
// the upload failed, so the flow never disappears from the collected
|
|
887
892
|
// entries.
|
|
888
893
|
if (args.includes('--preview')) {
|
|
894
|
+
const uploaded = Boolean(upload?.shareId || upload?.previewUrl)
|
|
889
895
|
const previewPayload = {
|
|
890
896
|
flow: path.relative(cwd, file) || file,
|
|
891
897
|
previewId: upload?.shareId ?? null,
|
|
892
898
|
previewUrl: upload?.previewUrl ?? null,
|
|
893
|
-
// the failing step as the junit report names it, on one line
|
|
899
|
+
// the failing step as the junit report names it, on one line, else
|
|
900
|
+
// why the required recording upload failed
|
|
894
901
|
failure:
|
|
895
|
-
exit === 0
|
|
902
|
+
exit === 0 && uploaded
|
|
896
903
|
? null
|
|
897
|
-
: failureFromTraceSteps(
|
|
898
|
-
|
|
899
|
-
|
|
904
|
+
: failureFromTraceSteps(
|
|
905
|
+
traceSteps,
|
|
906
|
+
uploaded
|
|
907
|
+
? 'flow playback failed'
|
|
908
|
+
: `recording upload failed: ${getLastFlowPreviewUploadError() ?? 'no preview returned'}`,
|
|
909
|
+
).body.split('\n')[0],
|
|
900
910
|
}
|
|
901
911
|
try {
|
|
902
912
|
for (const name of cloudTestResultFileNames('preview', flowIndex, files.length)) {
|
|
903
913
|
fs.writeFileSync(path.join(cwd, name), JSON.stringify(previewPayload))
|
|
904
914
|
}
|
|
905
915
|
} catch {}
|
|
906
|
-
if (!
|
|
916
|
+
if (!uploaded) {
|
|
907
917
|
console.error(' error: required hosted recording upload failed')
|
|
908
918
|
if (exit === 0) exit = 1
|
|
909
919
|
if (worstExit === 0) worstExit = 1
|
package/cli/commands/mode.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { rnxExit } from '../run-rnx'
|
|
2
2
|
import { runScreenshotMode } from './screenshot-mode'
|
|
3
|
+
import { runShowTouchesMode } from './show-touches-mode'
|
|
3
4
|
import { runThreeMode } from './three-mode'
|
|
4
5
|
|
|
5
6
|
interface ModeOptions {
|
|
@@ -8,7 +9,7 @@ interface ModeOptions {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const VALUE_FLAGS = new Set(['--sim', '--port', '-p'])
|
|
11
|
-
const SUBCOMMANDS = new Set(['screenshot', 'three'])
|
|
12
|
+
const SUBCOMMANDS = new Set(['screenshot', 'three', 'show-touches', 'touches'])
|
|
12
13
|
|
|
13
14
|
function findSubcommand(args: string[]): { name: string; index: number } | null {
|
|
14
15
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -32,21 +33,20 @@ peach mode — toggle live display modes
|
|
|
32
33
|
|
|
33
34
|
usage:
|
|
34
35
|
peach mode screenshot [on|off|toggle]
|
|
36
|
+
peach mode show-touches [on|off|toggle]
|
|
35
37
|
peach mode three [on|off|toggle]
|
|
36
38
|
peach mode three configure [options]
|
|
37
39
|
`)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export async function runMode(args: string[], opts: ModeOptions): Promise<void> {
|
|
41
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
42
|
-
printHelp()
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
43
|
const sub = findSubcommand(args)
|
|
47
44
|
if (!sub) {
|
|
48
45
|
printHelp()
|
|
49
|
-
|
|
46
|
+
if (!args.includes('--help') && !args.includes('-h')) {
|
|
47
|
+
rnxExit(1)
|
|
48
|
+
}
|
|
49
|
+
return
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const rest = withoutSubcommand(args, sub.index)
|
|
@@ -54,5 +54,9 @@ export async function runMode(args: string[], opts: ModeOptions): Promise<void>
|
|
|
54
54
|
await runScreenshotMode(rest, opts)
|
|
55
55
|
return
|
|
56
56
|
}
|
|
57
|
+
if (sub.name === 'show-touches' || sub.name === 'touches') {
|
|
58
|
+
await runShowTouchesMode(rest, opts)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
57
61
|
await runThreeMode(rest, opts)
|
|
58
62
|
}
|
package/cli/commands/platform.ts
CHANGED
|
@@ -454,6 +454,7 @@ async function runRemoteBox(
|
|
|
454
454
|
}
|
|
455
455
|
const created = await box.createPrebuiltBundleBox({
|
|
456
456
|
bundle: new Uint8Array(artifact.bytes),
|
|
457
|
+
device: selectedDevice,
|
|
457
458
|
name: input.name ?? boxNameForBundle(input.bundlePath),
|
|
458
459
|
endpoint,
|
|
459
460
|
apiOrigin: box.cloudBoxApiOrigin(args, auth),
|
|
@@ -260,7 +260,7 @@ export async function runReportIssue(
|
|
|
260
260
|
return 2
|
|
261
261
|
}
|
|
262
262
|
const approved = await dependencies.confirmSubmission(
|
|
263
|
-
'send this compatibility report to the peach team
|
|
263
|
+
'send this compatibility report to the peach team?',
|
|
264
264
|
false,
|
|
265
265
|
)
|
|
266
266
|
if (!approved) {
|
package/cli/commands/setup.ts
CHANGED
|
@@ -54,6 +54,7 @@ interface SetupOptions {
|
|
|
54
54
|
|
|
55
55
|
interface SetupRunOptions {
|
|
56
56
|
launchConnectRN?: boolean
|
|
57
|
+
port?: number
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface SetupResult {
|
|
@@ -120,6 +121,7 @@ export async function runSetup(
|
|
|
120
121
|
channel: 'stable',
|
|
121
122
|
setActive: true,
|
|
122
123
|
protectVersions: activeRuntime ? [activeRuntime] : [],
|
|
124
|
+
quiet: !options.json,
|
|
123
125
|
})
|
|
124
126
|
const result = options.json
|
|
125
127
|
? await install()
|
|
@@ -130,10 +132,16 @@ export async function runSetup(
|
|
|
130
132
|
let serviceInstalled = serviceStatus.installed
|
|
131
133
|
if (service && !serviceStatus.installed && !options.dryRun) {
|
|
132
134
|
if (options.json) {
|
|
133
|
-
await daemonInstall({
|
|
135
|
+
await daemonInstall({
|
|
136
|
+
port: runOptions.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
137
|
+
force: false,
|
|
138
|
+
})
|
|
134
139
|
} else {
|
|
135
140
|
console.log(`\n ${accent('◆')} ${bold('Enabling the background service')}`)
|
|
136
|
-
await daemonInstall({
|
|
141
|
+
await daemonInstall({
|
|
142
|
+
port: runOptions.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
143
|
+
force: false,
|
|
144
|
+
})
|
|
137
145
|
}
|
|
138
146
|
serviceInstalled = true
|
|
139
147
|
}
|
|
@@ -144,13 +152,10 @@ export async function runSetup(
|
|
|
144
152
|
|
|
145
153
|
if (launchConnectRN) {
|
|
146
154
|
console.log(`\n ${accent('◆')} Opening ConnectRN`)
|
|
147
|
-
await runOpenCommand(
|
|
148
|
-
'ConnectRN',
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
'--no-describe',
|
|
152
|
-
'--quiet',
|
|
153
|
-
])
|
|
155
|
+
await runOpenCommand(
|
|
156
|
+
['ConnectRN', '--driver', simulatorDriver, '--no-describe', '--quiet'],
|
|
157
|
+
{ port: runOptions.port },
|
|
158
|
+
)
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
const productAnalytics = await decidePrivacy(options, privacy)
|
|
@@ -324,7 +329,7 @@ async function decideSimulatorDriver(
|
|
|
324
329
|
desktopDescription,
|
|
325
330
|
'The Desktop app opens in a dedicated native window.',
|
|
326
331
|
])
|
|
327
|
-
const choice = await select('Where should peach open
|
|
332
|
+
const choice = await select('Where should peach open?', [
|
|
328
333
|
'Browser (recommended) No additional download',
|
|
329
334
|
desktopChoice,
|
|
330
335
|
])
|
|
@@ -343,7 +348,7 @@ async function decidePrivacy(
|
|
|
343
348
|
'Reports can contain package compatibility or an error class.',
|
|
344
349
|
'They never contain your code or app data.',
|
|
345
350
|
])
|
|
346
|
-
return confirm('Share bounded diagnostics to improve peach
|
|
351
|
+
return confirm('Share bounded diagnostics to improve peach?', false)
|
|
347
352
|
}
|
|
348
353
|
|
|
349
354
|
async function decideRepository(
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// peach mode show-touches — toggle OS touch indicators (Show Single Touches / Show taps)
|
|
2
|
+
// in the running peach sim.
|
|
3
|
+
|
|
4
|
+
import { rnxExit } from '../run-rnx'
|
|
5
|
+
import { runShellBooleanMode, type ShellBooleanModeOptions } from './shell-boolean-mode'
|
|
6
|
+
|
|
7
|
+
export async function runShowTouchesMode(args: string[], opts: ShellBooleanModeOptions) {
|
|
8
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
9
|
+
console.log(`
|
|
10
|
+
peach mode show-touches — toggle touch indicator overlays
|
|
11
|
+
|
|
12
|
+
usage:
|
|
13
|
+
peach mode show-touches [on|off|toggle]
|
|
14
|
+
|
|
15
|
+
arguments:
|
|
16
|
+
on enable touch indicators
|
|
17
|
+
off disable touch indicators
|
|
18
|
+
toggle flip the current state (default)
|
|
19
|
+
|
|
20
|
+
examples:
|
|
21
|
+
peach mode show-touches
|
|
22
|
+
peach mode show-touches on
|
|
23
|
+
peach mode show-touches off
|
|
24
|
+
`)
|
|
25
|
+
rnxExit(0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await runShellBooleanMode(args, opts, {
|
|
29
|
+
modeKey: 'showTouches',
|
|
30
|
+
displayName: 'show-touches',
|
|
31
|
+
actionId: 'toggle-touches',
|
|
32
|
+
})
|
|
33
|
+
}
|
package/cli/commands/skills.ts
CHANGED
|
@@ -9,10 +9,26 @@ import * as fs from 'fs'
|
|
|
9
9
|
import * as os from 'os'
|
|
10
10
|
import * as path from 'path'
|
|
11
11
|
import { fileURLToPath } from 'url'
|
|
12
|
+
import { getCliVersion } from '../../src/cli-version'
|
|
13
|
+
import { rnxHomeDir } from '../../src/home-paths'
|
|
12
14
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
13
15
|
import { rnxExit } from '../run-rnx'
|
|
14
16
|
|
|
17
|
+
declare const __PEACH_BUNDLED_SKILLS__: Record<string, string>
|
|
18
|
+
|
|
15
19
|
function getSkillsDir(): string {
|
|
20
|
+
// the standalone binary carries its skills instead of a package directory;
|
|
21
|
+
// write them once per cli version so installs can link to real files.
|
|
22
|
+
if (typeof __PEACH_BUNDLED_SKILLS__ !== 'undefined') {
|
|
23
|
+
const dir = path.join(rnxHomeDir(), 'skills', getCliVersion())
|
|
24
|
+
for (const [file, body] of Object.entries(__PEACH_BUNDLED_SKILLS__)) {
|
|
25
|
+
const target = path.join(dir, file)
|
|
26
|
+
if (fs.existsSync(target)) continue
|
|
27
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
28
|
+
fs.writeFileSync(target, body)
|
|
29
|
+
}
|
|
30
|
+
return dir
|
|
31
|
+
}
|
|
16
32
|
try {
|
|
17
33
|
const pkgPath = fileURLToPath(
|
|
18
34
|
import.meta.resolve(`${rnxPublicBrand.packageName}/package.json`),
|
package/cli/commands/test.ts
CHANGED
|
@@ -873,6 +873,20 @@ export async function runCloudTest(
|
|
|
873
873
|
|
|
874
874
|
const { id } = (await response.json()) as { id: string }
|
|
875
875
|
console.log(` cloud test job submitted: ${id}`)
|
|
876
|
+
// a job whose app this process serves over its tunnel cannot finish once
|
|
877
|
+
// the process exits, so an interrupt cancels it rather than leaving it to
|
|
878
|
+
// fail flow by flow while it bills.
|
|
879
|
+
if (closeTunnel) {
|
|
880
|
+
const cancelOnSignal = () => {
|
|
881
|
+
console.error(`\n interrupted: cancelling ${id}`)
|
|
882
|
+
void cancelCloudTest(id, origin).finally(() => {
|
|
883
|
+
closeTunnel?.()
|
|
884
|
+
process.exit(130)
|
|
885
|
+
})
|
|
886
|
+
}
|
|
887
|
+
process.once('SIGINT', cancelOnSignal)
|
|
888
|
+
process.once('SIGTERM', cancelOnSignal)
|
|
889
|
+
}
|
|
876
890
|
|
|
877
891
|
const pollUrl = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs?id=${encodeURIComponent(id)}`
|
|
878
892
|
const startedAt = Date.now()
|
|
@@ -970,7 +984,14 @@ export async function runTest(
|
|
|
970
984
|
return runCloudTest(args, opts)
|
|
971
985
|
}
|
|
972
986
|
|
|
973
|
-
|
|
987
|
+
// a missing or unknown target is the caller's mistake: one line, no stack.
|
|
988
|
+
let selected: ReturnType<typeof resolveTestCommand>
|
|
989
|
+
try {
|
|
990
|
+
selected = resolveTestCommand(args)
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.error(` error: ${error instanceof Error ? error.message : String(error)}`)
|
|
993
|
+
return 1
|
|
994
|
+
}
|
|
974
995
|
const app = appFromArgs(args)
|
|
975
996
|
const appTarget = app
|
|
976
997
|
? await resolveTestAppTarget(app)
|
package/cli/commands/upload.ts
CHANGED
|
@@ -194,7 +194,8 @@ options:
|
|
|
194
194
|
--repo <repo> repo name for --owner; the signed-in user must belong
|
|
195
195
|
to the local/org team that owns the link
|
|
196
196
|
--billing-kind test_run
|
|
197
|
-
|
|
197
|
+
the recording of a hosted test job, covered by the
|
|
198
|
+
job's minutes (set by the hosted recorder)
|
|
198
199
|
--sim <sim> target a specific sim (see: peach list)
|
|
199
200
|
--open open the resulting /preview/<id> url in the browser
|
|
200
201
|
--assets-only drop API/JSON/HTML records before upload; keep images,
|
|
@@ -476,6 +477,12 @@ export async function runUpload(
|
|
|
476
477
|
console.error(` invalid --billing-kind: ${billingKind}`)
|
|
477
478
|
rnxExit(1)
|
|
478
479
|
}
|
|
480
|
+
// the hosted recorder names the job whose minutes cover this recording
|
|
481
|
+
const runnerJobId = process.env.CONTRAST_RUNNER_JOB_ID
|
|
482
|
+
if (billingKind === 'test_run' && !runnerJobId) {
|
|
483
|
+
console.error(' --billing-kind test_run is only for hosted test jobs')
|
|
484
|
+
rnxExit(1)
|
|
485
|
+
}
|
|
479
486
|
if (!eventsPath && !videoPath) {
|
|
480
487
|
console.error(
|
|
481
488
|
' preview uploads require playback data: pass --events or --video.\n' +
|
|
@@ -1045,6 +1052,7 @@ export async function runUpload(
|
|
|
1045
1052
|
pullRequestNumber: provenance.pullRequestNumber ?? undefined,
|
|
1046
1053
|
pullRequestTitle: provenance.pullRequestTitle ?? undefined,
|
|
1047
1054
|
billingKind: billingKind === 'test_run' ? 'test_run' : undefined,
|
|
1055
|
+
runnerJobId: billingKind === 'test_run' ? runnerJobId : undefined,
|
|
1048
1056
|
runScope: runScope || undefined,
|
|
1049
1057
|
files: prepared.map((f) => ({
|
|
1050
1058
|
url: f.url,
|
|
@@ -1161,19 +1169,21 @@ export async function runUpload(
|
|
|
1161
1169
|
if (e.phase === 'init' && e.status === 401) {
|
|
1162
1170
|
console.error(' preview upload requires a valid login.')
|
|
1163
1171
|
console.error(' run `peach login` and retry.')
|
|
1164
|
-
rnxExit(1)
|
|
1172
|
+
rnxExit(1, 'preview upload requires a valid login')
|
|
1165
1173
|
}
|
|
1166
1174
|
if (e.status === 402) {
|
|
1167
1175
|
// billing gate — surface the server's own `message`, not raw JSON.
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1176
|
+
const message =
|
|
1177
|
+
parseServerMessage(e.responseText) ??
|
|
1178
|
+
'preview uploads require Personal, Team, or an active trial — upgrade from the billing dialog.'
|
|
1179
|
+
console.error(` ${message}`)
|
|
1180
|
+
rnxExit(1, message)
|
|
1172
1181
|
}
|
|
1173
1182
|
// PresignedUploadError.message already carries the "<phase> failed:"
|
|
1174
1183
|
// prefix — don't re-prefix it (that produced "init failed: init failed:").
|
|
1175
|
-
|
|
1176
|
-
|
|
1184
|
+
const message = e.message ?? `${e.phase ?? 'upload'} failed: ${String(err)}`
|
|
1185
|
+
console.error(` ${message}`)
|
|
1186
|
+
rnxExit(1, message)
|
|
1177
1187
|
}
|
|
1178
1188
|
|
|
1179
1189
|
const previewUrl = `${publicOrigin}${finalize.url}`
|
package/cli/commands/welcome.ts
CHANGED
|
@@ -4,14 +4,14 @@ import { printBrand } from '../ui'
|
|
|
4
4
|
import { runOpenCommand } from './control'
|
|
5
5
|
import { runSetup } from './setup'
|
|
6
6
|
|
|
7
|
-
export async function runWelcome(): Promise<number> {
|
|
7
|
+
export async function runWelcome(port: number | undefined): Promise<number> {
|
|
8
8
|
if (!isOnboardingComplete() && canPromptForPrivacyChoice()) {
|
|
9
|
-
await runSetup([], { launchConnectRN: true })
|
|
9
|
+
await runSetup([], { launchConnectRN: true, port })
|
|
10
10
|
return 0
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
printBrand('Run any React Native app in a local simulator.')
|
|
14
14
|
console.log(`\n Opening ConnectRN`)
|
|
15
|
-
await runOpenCommand(['ConnectRN', '--no-describe', '--quiet'])
|
|
15
|
+
await runOpenCommand(['ConnectRN', '--no-describe', '--quiet'], { port })
|
|
16
16
|
return 0
|
|
17
17
|
}
|
|
@@ -7,8 +7,10 @@ import { createBrowserPointerDispatcher } from '../../../peach-engine/src/render
|
|
|
7
7
|
//
|
|
8
8
|
// it resolves playwright from the absolute path the driver hands it, launches
|
|
9
9
|
// Chrome for Testing, opens the target url, then keeps itself (and therefore
|
|
10
|
-
// the browser) alive until the
|
|
11
|
-
// the
|
|
10
|
+
// the browser) alive until the page asks it to close (`peach close`, or the
|
|
11
|
+
// bridge reaping the sim after AUTOMATION_SIM_IDLE_REAP_TTL_MS with no cli),
|
|
12
|
+
// the browser disconnects, or its owning session exits. all inputs arrive
|
|
13
|
+
// through the environment.
|
|
12
14
|
//
|
|
13
15
|
// it stays a source string rather than a module of its own: playwright is
|
|
14
16
|
// resolved from the user's project at runtime and is not a dependency here, so
|
|
@@ -444,16 +446,17 @@ const cleanupProfile = () => {
|
|
|
444
446
|
const request = readBrowserCompositeCaptureRequest(value);
|
|
445
447
|
const session = await source.page.context().newCDPSession(source.page);
|
|
446
448
|
try {
|
|
449
|
+
const { clip, originX, originY } = resolveBrowserCompositeCdpClip(
|
|
450
|
+
request,
|
|
451
|
+
await session.send('Page.getLayoutMetrics'),
|
|
452
|
+
);
|
|
447
453
|
const result = await session.send('Page.captureScreenshot', {
|
|
448
454
|
format: 'png',
|
|
449
455
|
fromSurface: true,
|
|
450
456
|
captureBeyondViewport: true,
|
|
451
|
-
clip
|
|
452
|
-
request,
|
|
453
|
-
await session.send('Page.getLayoutMetrics'),
|
|
454
|
-
),
|
|
457
|
+
clip,
|
|
455
458
|
});
|
|
456
|
-
return 'data:image/png;base64,' + result.data;
|
|
459
|
+
return { dataUrl: 'data:image/png;base64,' + result.data, originX, originY };
|
|
457
460
|
} finally {
|
|
458
461
|
await session.detach();
|
|
459
462
|
}
|
|
@@ -515,7 +518,12 @@ const cleanupProfile = () => {
|
|
|
515
518
|
process.stderr.write('[console.error] ' + m.text() + '\\n');
|
|
516
519
|
}
|
|
517
520
|
});
|
|
518
|
-
page
|
|
521
|
+
// a crashed renderer never runs the page's bridge close path, so nothing
|
|
522
|
+
// would ever close this host.
|
|
523
|
+
page.on('crash', () => {
|
|
524
|
+
process.stderr.write('[page] renderer crashed; closing playwright host\\n');
|
|
525
|
+
void shutdown(1);
|
|
526
|
+
});
|
|
519
527
|
try {
|
|
520
528
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
521
529
|
} catch (err) {
|
|
@@ -16,6 +16,7 @@ import { spawn } from 'child_process'
|
|
|
16
16
|
import { closeSync, mkdtempSync, openSync, readFileSync } from 'fs'
|
|
17
17
|
import { tmpdir } from 'os'
|
|
18
18
|
import { join } from 'path'
|
|
19
|
+
import { AUTOMATION_SIM_IDLE_REAP_TTL_MS } from '../../src/bridge-constants'
|
|
19
20
|
import { playwrightHostLogName } from '../../src/playwright-host-log'
|
|
20
21
|
import { ensureProfile, playwrightProfileUserDataDir } from '../../src/profiles'
|
|
21
22
|
import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
|
|
@@ -172,12 +173,14 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
172
173
|
}${hint ? `\n${hint}` : ''}`,
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
|
-
// name
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// the
|
|
176
|
+
// name both lifetime bounds: the bridge closes an automation sim no cli
|
|
177
|
+
// has touched for the idle window, and the host watchdog closes chrome ~5s
|
|
178
|
+
// after the owning session pid exits, which surprises one-shot/detached
|
|
179
|
+
// shells (their topmost non-init ancestor is the shell itself, so the sim
|
|
180
|
+
// dies the moment the command finishes).
|
|
179
181
|
const ownerComm = getProcessComm(ownerPid)
|
|
180
|
-
const
|
|
182
|
+
const idleMinutes = AUTOMATION_SIM_IDLE_REAP_TTL_MS / 60_000
|
|
183
|
+
const ownerNote = ` — browser closes after ${idleMinutes} min with no peach command, or when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ''} exits`
|
|
181
184
|
return {
|
|
182
185
|
launched: true,
|
|
183
186
|
message: `playwright chrome launched → ${opts.url}${ownerNote}`,
|
package/cli/help-core.ts
CHANGED
|
@@ -96,8 +96,13 @@ Run \`peach <command> --help\` for leaf availability and usage.`
|
|
|
96
96
|
export function renderRnxHelp(options: RnxHelpOptions): string {
|
|
97
97
|
const command = options.command ?? null
|
|
98
98
|
const commandArgs = options.commandArgs ?? []
|
|
99
|
+
// the preview selectors and backend lists describe the factory's browser
|
|
100
|
+
// backend; the public cli runs the node backend and never sees them.
|
|
101
|
+
const factory = options.backend === 'browser'
|
|
99
102
|
if (!command) {
|
|
100
|
-
|
|
103
|
+
const index = renderCliIndexHelp(HELP_CONTEXT, formatFlagHelp()).trimEnd()
|
|
104
|
+
if (!factory) return index
|
|
105
|
+
return `${index}
|
|
101
106
|
|
|
102
107
|
browser preview selectors:
|
|
103
108
|
peach web select the existing Factory web preview
|
|
@@ -108,7 +113,7 @@ ${renderRootAvailability(options.backend)}`
|
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
let body: string | null
|
|
111
|
-
if (command === 'web' || command === 'ios' || command === 'android') {
|
|
116
|
+
if (factory && (command === 'web' || command === 'ios' || command === 'android')) {
|
|
112
117
|
body = renderPreviewHelp(command)
|
|
113
118
|
} else if (options.group === command && !options.prefer) {
|
|
114
119
|
body = renderCliGroupHelp(command, HELP_CONTEXT, formatFlagHelp())
|
|
@@ -130,11 +135,11 @@ ${renderRootAvailability(options.backend)}`
|
|
|
130
135
|
const registration = getRnxCommandRegistration(command)
|
|
131
136
|
if (!registration) return body
|
|
132
137
|
const current = getRnxCommandAvailability(command, commandArgs, options.backend)
|
|
138
|
+
if (!factory && current.available) return body
|
|
133
139
|
const currentLine = current.available
|
|
134
140
|
? `${options.backend} backend: available`
|
|
135
141
|
: `${options.backend} backend: ${current.reason}`
|
|
136
|
-
const factoryLeaves =
|
|
137
|
-
options.backend === 'browser' ? renderFactoryBrowserLeaves(command) : null
|
|
142
|
+
const factoryLeaves = factory ? renderFactoryBrowserLeaves(command) : null
|
|
138
143
|
return `${body.trimEnd()}
|
|
139
144
|
|
|
140
145
|
${factoryLeaves ? `${factoryLeaves}\n\n` : ''}${backendLabel(command, commandArgs)}
|