peachsim 0.1.527
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 +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
configPluginSplashFromExpoConfig,
|
|
7
|
+
type ConfigPluginSplashSpec,
|
|
8
|
+
} from 'sootsim-engine/engine/config-plugin-splash'
|
|
9
|
+
import { resolveAppProject } from './app-project'
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
|
|
13
|
+
// the filesystem scan and the engine's metro-manifest scan share the same
|
|
14
|
+
// parser (config-plugin-splash.ts) and produce the same resolved spec.
|
|
15
|
+
export type DiscoveredAppSplashSpec = ConfigPluginSplashSpec
|
|
16
|
+
|
|
17
|
+
type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
|
|
18
|
+
|
|
19
|
+
function localOriginOf(value: string): string | null {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(value)
|
|
22
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
23
|
+
if (
|
|
24
|
+
host === 'localhost' ||
|
|
25
|
+
host.endsWith('.localhost') ||
|
|
26
|
+
host === '0.0.0.0' ||
|
|
27
|
+
host === '::1' ||
|
|
28
|
+
/^127(?:\.\d{1,3}){3}$/.test(host)
|
|
29
|
+
) {
|
|
30
|
+
return url.origin
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// discover the app's configured splash by evaluating its expo config on the
|
|
37
|
+
// filesystem (mirrors discoverNativeLinkedAppFonts exactly). returns null when
|
|
38
|
+
// no splash is configured, no app config is found, or the bundle isn't local.
|
|
39
|
+
export function discoverConfigPluginAppSplash(opts: {
|
|
40
|
+
bundleUrl: string
|
|
41
|
+
projectDir: string
|
|
42
|
+
platform?: string
|
|
43
|
+
}): DiscoveredAppSplashSpec | null {
|
|
44
|
+
const metroOrigin = localOriginOf(opts.bundleUrl)
|
|
45
|
+
if (!metroOrigin) return null
|
|
46
|
+
|
|
47
|
+
const project = resolveAppProject(opts.projectDir)
|
|
48
|
+
const selectedAppDir = project?.appDir ?? null
|
|
49
|
+
if (!selectedAppDir) return null
|
|
50
|
+
|
|
51
|
+
const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
|
|
52
|
+
|
|
53
|
+
let getConfig: ExpoConfigGetter | null = null
|
|
54
|
+
try {
|
|
55
|
+
const modulePath = require.resolve('@expo/config', {
|
|
56
|
+
paths: [selectedAppDir],
|
|
57
|
+
})
|
|
58
|
+
const mod = require(modulePath) as { getConfig?: unknown }
|
|
59
|
+
if (typeof mod.getConfig === 'function') {
|
|
60
|
+
getConfig = mod.getConfig as ExpoConfigGetter
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
if (!getConfig) return null
|
|
64
|
+
|
|
65
|
+
let exp: unknown
|
|
66
|
+
try {
|
|
67
|
+
const result = getConfig(selectedAppDir, { skipSDKVersionRequirement: true })
|
|
68
|
+
if (result?.exp && typeof result.exp === 'object') exp = result.exp
|
|
69
|
+
} catch {}
|
|
70
|
+
if (!exp || typeof exp !== 'object') return null
|
|
71
|
+
|
|
72
|
+
return configPluginSplashFromExpoConfig(
|
|
73
|
+
exp,
|
|
74
|
+
platform === 'android' ? 'android' : 'ios',
|
|
75
|
+
metroOrigin,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// encode a resolved splash spec into the `?appSplash=` engine-URL wire value
|
|
80
|
+
// (URL-safe JSON). the engine decodes it with parseAppSplashSpec.
|
|
81
|
+
export function encodeAppSplashWire(spec: DiscoveredAppSplashSpec): string {
|
|
82
|
+
return encodeURIComponent(JSON.stringify(spec))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// parse a stamped `?appSplash=` wire value back into a spec (or null). used by
|
|
86
|
+
// the preview upload path to learn the splash image url it must stage.
|
|
87
|
+
export function parseAppSplashWire(wire: string): DiscoveredAppSplashSpec | null {
|
|
88
|
+
if (!wire) return null
|
|
89
|
+
try {
|
|
90
|
+
const decoded = JSON.parse(decodeURIComponent(wire)) as unknown
|
|
91
|
+
if (!decoded || typeof decoded !== 'object') return null
|
|
92
|
+
const spec = decoded as Record<string, unknown>
|
|
93
|
+
if (
|
|
94
|
+
spec.source !== 'expo-splash-screen' &&
|
|
95
|
+
spec.source !== 'expo-legacy' &&
|
|
96
|
+
spec.source !== 'react-native-bootsplash'
|
|
97
|
+
)
|
|
98
|
+
return null
|
|
99
|
+
if (typeof spec.backgroundColor !== 'string') return null
|
|
100
|
+
return decoded as DiscoveredAppSplashSpec
|
|
101
|
+
} catch {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// every distinct image url a splash spec references (base + dark variant). used
|
|
107
|
+
// by the preview upload path so both the light and dark splash images get
|
|
108
|
+
// staged as share assets.
|
|
109
|
+
export function splashImageUrls(spec: DiscoveredAppSplashSpec): string[] {
|
|
110
|
+
const urls = new Set<string>()
|
|
111
|
+
if (spec.imageUrl) urls.add(spec.imageUrl)
|
|
112
|
+
if (spec.dark?.imageUrl) urls.add(spec.dark.imageUrl)
|
|
113
|
+
return [...urls]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// content type for a splash image url, by extension. config splash images ship
|
|
117
|
+
// as png/jpg/webp/gif/svg; fall back to octet-stream so the share store still
|
|
118
|
+
// serves bytes the engine's <img> can load.
|
|
119
|
+
export function splashImageContentType(url: string): string {
|
|
120
|
+
const ext = (url.split('?')[0].split('.').pop() || '').toLowerCase()
|
|
121
|
+
if (ext === 'png') return 'image/png'
|
|
122
|
+
if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
|
|
123
|
+
if (ext === 'webp') return 'image/webp'
|
|
124
|
+
if (ext === 'gif') return 'image/gif'
|
|
125
|
+
if (ext === 'svg') return 'image/svg+xml'
|
|
126
|
+
return 'application/octet-stream'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type FetchedAppSplashImage = {
|
|
130
|
+
url: string
|
|
131
|
+
// sha256(url) — the share-store key, matching the font/asset convention.
|
|
132
|
+
urlhash: string
|
|
133
|
+
contentType: string
|
|
134
|
+
bytes: Uint8Array
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// fetch every splash image declared on an `?appSplash=` wire. like the config-
|
|
138
|
+
// plugin fonts, these images live in the native binary / launch storyboard,
|
|
139
|
+
// never in the metro bundle graph, so the preview upload path backfills them by
|
|
140
|
+
// re-fetching the declared urls off the still-live metro — the same staging
|
|
141
|
+
// pattern fetchAppFontFiles uses, keyed by sha256(url).
|
|
142
|
+
export async function fetchAppSplashImages(
|
|
143
|
+
wire: string,
|
|
144
|
+
hooks?: {
|
|
145
|
+
onStaged?: (info: { url: string; byteLength: number }) => void
|
|
146
|
+
onError?: (url: string, err: unknown) => void
|
|
147
|
+
},
|
|
148
|
+
): Promise<FetchedAppSplashImage[]> {
|
|
149
|
+
const spec = parseAppSplashWire(wire)
|
|
150
|
+
if (!spec) return []
|
|
151
|
+
const out: FetchedAppSplashImage[] = []
|
|
152
|
+
const seen = new Set<string>()
|
|
153
|
+
for (const url of splashImageUrls(spec)) {
|
|
154
|
+
const urlhash = createHash('sha256').update(url).digest('hex')
|
|
155
|
+
if (seen.has(urlhash)) continue
|
|
156
|
+
seen.add(urlhash)
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(url)
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
hooks?.onError?.(url, new Error(`${res.status} ${res.statusText}`))
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
164
|
+
out.push({ url, urlhash, contentType: splashImageContentType(url), bytes })
|
|
165
|
+
hooks?.onStaged?.({ url, byteLength: bytes.byteLength })
|
|
166
|
+
} catch (err) {
|
|
167
|
+
hooks?.onError?.(url, err)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out
|
|
171
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// resets the guest app through the engine lifecycle bridge. a storage reset
|
|
2
|
+
// replaces the tenant worker before clearing its scoped stores, which is the
|
|
3
|
+
// maestro `clearState` contract. a plain reload keeps the faster reload path.
|
|
4
|
+
//
|
|
5
|
+
// this replaces a host-realm `localStorage.clear()` + `indexedDB.deleteDatabase`
|
|
6
|
+
// loop that was wrong twice: (1) `localStorage.clear()` wiped engine settings
|
|
7
|
+
// and every co-tenant sim's tenant storage, not just this app's; (2) the delete
|
|
8
|
+
// loop ran in the host realm where the only database is the engine's own
|
|
9
|
+
// `preview-share-state`, which the engine holds open — `deleteDatabase` then
|
|
10
|
+
// hung forever (no success/error/blocked event fired), so the whole eval timed
|
|
11
|
+
// out at 60s and never cleared the guest's secure-store session either. that
|
|
12
|
+
// was the bluesky e2e suite's blanket setupApp failure.
|
|
13
|
+
export function resetGuestAppStateEval(resetStorage: boolean): string {
|
|
14
|
+
return `(async () => {
|
|
15
|
+
const bridge = globalThis.SootSim && globalThis.SootSim.bridges && globalThis.SootSim.bridges.hotRemount
|
|
16
|
+
if (!bridge) return false
|
|
17
|
+
if (${resetStorage ? 'true' : 'false'}) {
|
|
18
|
+
if (typeof bridge.resetExternalApp !== 'function') return false
|
|
19
|
+
const result = await bridge.resetExternalApp({ strategy: 'full' })
|
|
20
|
+
return result.failures.length === 0 && result.relaunched
|
|
21
|
+
}
|
|
22
|
+
if (typeof bridge.reloadExternalApp !== 'function') return false
|
|
23
|
+
await bridge.reloadExternalApp()
|
|
24
|
+
return true
|
|
25
|
+
})()`
|
|
26
|
+
}
|
package/cli/auth.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// rnx cli auth resolution for cloud-hitting commands.
|
|
2
|
+
//
|
|
3
|
+
// priority order (first match wins):
|
|
4
|
+
// 1. RNX_API_KEY — ci path
|
|
5
|
+
// 2. CONTRAST_INSTALLATION_TOKEN/GITHUB_TOKEN + repo env — github action
|
|
6
|
+
// preview uploads from the Contrast runner, no extra secret
|
|
7
|
+
// 3. desktop session token from shared desktop auth (`rnx login`)
|
|
8
|
+
//
|
|
9
|
+
// callers wanting an auth header: `authHeaderOrExit('upload')`.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readSharedDesktopAuthSession,
|
|
13
|
+
sharedDesktopAuthAccounts,
|
|
14
|
+
} from '../src/auth/shared-session.ts'
|
|
15
|
+
import { rnxExit } from './run-rnx.ts'
|
|
16
|
+
|
|
17
|
+
export type CliApiKeyEnvName = 'RNX_API_KEY'
|
|
18
|
+
|
|
19
|
+
export type CliAuth =
|
|
20
|
+
| { kind: 'api-key'; secret: string; source: 'env'; envName: CliApiKeyEnvName }
|
|
21
|
+
| {
|
|
22
|
+
kind: 'github'
|
|
23
|
+
token: string
|
|
24
|
+
// historical name: this is the github owner/repo slug, not the
|
|
25
|
+
// numeric repository id.
|
|
26
|
+
repoId: string
|
|
27
|
+
repositoryId: string | null
|
|
28
|
+
owner: string | null
|
|
29
|
+
repo: string | null
|
|
30
|
+
installationId: string | null
|
|
31
|
+
source: 'contrast-runner' | 'github-actions'
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
kind: 'session'
|
|
35
|
+
token: string
|
|
36
|
+
origin: string
|
|
37
|
+
// the account this sign-in bills cloud sessions to. null when the person
|
|
38
|
+
// belongs to several accounts and has not chosen one with `rnx login
|
|
39
|
+
// --account`; a person with exactly one account never has to choose.
|
|
40
|
+
accountId: string | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type GitHubUploadIdentity = {
|
|
44
|
+
repoId: string
|
|
45
|
+
owner?: string
|
|
46
|
+
repo?: string
|
|
47
|
+
installationId?: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── resolution ────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const GITHUB_TOKEN_PREFIXES = ['ghs_', 'ghp_', 'gho_', 'ghu_', 'github_pat_']
|
|
53
|
+
|
|
54
|
+
function looksLikeGitHubToken(token: string | undefined): boolean {
|
|
55
|
+
// github's 2026 stateless installation tokens (`secrets.GITHUB_TOKEN` /
|
|
56
|
+
// github.token in actions) are ~520 chars and vary by payload — keep them
|
|
57
|
+
// opaque with NO upper length cap, matching the canonical server-side check
|
|
58
|
+
// in src/github/token.ts. the length floor + prefix is only a cheap typo
|
|
59
|
+
// guard before the cloud endpoints verify real repo access. an upper cap
|
|
60
|
+
// here silently rejects the long actions token, so resolveCliAuth() returns
|
|
61
|
+
// null and every CI `record start` dies with "set RNX_API_KEY".
|
|
62
|
+
return Boolean(
|
|
63
|
+
token &&
|
|
64
|
+
token.length >= 20 &&
|
|
65
|
+
GITHUB_TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix)),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveGitHubActionsAuth(): CliAuth | null {
|
|
70
|
+
const contrastRunnerToken = process.env.CONTRAST_INSTALLATION_TOKEN?.trim()
|
|
71
|
+
const githubActionsToken = process.env.GITHUB_TOKEN?.trim()
|
|
72
|
+
const token = contrastRunnerToken || githubActionsToken
|
|
73
|
+
if (!looksLikeGitHubToken(token)) return null
|
|
74
|
+
|
|
75
|
+
const repoSlug = (
|
|
76
|
+
process.env.CONTRAST_REPO ||
|
|
77
|
+
process.env.GITHUB_REPOSITORY ||
|
|
78
|
+
''
|
|
79
|
+
).trim()
|
|
80
|
+
if (!repoSlug) return null
|
|
81
|
+
const [owner, repo] = repoSlug.includes('/') ? repoSlug.split('/', 2) : [null, null]
|
|
82
|
+
const repositoryId = (
|
|
83
|
+
process.env.CONTRAST_REPO_ID ||
|
|
84
|
+
process.env.GITHUB_REPOSITORY_ID ||
|
|
85
|
+
''
|
|
86
|
+
).trim()
|
|
87
|
+
|
|
88
|
+
const installationId = (
|
|
89
|
+
process.env.CONTRAST_INSTALLATION_ID ||
|
|
90
|
+
process.env.GITHUB_APP_INSTALLATION_ID ||
|
|
91
|
+
''
|
|
92
|
+
).trim()
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
kind: 'github',
|
|
96
|
+
token: token!,
|
|
97
|
+
repoId: repoSlug,
|
|
98
|
+
repositoryId: repositoryId || null,
|
|
99
|
+
owner: owner || null,
|
|
100
|
+
repo: repo || null,
|
|
101
|
+
installationId: installationId || null,
|
|
102
|
+
source: contrastRunnerToken ? 'contrast-runner' : 'github-actions',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isCliApiKeySecret(secret: string): boolean {
|
|
107
|
+
return secret.startsWith('sk_rnx_')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readApiKeyFromEnv(): { secret: string; envName: CliApiKeyEnvName } | null {
|
|
111
|
+
const envName = 'RNX_API_KEY'
|
|
112
|
+
const secret = process.env[envName]?.trim()
|
|
113
|
+
return secret && isCliApiKeySecret(secret) ? { secret, envName } : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveCliAuth(): CliAuth | null {
|
|
117
|
+
const envKey = readApiKeyFromEnv()
|
|
118
|
+
if (envKey) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'api-key',
|
|
121
|
+
secret: envKey.secret,
|
|
122
|
+
source: 'env',
|
|
123
|
+
envName: envKey.envName,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const githubAuth = resolveGitHubActionsAuth()
|
|
127
|
+
if (githubAuth) return githubAuth
|
|
128
|
+
const session = readSharedDesktopAuthSession()
|
|
129
|
+
if (session?.token) {
|
|
130
|
+
const accounts = sharedDesktopAuthAccounts(session.user)
|
|
131
|
+
return {
|
|
132
|
+
kind: 'session',
|
|
133
|
+
token: session.token,
|
|
134
|
+
origin: session.origin,
|
|
135
|
+
accountId:
|
|
136
|
+
session.accountId ?? (accounts.length === 1 ? (accounts[0]?.id ?? null) : null),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const CHOOSE_ACCOUNT_HINT =
|
|
143
|
+
'this sign-in belongs to several accounts; run `rnx login --account <name>` to choose the one to bill'
|
|
144
|
+
|
|
145
|
+
// the account a request that opens a paid cloud session names. an api key
|
|
146
|
+
// names its own account, so it sends none; a session token has to say.
|
|
147
|
+
export function cloudAccountIdOrExit(auth: CliAuth): string | null {
|
|
148
|
+
if (auth.kind !== 'session') return null
|
|
149
|
+
if (auth.accountId) return auth.accountId
|
|
150
|
+
process.stderr.write(`\n ${CHOOSE_ACCOUNT_HINT}\n\n`)
|
|
151
|
+
rnxExit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function authHeaderValue(auth: CliAuth): string {
|
|
155
|
+
return auth.kind === 'api-key' ? `Bearer ${auth.secret}` : `Bearer ${auth.token}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function githubUploadIdentity(auth: CliAuth | null): GitHubUploadIdentity | null {
|
|
159
|
+
if (!auth || auth.kind !== 'github') return null
|
|
160
|
+
return {
|
|
161
|
+
repoId: auth.repoId,
|
|
162
|
+
owner: auth.owner ?? undefined,
|
|
163
|
+
repo: auth.repo ?? undefined,
|
|
164
|
+
installationId: auth.installationId,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// fail fast with a friendly message — use at the top of cloud-hitting
|
|
169
|
+
// commands that can't proceed without auth.
|
|
170
|
+
export function authHeaderOrExit(commandLabel: string): {
|
|
171
|
+
auth: CliAuth
|
|
172
|
+
header: string
|
|
173
|
+
} {
|
|
174
|
+
const auth = resolveCliAuth()
|
|
175
|
+
if (auth) return { auth, header: authHeaderValue(auth) }
|
|
176
|
+
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
`\n rnx ${commandLabel} needs to be authenticated.\n\n` +
|
|
179
|
+
` pick one:\n` +
|
|
180
|
+
` • run \`rnx login\` to sign in with your GH account\n` +
|
|
181
|
+
` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`,
|
|
182
|
+
)
|
|
183
|
+
rnxExit(1)
|
|
184
|
+
}
|
package/cli/bin.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// rnx CLI entry point. the full command and verb surface lives in the
|
|
4
|
+
// registry at `packages/rnx-skills/src/cli/meta.ts` — help output,
|
|
5
|
+
// website docs, and agent skills all read from there. do not duplicate
|
|
6
|
+
// command listings in this file; run `rnx --help` for the canonical
|
|
7
|
+
// surface.
|
|
8
|
+
//
|
|
9
|
+
// this file only routes. the hidden entry points below each take over the
|
|
10
|
+
// whole process, so the CLI itself lives in `./main` and is imported only once
|
|
11
|
+
// none of them has claimed the run.
|
|
12
|
+
|
|
13
|
+
import { RNX_INTERNAL_COMMAND } from './internal-child'
|
|
14
|
+
|
|
15
|
+
if (process.argv[2] === '__shell-init') {
|
|
16
|
+
const { renderRnxShellInit } = await import('./shell-init')
|
|
17
|
+
try {
|
|
18
|
+
process.stdout.write(renderRnxShellInit(process.argv[3] ?? ''))
|
|
19
|
+
process.exit(0)
|
|
20
|
+
} catch (error) {
|
|
21
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (process.argv[2] === '__cloud-confirm') {
|
|
27
|
+
try {
|
|
28
|
+
const [{ confirmCloudSimulator }, { readCloudSession }] = await Promise.all([
|
|
29
|
+
import('./cloud-client'),
|
|
30
|
+
import('./cloud-session'),
|
|
31
|
+
])
|
|
32
|
+
const session = readCloudSession()
|
|
33
|
+
if (!session) throw new Error('RNX cloud confirmation has no shell session')
|
|
34
|
+
// a box has no claim to keep alive: its simulator is held by the Box page
|
|
35
|
+
// for as long as the box runs, so a shell on a box confirms nothing.
|
|
36
|
+
if (session.service === 'sim') await confirmCloudSimulator(session)
|
|
37
|
+
process.exit(0)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`RNX cloud confirmation failed: ${
|
|
41
|
+
error instanceof Error ? error.message : String(error)
|
|
42
|
+
}\n`,
|
|
43
|
+
)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// child processes this CLI spawns as itself: the detached browser host,
|
|
49
|
+
// playwright's installer, maestro's blocking fetch worker. unlike the two
|
|
50
|
+
// above, these do not exit on the way out — the installer and the browser host
|
|
51
|
+
// are still working when their handler returns, and the process ends when that
|
|
52
|
+
// work does. so this is the branch that must skip the CLI rather than the CLI
|
|
53
|
+
// that must skip it.
|
|
54
|
+
if (process.argv[2] === RNX_INTERNAL_COMMAND) {
|
|
55
|
+
const { runInternalChild } = await import('./internal-child')
|
|
56
|
+
await runInternalChild(process.argv.slice(3))
|
|
57
|
+
} else {
|
|
58
|
+
await import('./main')
|
|
59
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { MERGED_CONSOLE_COUNT_EVAL, inspectErrors } from './commands/inspect/core'
|
|
2
|
+
import { formatLogTimestamp } from './commands/inspect/shared'
|
|
3
|
+
import type { WsBridge } from './ws-bridge'
|
|
4
|
+
import type { SootSimRequestEntry } from '@rnx/globals'
|
|
5
|
+
|
|
6
|
+
interface ConsoleSummaryOptions {
|
|
7
|
+
includeTail?: boolean
|
|
8
|
+
errorsCommand?: string
|
|
9
|
+
warningsCommand?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RequestSummaryOptions {
|
|
13
|
+
includeTail?: boolean
|
|
14
|
+
requestsCommand?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FailureDiagnosticsOptions {
|
|
18
|
+
errorsCommand?: string
|
|
19
|
+
warningsCommand?: string
|
|
20
|
+
requestsCommand?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatRequestMessage(entry: SootSimRequestEntry) {
|
|
24
|
+
const target = entry.displayUrl || entry.url
|
|
25
|
+
if (entry.status != null) {
|
|
26
|
+
return `${entry.method} ${target} -> ${entry.status}${entry.statusText ? ` ${entry.statusText}` : ''}`
|
|
27
|
+
}
|
|
28
|
+
if (entry.error) {
|
|
29
|
+
return `${entry.method} ${target} -> ${entry.error}`
|
|
30
|
+
}
|
|
31
|
+
return `${entry.method} ${target}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function callTestBridge<T = unknown>(
|
|
35
|
+
bridge: WsBridge,
|
|
36
|
+
method: string,
|
|
37
|
+
...args: unknown[]
|
|
38
|
+
) {
|
|
39
|
+
return bridge.send({
|
|
40
|
+
type: 'call',
|
|
41
|
+
path: `__sootsimTest.${method}`,
|
|
42
|
+
args,
|
|
43
|
+
}) as Promise<T>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function printBridgeConsoleSummary(
|
|
47
|
+
bridge: WsBridge,
|
|
48
|
+
opts: ConsoleSummaryOptions = {},
|
|
49
|
+
) {
|
|
50
|
+
const counts = (await bridge.send({
|
|
51
|
+
type: 'evaluate',
|
|
52
|
+
code: MERGED_CONSOLE_COUNT_EVAL,
|
|
53
|
+
})) as { errors?: number; warnings?: number; total?: number } | null
|
|
54
|
+
|
|
55
|
+
if (!counts || typeof counts !== 'object') return
|
|
56
|
+
|
|
57
|
+
const errors = Math.max(0, Number(counts.errors) || 0)
|
|
58
|
+
const warnings = Math.max(0, Number(counts.warnings) || 0)
|
|
59
|
+
if (errors === 0 && warnings === 0) return
|
|
60
|
+
|
|
61
|
+
const parts = []
|
|
62
|
+
if (errors > 0) parts.push(`${errors} console error${errors === 1 ? '' : 's'}`)
|
|
63
|
+
if (warnings > 0) {
|
|
64
|
+
parts.push(`${warnings} console warning${warnings === 1 ? '' : 's'}`)
|
|
65
|
+
}
|
|
66
|
+
console.log(`\n console: ${parts.join(', ')}`)
|
|
67
|
+
if (opts.errorsCommand) console.log(` inspect: ${opts.errorsCommand}`)
|
|
68
|
+
if (warnings > 0 && opts.warningsCommand) {
|
|
69
|
+
console.log(` inspect: ${opts.warningsCommand}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!opts.includeTail || errors === 0) return
|
|
73
|
+
|
|
74
|
+
// merged read (ws-bridge buffer + observability) so forwarded render-worker
|
|
75
|
+
// errors are shown in the tail, not just counted.
|
|
76
|
+
const recentErrors = await inspectErrors(bridge, 5)
|
|
77
|
+
if (!Array.isArray(recentErrors) || recentErrors.length === 0) return
|
|
78
|
+
|
|
79
|
+
console.log('\n recent console errors:\n')
|
|
80
|
+
for (const entry of recentErrors as Array<{
|
|
81
|
+
timestamp: number
|
|
82
|
+
args?: unknown[]
|
|
83
|
+
}>) {
|
|
84
|
+
const time = formatLogTimestamp(entry.timestamp)
|
|
85
|
+
const msg = Array.isArray(entry.args)
|
|
86
|
+
? entry.args
|
|
87
|
+
.map((value) =>
|
|
88
|
+
typeof value === 'object' ? JSON.stringify(value) : String(value),
|
|
89
|
+
)
|
|
90
|
+
.join(' ')
|
|
91
|
+
: String(entry)
|
|
92
|
+
console.log(` [${time}] ${msg}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function printBridgeRequestSummary(
|
|
97
|
+
bridge: WsBridge,
|
|
98
|
+
opts: RequestSummaryOptions = {},
|
|
99
|
+
) {
|
|
100
|
+
const counts = await callTestBridge<{ failed?: number; total?: number }>(
|
|
101
|
+
bridge,
|
|
102
|
+
'getRequestCounts',
|
|
103
|
+
)
|
|
104
|
+
if (!counts || typeof counts !== 'object') return
|
|
105
|
+
|
|
106
|
+
const failed = Math.max(0, Number(counts.failed) || 0)
|
|
107
|
+
if (failed === 0) return
|
|
108
|
+
|
|
109
|
+
console.log(`\n network: ${failed} failed request${failed === 1 ? '' : 's'}`)
|
|
110
|
+
if (opts.requestsCommand) console.log(` inspect: ${opts.requestsCommand}`)
|
|
111
|
+
|
|
112
|
+
if (!opts.includeTail) return
|
|
113
|
+
|
|
114
|
+
const recentFailed = await callTestBridge<SootSimRequestEntry[]>(
|
|
115
|
+
bridge,
|
|
116
|
+
'getFailedRequests',
|
|
117
|
+
5,
|
|
118
|
+
)
|
|
119
|
+
if (!Array.isArray(recentFailed) || recentFailed.length === 0) return
|
|
120
|
+
|
|
121
|
+
console.log('\n recent failed requests:\n')
|
|
122
|
+
for (const entry of recentFailed) {
|
|
123
|
+
const time = formatLogTimestamp(entry.timestamp)
|
|
124
|
+
console.log(` [${time}] ${formatRequestMessage(entry)}`)
|
|
125
|
+
if (entry.responseBody) {
|
|
126
|
+
console.log(` ${entry.responseBody}`)
|
|
127
|
+
} else if (entry.error) {
|
|
128
|
+
console.log(` ${entry.error}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface BridgeStateSummary {
|
|
134
|
+
url?: string
|
|
135
|
+
simId?: string | null
|
|
136
|
+
nodeCount?: number
|
|
137
|
+
mode?: string
|
|
138
|
+
shell?: {
|
|
139
|
+
state?: string | null
|
|
140
|
+
activeApp?: string | null
|
|
141
|
+
showSwitcher?: boolean
|
|
142
|
+
switcherPhase?: string | null
|
|
143
|
+
} | null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function printBridgeStateSummary(bridge: WsBridge) {
|
|
147
|
+
const state = (await bridge.send({
|
|
148
|
+
type: 'evaluate',
|
|
149
|
+
code: `(async () => {
|
|
150
|
+
const test = window.__sootsimTest
|
|
151
|
+
const mainShell = window.SootSim?.bridges?.mainShell
|
|
152
|
+
let shell = null
|
|
153
|
+
try {
|
|
154
|
+
shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
|
|
155
|
+
} catch {}
|
|
156
|
+
let nodeCount = 0
|
|
157
|
+
try {
|
|
158
|
+
nodeCount = typeof test?.getNodeCount === 'function' ? await test.getNodeCount() : 0
|
|
159
|
+
} catch {}
|
|
160
|
+
return {
|
|
161
|
+
url: window.location.href,
|
|
162
|
+
simId: window.__sootsimSimId || null,
|
|
163
|
+
nodeCount,
|
|
164
|
+
mode: window.__sootsimRenderHost ? 'render-worker' : 'main-thread',
|
|
165
|
+
shell: shell
|
|
166
|
+
? {
|
|
167
|
+
state: shell.state || null,
|
|
168
|
+
activeApp: shell.activeApp || null,
|
|
169
|
+
showSwitcher: !!shell.showSwitcher,
|
|
170
|
+
switcherPhase: shell.switcherPhase || null,
|
|
171
|
+
}
|
|
172
|
+
: null,
|
|
173
|
+
}
|
|
174
|
+
})()`,
|
|
175
|
+
})) as BridgeStateSummary | null
|
|
176
|
+
|
|
177
|
+
if (!state || typeof state !== 'object') return
|
|
178
|
+
|
|
179
|
+
console.log('\n state:')
|
|
180
|
+
if (state.simId) console.log(` sim: ${state.simId}`)
|
|
181
|
+
if (state.url) console.log(` url: ${state.url}`)
|
|
182
|
+
if (state.mode) console.log(` mode: ${state.mode}`)
|
|
183
|
+
const nodeCount = Number(state.nodeCount) || 0
|
|
184
|
+
console.log(` nodes: ${nodeCount}${nodeCount > 10 ? ' (ready)' : ' (not ready)'}`)
|
|
185
|
+
|
|
186
|
+
const shell = state.shell
|
|
187
|
+
if (shell && typeof shell === 'object') {
|
|
188
|
+
const shellParts = [
|
|
189
|
+
shell.state ? `state=${shell.state}` : null,
|
|
190
|
+
shell.activeApp ? `app=${shell.activeApp}` : null,
|
|
191
|
+
shell.showSwitcher ? 'switcher=open' : null,
|
|
192
|
+
shell.switcherPhase ? `phase=${shell.switcherPhase}` : null,
|
|
193
|
+
].filter(Boolean)
|
|
194
|
+
if (shellParts.length > 0) {
|
|
195
|
+
console.log(` shell: ${shellParts.join(' ')}`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function printBridgeFailureDiagnostics(
|
|
201
|
+
bridge: WsBridge,
|
|
202
|
+
opts: FailureDiagnosticsOptions = {},
|
|
203
|
+
) {
|
|
204
|
+
try {
|
|
205
|
+
await printBridgeStateSummary(bridge)
|
|
206
|
+
} catch {
|
|
207
|
+
// ignore state snapshot failures while already handling a failure
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await printBridgeConsoleSummary(bridge, {
|
|
211
|
+
includeTail: true,
|
|
212
|
+
errorsCommand: opts.errorsCommand,
|
|
213
|
+
warningsCommand: opts.warningsCommand,
|
|
214
|
+
})
|
|
215
|
+
} catch {
|
|
216
|
+
// ignore console snapshot failures while already handling a failure
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await printBridgeRequestSummary(bridge, {
|
|
220
|
+
includeTail: true,
|
|
221
|
+
requestsCommand: opts.requestsCommand,
|
|
222
|
+
})
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore request snapshot failures while already handling a failure
|
|
225
|
+
}
|
|
226
|
+
}
|