peachsim 0.1.578 → 0.1.580
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 +11 -9
- package/cli/app-catalog.ts +77 -0
- package/cli/app-fonts.ts +4 -20
- package/cli/app-splash.ts +2 -18
- package/cli/bridge-flow-runner.ts +505 -139
- package/cli/cloud-client.ts +1 -4
- package/cli/cloud-session.ts +1 -4
- package/cli/cloud-test-result-files.ts +3 -0
- package/cli/cloud-test-tunnel.ts +2 -5
- package/cli/command-registry.ts +2 -0
- package/cli/commands/agent.ts +149 -23
- package/cli/commands/assert.ts +1 -4
- package/cli/commands/box/peach-command.ts +62 -0
- package/cli/commands/box/plane-share.ts +0 -70
- package/cli/commands/box/rnx-command.ts +1 -60
- package/cli/commands/box/search-command.ts +14 -0
- package/cli/commands/box.ts +2 -2
- package/cli/commands/control.ts +121 -27
- package/cli/commands/cpu-profile.ts +23 -10
- package/cli/commands/daemon.ts +1 -9
- package/cli/commands/detox.ts +26 -18
- package/cli/commands/device.ts +129 -1
- package/cli/commands/film.ts +25 -16
- package/cli/commands/flow.ts +9 -2
- package/cli/commands/inspect/actions.ts +75 -1
- package/cli/commands/inspect/core.ts +81 -2
- package/cli/commands/inspect/describe.ts +2 -6
- package/cli/commands/inspect/env.ts +1 -1
- package/cli/commands/inspect/settling.ts +20 -3
- package/cli/commands/inspect/shared.ts +1 -4
- package/cli/commands/inspect/sleep.ts +1 -1
- package/cli/commands/inspect.ts +16 -49
- package/cli/commands/install-desktop.ts +8 -6
- package/cli/commands/login.ts +3 -5
- package/cli/commands/maestro-generate.ts +1 -4
- package/cli/commands/maestro.ts +38 -21
- package/cli/commands/no-bridge-hint.ts +0 -14
- package/cli/commands/permissions.ts +2 -2
- package/cli/commands/platform.ts +13 -6
- package/cli/commands/screenshot.ts +6 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/test.ts +6 -2
- package/cli/commands/three-mode.ts +162 -19
- package/cli/commands/upload.ts +3 -18
- package/cli/current-sim.ts +0 -1
- package/cli/desktop-companion.ts +40 -8
- package/cli/drivers/playwright-provisioning.ts +1 -4
- package/cli/flow-export.ts +1 -8
- package/cli/flow-session.ts +0 -4
- package/cli/hints.ts +1 -16
- package/cli/inspect-notice-state.ts +0 -4
- package/cli/main.ts +2 -4
- package/cli/outbound-endpoints.ts +1 -24
- package/cli/parse-args.ts +1 -2
- package/cli/rnx-in-process.ts +3 -1
- package/cli/run-rnx-core.ts +4 -2
- package/cli/run-rnx.ts +18 -10
- package/cli/send-to-box.ts +1 -4
- package/cli/telemetry.ts +1 -1
- package/cli/ws-bridge.ts +1 -5
- package/detox/gestures.ts +20 -6
- package/detox/index.ts +379 -123
- package/detox/navigation-lifecycle.ts +22 -13
- package/dist-lib/agent-daemon-client.cjs +21 -15
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +3 -17
- package/dist-lib/agent-sessions.cjs +5 -4
- package/dist-lib/attached-projects.cjs +1 -77
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +11 -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 +7 -5
- package/dist-lib/bridge-contract-input.mjs +7 -5
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +30 -7
- package/dist-lib/capture-contract.mjs +30 -7
- 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 +17 -11
- package/dist-lib/cloud.mjs +17 -11
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +321 -122
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +427 -212
- package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
- 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 +315 -58
- 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 +67 -31
- package/dist-lib/menu.mjs +64 -28
- package/dist-lib/metro-fingerprint-registry.cjs +8 -4
- package/dist-lib/metro-fingerprint-registry.mjs +6 -4
- package/dist-lib/metro-production-bundle.cjs +2 -2
- package/dist-lib/metro-production-bundle.mjs +2 -2
- package/dist-lib/metro.cjs +8 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +7 -4
- 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 +3 -13
- package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
- package/dist-lib/sdk.cjs +129 -10
- package/dist-lib/sdk.mjs +127 -10
- package/dist-lib/skills.cjs +1635 -1121
- package/dist-lib/swift-compile.cjs +277 -70
- package/dist-lib/swift-compile.mjs +275 -70
- package/dist-lib/swift-package.cjs +17 -5
- package/dist-lib/swift-package.mjs +15 -4
- package/dist-lib/swift-submit.cjs +32 -8
- package/dist-lib/swift-submit.mjs +29 -7
- package/dist-lib/swift.cjs +30912 -577
- package/dist-lib/vite-kotlin.cjs +356 -0
- package/dist-lib/vite-swift.cjs +1743 -0
- package/dist-lib/vite.cjs +208 -1039
- package/kotlin/README.md +18 -0
- package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
- package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
- package/kotlin/Sources/org/json/JSON.kt +244 -0
- package/kotlin/toolchain.json +19 -0
- package/package.json +14 -2
- package/scripts/dev-server-scanner.ts +0 -18
- package/scripts/optional-demo-registry.ts +0 -17
- package/skills/contrast/SKILL.md +2 -2
- package/skills/peach-setup/SKILL.md +16 -0
- package/src/agent-daemon-client.ts +36 -22
- package/src/agent-identity.ts +0 -15
- package/src/agent-prompt.ts +12 -0
- package/src/attached-projects.ts +0 -72
- package/src/backend-origin.ts +10 -0
- package/src/bridge-contract-input.ts +2 -5
- package/src/bridge-contract.ts +2 -0
- package/src/capture-contract.ts +53 -11
- package/src/cloud.ts +4 -9
- package/src/dev-server-open.ts +0 -99
- package/src/home-paths.ts +2 -3
- package/src/host/agent-host.ts +82 -36
- package/src/host/bridge-host.ts +5 -11
- package/src/host/fetch-proxy-handler.ts +19 -43
- package/src/is-record.ts +4 -0
- package/src/menu.ts +82 -37
- package/src/metro-fingerprint-registry.ts +1 -4
- package/src/metro-production-bundle.ts +1 -1
- package/src/plist.ts +1 -4
- package/src/public-brand.ts +5 -3
- package/src/render-mode.ts +0 -12
- package/src/runtime-assets.ts +9 -2
- package/src/screenshots/compose.ts +7 -1
- package/src/screenshots/frame-compose.ts +21 -10
- package/src/screenshots/schema.ts +1 -4
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/maestro-test.ts +1 -4
- package/src/sleep.ts +4 -0
- package/src/swift-compile.ts +209 -47
- package/src/swift-icu.ts +140 -0
- package/src/swift-package.ts +26 -2
- package/src/swift-submit.ts +84 -4
- package/src/swift.ts +2 -0
- package/src/vite-plugin-kotlin.ts +376 -0
- package/src/vite-plugin-one.ts +224 -36
- package/src/vite-plugin-swift.ts +382 -96
- package/src/vite-plugin.ts +121 -134
- package/src/vite.d.ts +10 -0
- package/cli/resolve-assets.ts +0 -63
- package/src/rolldown-plugin-swift.ts +0 -244
- package/src/team-machine-prompt.ts +0 -73
package/cli/commands/film.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
14
14
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
16
16
|
import { dirname, extname, join, resolve } from 'node:path'
|
|
17
|
+
import { sleep } from '../../src/sleep'
|
|
17
18
|
import {
|
|
18
19
|
callInBridgeWrite,
|
|
19
20
|
createBridgeFromParsed,
|
|
@@ -27,8 +28,6 @@ interface FilmOptions {
|
|
|
27
28
|
verbose?: boolean
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
31
|
-
|
|
32
31
|
// flags consumed by film itself; everything else that buildConfigureOptions
|
|
33
32
|
// understands (--background, --colorway, --env-*, --room, --focus*, ...)
|
|
34
33
|
// passes through to the stage configure call.
|
|
@@ -142,26 +141,36 @@ examples:
|
|
|
142
141
|
|
|
143
142
|
// -- phase 1: record the flow on the flat canvas ----------------------
|
|
144
143
|
console.log(` film: recording flow ${flowName} on the flat canvas`)
|
|
145
|
-
const started = await evalInPage<boolean>(
|
|
146
|
-
bridge,
|
|
147
|
-
`(async () => !!(await window.SootSim?.bridges?.startRecording?.('video', undefined, { skipEntitlement: true })))()`,
|
|
148
|
-
)
|
|
149
|
-
if (!started) {
|
|
150
|
-
console.error(
|
|
151
|
-
' film: engine recording store did not start (is @peach/plugin-recording installed?)',
|
|
152
|
-
)
|
|
153
|
-
return 1
|
|
154
|
-
}
|
|
155
|
-
const t0 = Date.now()
|
|
156
|
-
await sleep(500)
|
|
157
|
-
|
|
158
144
|
const { runFlowPlayback } = await import('./flow')
|
|
159
145
|
const playbackArgs = [flowPath]
|
|
160
146
|
const simFlag = flagValue(args, '--sim')
|
|
161
147
|
if (simFlag) playbackArgs.push('--sim', simFlag)
|
|
162
148
|
const slowFlag = flagValue(args, '--slow')
|
|
163
149
|
if (slowFlag) playbackArgs.push('--slow', slowFlag)
|
|
164
|
-
|
|
150
|
+
let t0 = 0
|
|
151
|
+
const flowCode = await runFlowPlayback(playbackArgs, {
|
|
152
|
+
beforeSteps: async () => {
|
|
153
|
+
// the sim can arrive in 3d (left there by --keep-three, or brought back
|
|
154
|
+
// by the flow's open), and this phase records the flat canvas
|
|
155
|
+
await evalInPage(
|
|
156
|
+
bridge,
|
|
157
|
+
`(() => { const s = window.SootSim?.bridges?.settings
|
|
158
|
+
s?.set?.('threeRecordingEditor', false)
|
|
159
|
+
s?.set?.('threeMode', false) })()`,
|
|
160
|
+
)
|
|
161
|
+
const started = await evalInPage<boolean>(
|
|
162
|
+
bridge,
|
|
163
|
+
`(async () => !!(await window.SootSim?.bridges?.startRecording?.('video', undefined, { skipEntitlement: true })))()`,
|
|
164
|
+
)
|
|
165
|
+
if (!started) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'film: engine recording store did not start (is @peach/plugin-recording installed?)',
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
t0 = Date.now()
|
|
171
|
+
await sleep(500)
|
|
172
|
+
},
|
|
173
|
+
})
|
|
165
174
|
await sleep(400)
|
|
166
175
|
const durationMs = Math.min(Date.now() - t0, MAX_FILM_MS)
|
|
167
176
|
await evalInPage(bridge, `window.SootSim?.bridges?.stopRecording?.()`)
|
package/cli/commands/flow.ts
CHANGED
|
@@ -203,7 +203,13 @@ export async function discoverSootsimUrl(): Promise<string | null> {
|
|
|
203
203
|
return null
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
export async function runFlowPlayback(
|
|
206
|
+
export async function runFlowPlayback(
|
|
207
|
+
args: string[],
|
|
208
|
+
// beforeSteps runs once the flow is on its target and about to run its first
|
|
209
|
+
// step. opening a frontmatter `app:` navigates the sim, which wipes in-page
|
|
210
|
+
// state, so a caller capturing the run itself (film) starts here.
|
|
211
|
+
hooks: { beforeSteps?: () => Promise<void> } = {},
|
|
212
|
+
): Promise<number> {
|
|
207
213
|
lastPreviewUploadResult = null
|
|
208
214
|
lastFlowTraceSteps = []
|
|
209
215
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
@@ -269,7 +275,7 @@ options:
|
|
|
269
275
|
--preview-origin <url>
|
|
270
276
|
upload target for --preview (default: auto)
|
|
271
277
|
--preview-public-origin <url>
|
|
272
|
-
public link origin for --preview (default:
|
|
278
|
+
public link origin for --preview (default: peachsim.com
|
|
273
279
|
for prod uploads)
|
|
274
280
|
--billing-kind test_run
|
|
275
281
|
meter the chained preview upload as a test run
|
|
@@ -745,6 +751,7 @@ flow extension:
|
|
|
745
751
|
)
|
|
746
752
|
}
|
|
747
753
|
}
|
|
754
|
+
await hooks.beforeSteps?.()
|
|
748
755
|
await driver.runFlow(steps, { skipPlan: dryRun })
|
|
749
756
|
if (profile) {
|
|
750
757
|
profileResult = await driver.stopProfile()
|
|
@@ -2,7 +2,12 @@ import { inspectTree, type InspectBridge } from './core'
|
|
|
2
2
|
import { resolveTargetCoords, type ResolvedTargetCoords } from './resolve-target'
|
|
3
3
|
import { waitForSootsimIdle } from './settling'
|
|
4
4
|
import type { SimSemanticNode } from '../../../src/bridge-contract'
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
SootSimAccessibilityState,
|
|
7
|
+
SootSimNodeInfo,
|
|
8
|
+
SootSimReplayTarget,
|
|
9
|
+
SootSimTapTargetInfo,
|
|
10
|
+
} from '@peach/globals'
|
|
6
11
|
|
|
7
12
|
export type TapFailure = 'not-found' | 'missed' | 'special'
|
|
8
13
|
|
|
@@ -472,6 +477,75 @@ export async function tapByText(
|
|
|
472
477
|
})
|
|
473
478
|
}
|
|
474
479
|
|
|
480
|
+
export type TapQuery =
|
|
481
|
+
| { by: 'label' | 'role' | 'hint'; value: string }
|
|
482
|
+
| { by: 'state'; value: Partial<SootSimAccessibilityState> }
|
|
483
|
+
|
|
484
|
+
const TAP_QUERY_FINDERS = {
|
|
485
|
+
label: 'findByLabel',
|
|
486
|
+
role: 'findByRole',
|
|
487
|
+
hint: 'findByHint',
|
|
488
|
+
state: 'findByA11yState',
|
|
489
|
+
} as const
|
|
490
|
+
|
|
491
|
+
function isTapNode(value: unknown): value is SootSimNodeInfo {
|
|
492
|
+
return (
|
|
493
|
+
value !== null &&
|
|
494
|
+
typeof value === 'object' &&
|
|
495
|
+
typeof Reflect.get(value, 'nodeId') === 'number'
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isTapTargetInfo(value: unknown): value is SootSimTapTargetInfo {
|
|
500
|
+
return (
|
|
501
|
+
value !== null &&
|
|
502
|
+
typeof value === 'object' &&
|
|
503
|
+
typeof Reflect.get(value, 'cx') === 'number' &&
|
|
504
|
+
typeof Reflect.get(value, 'cy') === 'number' &&
|
|
505
|
+
isTapNode(Reflect.get(value, 'target'))
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// accessibility-query taps. the test bridge finds the node and names the node
|
|
510
|
+
// a finger activates, then the tap runs exactly as tapById's does: settled
|
|
511
|
+
// first and verified at the point, so a covered target reports target-covered
|
|
512
|
+
// instead of pressing whatever covers it.
|
|
513
|
+
export async function tapByQuery(
|
|
514
|
+
bridge: InspectBridge,
|
|
515
|
+
query: TapQuery,
|
|
516
|
+
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
517
|
+
): Promise<TapOutcome> {
|
|
518
|
+
const call = (method: string, arg: unknown): Promise<unknown> =>
|
|
519
|
+
bridge.send({ type: 'call', path: `SootSim.bridges.test.${method}`, args: [arg] })
|
|
520
|
+
return tapResolvedTarget(bridge, {
|
|
521
|
+
agent: opts.agent,
|
|
522
|
+
timing: opts.timing,
|
|
523
|
+
textFallback: typeof query.value === 'string' ? query.value : undefined,
|
|
524
|
+
resolve: async () => {
|
|
525
|
+
const node = await call(TAP_QUERY_FINDERS[query.by], query.value)
|
|
526
|
+
if (!isTapNode(node)) return { cx: null }
|
|
527
|
+
const tap = await call('resolveTapTarget', node.nodeId)
|
|
528
|
+
if (!isTapTargetInfo(tap)) return { cx: null }
|
|
529
|
+
return payloadFromResolved(
|
|
530
|
+
{
|
|
531
|
+
x: tap.cx,
|
|
532
|
+
y: tap.cy,
|
|
533
|
+
nodeId: tap.target.nodeId,
|
|
534
|
+
testID: tap.target.testID,
|
|
535
|
+
text: tap.target.text ?? tap.target.accessibilityLabel,
|
|
536
|
+
type: tap.target.type,
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
nodeId: node.nodeId,
|
|
540
|
+
testID: node.testID,
|
|
541
|
+
text: node.text ?? node.accessibilityLabel,
|
|
542
|
+
type: node.type,
|
|
543
|
+
},
|
|
544
|
+
)
|
|
545
|
+
},
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
475
549
|
export async function tapBest(
|
|
476
550
|
bridge: InspectBridge,
|
|
477
551
|
query: string,
|
|
@@ -328,6 +328,35 @@ export interface LayoutTextContrast {
|
|
|
328
328
|
reason?: string
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// first family of a bound font stack, so a no-vision agent can verify a
|
|
332
|
+
// custom display family landed on the right nodes. system stacks stay omitted
|
|
333
|
+
// as a boring default. self-contained: the native eval below interpolates
|
|
334
|
+
// this source (mirrored by the web extractor's set in app-bridge webLayout).
|
|
335
|
+
export function layoutBoundFontFamily(style: unknown): string | undefined {
|
|
336
|
+
const raw =
|
|
337
|
+
typeof style === 'object' && style !== null
|
|
338
|
+
? (style as { fontFamily?: unknown }).fontFamily
|
|
339
|
+
: undefined
|
|
340
|
+
if (typeof raw !== 'string') return undefined
|
|
341
|
+
const first = raw
|
|
342
|
+
.split(',')[0]
|
|
343
|
+
.trim()
|
|
344
|
+
.replace(/^['"]|['"]$/g, '')
|
|
345
|
+
if (!first) return undefined
|
|
346
|
+
const systemFamilies = new Set([
|
|
347
|
+
'system',
|
|
348
|
+
'-apple-system',
|
|
349
|
+
'system-ui',
|
|
350
|
+
'blinkmacsystemfont',
|
|
351
|
+
'segoe ui',
|
|
352
|
+
'roboto',
|
|
353
|
+
'sans-serif',
|
|
354
|
+
'serif',
|
|
355
|
+
'monospace',
|
|
356
|
+
])
|
|
357
|
+
return systemFamilies.has(first.toLowerCase()) ? undefined : first
|
|
358
|
+
}
|
|
359
|
+
|
|
331
360
|
export function scoreLayoutTextContrast(input: {
|
|
332
361
|
normal: { width: number; height: number; data: ArrayLike<number> }
|
|
333
362
|
background: { width: number; height: number; data: ArrayLike<number> }
|
|
@@ -476,6 +505,7 @@ export interface LayoutElement {
|
|
|
476
505
|
text?: string
|
|
477
506
|
// on-screen bounding box in device/viewport px (x/y are absolute).
|
|
478
507
|
box: LayoutBox
|
|
508
|
+
hitSlop?: number | LayoutPadding
|
|
479
509
|
// box-model fields are available only when the caller opts into styling.
|
|
480
510
|
// measured zero remains distinct from an unavailable field.
|
|
481
511
|
borderRadius?: number
|
|
@@ -491,6 +521,15 @@ export interface LayoutElement {
|
|
|
491
521
|
bg?: string
|
|
492
522
|
opacity?: number
|
|
493
523
|
fontWeight?: number
|
|
524
|
+
// first family of the bound font stack (web: computed; native: authored
|
|
525
|
+
// style), so a no-vision agent can verify a custom display family landed on
|
|
526
|
+
// the right nodes. system stacks stay omitted as a boring default.
|
|
527
|
+
fontFamily?: string
|
|
528
|
+
// web extractor only: false when document.fonts reports the bound family
|
|
529
|
+
// unavailable for this element's own style and weight — the face failed to
|
|
530
|
+
// load and the browser fell back. omitted otherwise, including all native
|
|
531
|
+
// rows.
|
|
532
|
+
fontLoaded?: boolean
|
|
494
533
|
textDecoration?: string
|
|
495
534
|
overflow?: string
|
|
496
535
|
borderWidth?: number
|
|
@@ -574,6 +613,7 @@ export function layoutGetNativeEval(opts: LayoutExtractionOptions = {}): string
|
|
|
574
613
|
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
575
614
|
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
576
615
|
}
|
|
616
|
+
const familyOf = ${layoutBoundFontFamily.toString()}
|
|
577
617
|
const decoOf = (style) => {
|
|
578
618
|
const d = style && (style.textDecorationLine || style.textDecoration)
|
|
579
619
|
return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
|
|
@@ -621,6 +661,27 @@ export function layoutGetNativeEval(opts: LayoutExtractionOptions = {}): string
|
|
|
621
661
|
const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
|
|
622
662
|
const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
|
|
623
663
|
const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
664
|
+
const rawHitSlop =
|
|
665
|
+
n.hitSlop != null
|
|
666
|
+
? n.hitSlop
|
|
667
|
+
: (n.props && n.props.hitSlop != null)
|
|
668
|
+
? n.props.hitSlop
|
|
669
|
+
: (n.style && n.style.hitSlop != null)
|
|
670
|
+
? n.style.hitSlop
|
|
671
|
+
: (n.authoredStyle && n.authoredStyle.hitSlop != null)
|
|
672
|
+
? n.authoredStyle.hitSlop
|
|
673
|
+
: undefined
|
|
674
|
+
const hitSlop =
|
|
675
|
+
typeof rawHitSlop === 'number'
|
|
676
|
+
? (rawHitSlop !== 0 ? rawHitSlop : undefined)
|
|
677
|
+
: rawHitSlop && typeof rawHitSlop === 'object'
|
|
678
|
+
? {
|
|
679
|
+
t: round(rawHitSlop.top ?? rawHitSlop.t),
|
|
680
|
+
r: round(rawHitSlop.right ?? rawHitSlop.r),
|
|
681
|
+
b: round(rawHitSlop.bottom ?? rawHitSlop.b),
|
|
682
|
+
l: round(rawHitSlop.left ?? rawHitSlop.l),
|
|
683
|
+
}
|
|
684
|
+
: undefined
|
|
624
685
|
const base = {
|
|
625
686
|
...(typeof n.nodeId === 'number'
|
|
626
687
|
? {
|
|
@@ -635,6 +696,7 @@ export function layoutGetNativeEval(opts: LayoutExtractionOptions = {}): string
|
|
|
635
696
|
type: typeof n.type === 'string' ? n.type : 'node',
|
|
636
697
|
text,
|
|
637
698
|
box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
|
|
699
|
+
...(hitSlop !== undefined ? { hitSlop } : {}),
|
|
638
700
|
}
|
|
639
701
|
if (!includeStyle) return base
|
|
640
702
|
// computedStyle has the resolved values; style is the curated subset —
|
|
@@ -651,6 +713,7 @@ export function layoutGetNativeEval(opts: LayoutExtractionOptions = {}): string
|
|
|
651
713
|
bg: colorStr(style.backgroundColor),
|
|
652
714
|
opacity: opacityOf(style),
|
|
653
715
|
fontWeight: weightOf(style),
|
|
716
|
+
fontFamily: text ? familyOf(style) : undefined,
|
|
654
717
|
textDecoration: decoOf(style),
|
|
655
718
|
overflow: overflowOf(style),
|
|
656
719
|
borderWidth,
|
|
@@ -773,6 +836,17 @@ export function formatLayoutElements(
|
|
|
773
836
|
const uniform = p.t === p.r && p.r === p.b && p.b === p.l
|
|
774
837
|
parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`)
|
|
775
838
|
}
|
|
839
|
+
if (el.hitSlop !== undefined) {
|
|
840
|
+
if (typeof el.hitSlop === 'number') {
|
|
841
|
+
parts.push(`hitSlop:${el.hitSlop}`)
|
|
842
|
+
} else {
|
|
843
|
+
const hs = el.hitSlop
|
|
844
|
+
const uniform = hs.t === hs.r && hs.r === hs.b && hs.b === hs.l
|
|
845
|
+
parts.push(
|
|
846
|
+
uniform ? `hitSlop:${hs.t}` : `hitSlop:${hs.t},${hs.r},${hs.b},${hs.l}`,
|
|
847
|
+
)
|
|
848
|
+
}
|
|
849
|
+
}
|
|
776
850
|
if (el.fontSize !== undefined && el.fontSize > 0) {
|
|
777
851
|
parts.push(`font:${el.fontSize}`)
|
|
778
852
|
}
|
|
@@ -781,6 +855,8 @@ export function formatLayoutElements(
|
|
|
781
855
|
// visual quality (contrast, washed-out, garbled/struck, clipping, flat).
|
|
782
856
|
if (opts?.styling) {
|
|
783
857
|
if (el.fontWeight) parts.push(`weight:${el.fontWeight}`)
|
|
858
|
+
if (el.fontFamily) parts.push(`family:"${el.fontFamily}"`)
|
|
859
|
+
if (el.fontLoaded === false) parts.push(`fontLoaded:false`)
|
|
784
860
|
if (el.color) parts.push(`color:${el.color}`)
|
|
785
861
|
if (el.bg) parts.push(`bg:${el.bg}`)
|
|
786
862
|
if (el.contrast) {
|
|
@@ -824,7 +900,8 @@ export interface AccessibilityTreeNode {
|
|
|
824
900
|
}
|
|
825
901
|
|
|
826
902
|
// `get a11y` — flat accessibility summary projected from the visible semantic
|
|
827
|
-
// tree, which already excludes hidden and covered content
|
|
903
|
+
// tree, which already excludes hidden and covered content, minus the painted
|
|
904
|
+
// content VoiceOver cannot reach.
|
|
828
905
|
export async function inspectAccessibilityTree(
|
|
829
906
|
bridge: InspectBridge,
|
|
830
907
|
): Promise<AccessibilityTreeNode[]> {
|
|
@@ -834,6 +911,7 @@ export async function inspectAccessibilityTree(
|
|
|
834
911
|
})
|
|
835
912
|
if (!isSemanticQueryResult(result)) return []
|
|
836
913
|
return result.nodes.flatMap((node) => {
|
|
914
|
+
if (node.accessibilityHidden) return []
|
|
837
915
|
if (!node.role && !node.label && !node.text && !node.pressable) return []
|
|
838
916
|
return [
|
|
839
917
|
{
|
|
@@ -985,7 +1063,8 @@ export function scoreInteractive(n: InteractiveNode): number {
|
|
|
985
1063
|
export function tapCommandForNode(n: InteractiveNode): string {
|
|
986
1064
|
if (n.testID) return `peach do tap-id ${shellEscape(n.testID)}`
|
|
987
1065
|
const text = typeof n.text === 'string' ? n.text.trim() : ''
|
|
988
|
-
if (text.length > 0 && text.length <= 80)
|
|
1066
|
+
if (text.length > 0 && text.length <= 80)
|
|
1067
|
+
return `peach do tap-text ${shellEscape(text)}`
|
|
989
1068
|
const x =
|
|
990
1069
|
Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10
|
|
991
1070
|
const y =
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
// `peach describe` — format the pruned visible semantic tree.
|
|
2
2
|
|
|
3
|
+
import { sleep } from '../../../src/sleep'
|
|
3
4
|
import { maybeHint } from '../../hints'
|
|
4
5
|
import { inspectDescribe, type DescribeDumpOptions } from './core'
|
|
5
|
-
import {
|
|
6
|
-
maybeWaitForStartedScreenTransitions,
|
|
7
|
-
printJson,
|
|
8
|
-
sleep,
|
|
9
|
-
wantsJson,
|
|
10
|
-
} from './shared'
|
|
6
|
+
import { maybeWaitForStartedScreenTransitions, printJson, wantsJson } from './shared'
|
|
11
7
|
import type { WsBridge } from '../../ws-bridge'
|
|
12
8
|
|
|
13
9
|
export interface DescribeOptions {
|
|
@@ -7,6 +7,10 @@ export type WaitForSootsimIdleOptions = {
|
|
|
7
7
|
pollMs?: number
|
|
8
8
|
stablePolls?: number
|
|
9
9
|
strict?: boolean
|
|
10
|
+
// also require no newly painted frame across the window, the way maestro
|
|
11
|
+
// compares screenshots: a spinner or a rotation repaints without moving
|
|
12
|
+
// layout or registering as a driven animation.
|
|
13
|
+
paintStill?: boolean
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export type WaitForSootsimIdleResult = {
|
|
@@ -25,6 +29,7 @@ export async function waitForSootsimIdle({
|
|
|
25
29
|
pollMs = 50,
|
|
26
30
|
stablePolls = 3,
|
|
27
31
|
strict = false,
|
|
32
|
+
paintStill = false,
|
|
28
33
|
}: WaitForSootsimIdleOptions): Promise<WaitForSootsimIdleResult> {
|
|
29
34
|
const result = await bridge.send(
|
|
30
35
|
{
|
|
@@ -36,6 +41,7 @@ export async function waitForSootsimIdle({
|
|
|
36
41
|
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
37
42
|
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
38
43
|
const strict = ${strict ? 'true' : 'false'}
|
|
44
|
+
const paintStill = ${paintStill ? 'true' : 'false'}
|
|
39
45
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
40
46
|
|
|
41
47
|
// route-aware settle: a tap that pushes/pops a screen is async — the
|
|
@@ -67,10 +73,12 @@ export async function waitForSootsimIdle({
|
|
|
67
73
|
let requestTotal = 0
|
|
68
74
|
let requestInFlight = 0
|
|
69
75
|
let renderStatsAvailable = false
|
|
76
|
+
let frameCount = -1
|
|
70
77
|
try {
|
|
71
78
|
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
72
79
|
if (stats) {
|
|
73
80
|
renderStatsAvailable = true
|
|
81
|
+
frameCount = typeof stats.frameCount === 'number' ? stats.frameCount : -1
|
|
74
82
|
animating =
|
|
75
83
|
stats.hasActiveAnims === true ||
|
|
76
84
|
stats.hasActiveNativeAnimations === true ||
|
|
@@ -103,7 +111,7 @@ export async function waitForSootsimIdle({
|
|
|
103
111
|
}
|
|
104
112
|
walk(root)
|
|
105
113
|
}
|
|
106
|
-
return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
|
|
114
|
+
return { layout: nodes.join(';'), frameCount, animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
// how long generic background network is allowed to keep us waiting AFTER
|
|
@@ -118,6 +126,8 @@ export async function waitForSootsimIdle({
|
|
|
118
126
|
// only within this grace once the screen is visually still.
|
|
119
127
|
const networkQuietGraceMs = 1200
|
|
120
128
|
let lastLayout = ''
|
|
129
|
+
let lastFrameCount = -2
|
|
130
|
+
let paintedPolls = 0
|
|
121
131
|
let lastRequestTotal = -1
|
|
122
132
|
let stable = 0
|
|
123
133
|
let visuallyStillSince = 0
|
|
@@ -134,13 +144,18 @@ export async function waitForSootsimIdle({
|
|
|
134
144
|
if (snapshot.layoutDirty) dirtyPolls++
|
|
135
145
|
if (snapshot.animating) animatingPolls++
|
|
136
146
|
if (polls > 1 && snapshot.layout !== lastLayout) layoutChangedPolls++
|
|
147
|
+
const painted = paintStill && snapshot.frameCount !== lastFrameCount
|
|
148
|
+
if (polls > 1 && painted) paintedPolls++
|
|
137
149
|
const strictOk =
|
|
138
150
|
!strict ||
|
|
139
151
|
(snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
|
|
140
152
|
// visually settled: no animations, no image content loading, layout
|
|
141
153
|
// unchanged since the last poll.
|
|
142
154
|
const visuallyStill =
|
|
143
|
-
strictOk &&
|
|
155
|
+
strictOk &&
|
|
156
|
+
!painted &&
|
|
157
|
+
snapshot.pendingFetches === 0 &&
|
|
158
|
+
snapshot.layout === lastLayout
|
|
144
159
|
// generic network quiet: nothing in flight and no new requests issued.
|
|
145
160
|
const networkQuiet =
|
|
146
161
|
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
@@ -161,6 +176,7 @@ export async function waitForSootsimIdle({
|
|
|
161
176
|
visuallyStillSince = 0
|
|
162
177
|
}
|
|
163
178
|
lastLayout = snapshot.layout
|
|
179
|
+
lastFrameCount = snapshot.frameCount
|
|
164
180
|
lastRequestTotal = snapshot.requestTotal
|
|
165
181
|
await sleep(pollMs)
|
|
166
182
|
}
|
|
@@ -171,6 +187,7 @@ export async function waitForSootsimIdle({
|
|
|
171
187
|
if (last.layoutDirty) blockers.push('layout still dirty')
|
|
172
188
|
if (last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
173
189
|
if (last.layout !== lastLayout) blockers.push('layout still changing')
|
|
190
|
+
if (paintStill && last.frameCount !== lastFrameCount) blockers.push('still painting')
|
|
174
191
|
if (last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
175
192
|
return {
|
|
176
193
|
settled: false,
|
|
@@ -178,7 +195,7 @@ export async function waitForSootsimIdle({
|
|
|
178
195
|
blockedBy:
|
|
179
196
|
(blockers.length ? blockers.join(', ') : 'unknown') +
|
|
180
197
|
' over ' + polls + ' polls (layout changed ' + layoutChangedPolls +
|
|
181
|
-
'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x)',
|
|
198
|
+
'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x, painted ' + paintedPolls + 'x)',
|
|
182
199
|
}
|
|
183
200
|
})()`,
|
|
184
201
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// own file without reaching back into a 4000-line switch. everything here is
|
|
5
5
|
// call-agnostic: just bridge-facing helpers that any subcommand can use.
|
|
6
6
|
|
|
7
|
+
import { sleep } from '../../../src/sleep'
|
|
7
8
|
import {
|
|
8
9
|
callInBridge,
|
|
9
10
|
callInBridgeWrite,
|
|
@@ -25,10 +26,6 @@ export const SCREEN_TRANSITION_PREFLIGHT = {
|
|
|
25
26
|
startWindowMs: 64,
|
|
26
27
|
} as const
|
|
27
28
|
|
|
28
|
-
export function sleep(ms: number) {
|
|
29
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
29
|
// true when the caller passed --json. read verbs use this to route their
|
|
33
30
|
// payload to stdout-as-json and send everything else (headers, hints,
|
|
34
31
|
// warnings) to stderr so `jq` on stdout never sees framing noise.
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getCliInspectVerbMeta } from '@peach/skills/cli'
|
|
|
5
5
|
import { renderCliCommandHelp, renderCliGroupHelp } from '@peach/skills/cli/renderers'
|
|
6
6
|
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
|
|
7
7
|
import { DEFAULT_SOOTSIM_SHELL_URL } from '../../src/cli-constants'
|
|
8
|
+
import { sleep } from '../../src/sleep'
|
|
8
9
|
import { resetGuestAppStateEval } from '../app-state-reset'
|
|
9
10
|
import { printBridgeStateSummary } from '../bridge-diagnostics'
|
|
10
11
|
import {
|
|
@@ -87,7 +88,6 @@ import {
|
|
|
87
88
|
printJson,
|
|
88
89
|
printWarn,
|
|
89
90
|
type ReloadReadyResult,
|
|
90
|
-
sleep,
|
|
91
91
|
SCREEN_TRANSITION_PREFLIGHT,
|
|
92
92
|
waitForBridgeConnected,
|
|
93
93
|
wantsJson,
|
|
@@ -1148,9 +1148,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1148
1148
|
if (!subcommand || args.includes('--help') || args.includes('-h')) {
|
|
1149
1149
|
// all help output comes from the registry in packages/peach-skills
|
|
1150
1150
|
// so `peach do --help`, `peach get --help`, the website docs, and
|
|
1151
|
-
// the Contrast skill markdown stay in lock-step.
|
|
1152
|
-
// test/sootsimCliRegistry.test.ts fails CI if a verb lands without a
|
|
1153
|
-
// matching registry entry.
|
|
1151
|
+
// the Contrast skill markdown stay in lock-step.
|
|
1154
1152
|
const docContext = {
|
|
1155
1153
|
bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
1156
1154
|
defaultShellUrl: DEFAULT_SOOTSIM_SHELL_URL,
|
|
@@ -2859,11 +2857,18 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2859
2857
|
type: resolved.type ?? null,
|
|
2860
2858
|
}
|
|
2861
2859
|
}
|
|
2862
|
-
const durationArg = target
|
|
2860
|
+
const durationArg = target
|
|
2861
|
+
? Number.isFinite(Number(positional[1]))
|
|
2862
|
+
? positional[1]
|
|
2863
|
+
: positional[2]
|
|
2864
|
+
: positional[3]
|
|
2863
2865
|
const durationMs = durationArg ? Number(durationArg) : 600
|
|
2864
2866
|
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(durationMs)) {
|
|
2865
2867
|
console.error(
|
|
2866
|
-
inspectUsage(
|
|
2868
|
+
inspectUsage(
|
|
2869
|
+
'long-press',
|
|
2870
|
+
'<testid> [durationMs] | <x> <y> [durationMs] | --testid <id>',
|
|
2871
|
+
),
|
|
2867
2872
|
)
|
|
2868
2873
|
rnxExit(1)
|
|
2869
2874
|
}
|
|
@@ -2887,50 +2892,12 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2887
2892
|
}
|
|
2888
2893
|
|
|
2889
2894
|
case 'touch': {
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
const method =
|
|
2895
|
-
phase === 'down'
|
|
2896
|
-
? 'touchDown'
|
|
2897
|
-
: phase === 'move'
|
|
2898
|
-
? 'touchMove'
|
|
2899
|
-
: phase === 'up'
|
|
2900
|
-
? 'touchUp'
|
|
2901
|
-
: phase === 'cancel'
|
|
2902
|
-
? 'touchCancel'
|
|
2903
|
-
: null
|
|
2904
|
-
if (!method) {
|
|
2905
|
-
console.error(
|
|
2906
|
-
inspectUsage('touch', '<down|move|up|cancel> <x> <y> [pointerId]'),
|
|
2907
|
-
)
|
|
2908
|
-
rnxExit(1)
|
|
2909
|
-
}
|
|
2910
|
-
if (phase !== 'cancel' && (!Number.isFinite(x) || !Number.isFinite(y))) {
|
|
2911
|
-
console.error(
|
|
2912
|
-
inspectUsage('touch', '<down|move|up|cancel> <x> <y> [pointerId]'),
|
|
2913
|
-
)
|
|
2914
|
-
rnxExit(1)
|
|
2915
|
-
}
|
|
2916
|
-
// the engine's perform executor emits the matching agent-cursor
|
|
2917
|
-
// action (tap for down, move for move) natively.
|
|
2918
|
-
const roundedPointerId = Math.max(1, Math.round(pointerId))
|
|
2919
|
-
const result = await performSingleStep(
|
|
2920
|
-
bridge,
|
|
2921
|
-
method === 'touchCancel'
|
|
2922
|
-
? { type: method, pointerId: roundedPointerId }
|
|
2923
|
-
: { type: method, x, y, pointerId: roundedPointerId },
|
|
2895
|
+
// perform batches are self-contained (a pointer never outlives its
|
|
2896
|
+
// batch), so a lone phase could never hold a press between commands.
|
|
2897
|
+
console.error(
|
|
2898
|
+
' error: a touch lives within one batch; chain its phases, e.g. peach do touch down 200 720 --then touch move 200 400 --then touch up 200 400',
|
|
2924
2899
|
)
|
|
2925
|
-
|
|
2926
|
-
await recordInspectAction(
|
|
2927
|
-
'inspect touch',
|
|
2928
|
-
{ tapAtCoords: { x, y } },
|
|
2929
|
-
`touch ${phase} @${x},${y}`,
|
|
2930
|
-
)
|
|
2931
|
-
}
|
|
2932
|
-
console.log(JSON.stringify(result, null, 2))
|
|
2933
|
-
break
|
|
2900
|
+
rnxExit(1)
|
|
2934
2901
|
}
|
|
2935
2902
|
|
|
2936
2903
|
case 'gesture': {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// for users who want the Electron-based GUI companion on top of that.
|
|
5
5
|
//
|
|
6
6
|
// fetches the right platform artifact from https://contrast.dev/api/electron-release/
|
|
7
|
-
// and installs it into the OS-native location that
|
|
7
|
+
// and installs it into the OS-native location that findInstalledDesktopCompanion() looks for:
|
|
8
8
|
// macos: ~/Applications/peach.app (or /Applications if writable)
|
|
9
9
|
// linux: ~/Applications/peach.AppImage
|
|
10
10
|
// win: spawns the NSIS installer
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from 'fs'
|
|
26
26
|
import { tmpdir } from 'os'
|
|
27
27
|
import { dirname, join, resolve } from 'path'
|
|
28
|
-
import {
|
|
28
|
+
import { findInstalledDesktopCompanion } from '../desktop-companion'
|
|
29
29
|
import { confirm } from '../prompt'
|
|
30
30
|
import { rnxExit } from '../run-rnx'
|
|
31
31
|
import { accent, bold, failure, muted, success, withSpinner } from '../ui'
|
|
@@ -155,7 +155,7 @@ async function installMacDmg(dmgPath: string): Promise<string> {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// prefer /Applications if writable, otherwise ~/Applications (same paths
|
|
158
|
-
//
|
|
158
|
+
// findInstalledDesktopCompanion checks, so no extra config needed post-install).
|
|
159
159
|
const systemApps = '/Applications'
|
|
160
160
|
const userApps = resolve(process.env.HOME || '', 'Applications')
|
|
161
161
|
let destRoot = systemApps
|
|
@@ -249,7 +249,7 @@ async function installWindowsExe(exePath: string): Promise<string> {
|
|
|
249
249
|
else rejectPromise(new Error(`installer exited with code ${code}`))
|
|
250
250
|
})
|
|
251
251
|
})
|
|
252
|
-
const installed =
|
|
252
|
+
const installed = findInstalledDesktopCompanion()
|
|
253
253
|
if (!installed) {
|
|
254
254
|
throw new Error('the installer finished but the peach application was not registered')
|
|
255
255
|
}
|
|
@@ -286,7 +286,7 @@ examples:
|
|
|
286
286
|
!process.stdin.isTTY
|
|
287
287
|
const force = args.includes('--force')
|
|
288
288
|
|
|
289
|
-
const existing =
|
|
289
|
+
const existing = findInstalledDesktopCompanion()
|
|
290
290
|
if (existing && !force) {
|
|
291
291
|
console.log(` ${success('✓')} Desktop app is already installed at ${existing.path}`)
|
|
292
292
|
console.log(` ${muted('Pass --force to reinstall.')}`)
|
|
@@ -347,7 +347,9 @@ examples:
|
|
|
347
347
|
console.error(
|
|
348
348
|
' network blocked the request. Check your connection and try again later.',
|
|
349
349
|
)
|
|
350
|
-
console.error(
|
|
350
|
+
console.error(
|
|
351
|
+
' The CLI and daemon work without the desktop app. Run `peach --help`.',
|
|
352
|
+
)
|
|
351
353
|
rnxExit(1)
|
|
352
354
|
}
|
|
353
355
|
|