peachsim 0.1.648 → 0.1.650
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/cli/command-registry.ts +2 -1
- package/cli/commands/guest-host.ts +67 -0
- package/cli/commands/kotlin.ts +231 -0
- package/cli/commands/project-report.ts +1 -1
- package/cli/commands/report-issue.ts +55 -32
- package/cli/commands/setup.ts +10 -14
- package/cli/commands/support.ts +399 -0
- package/cli/commands/swift.ts +6 -52
- package/cli/commands/telemetry.ts +10 -13
- package/cli/main.ts +22 -10
- package/cli/outbound-endpoints.ts +23 -2
- package/cli/privacy.ts +23 -25
- package/cli/runtime-notes.ts +3 -2
- package/cli/telemetry.ts +109 -14
- package/dist-lib/agent-daemon-client.cjs +1 -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 +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- 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 +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +3 -3
- package/dist-lib/host/bridge-host.cjs +1 -1
- 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 +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +102 -21
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +28 -39
- package/dist-lib/swift-compile.cjs +6 -3
- package/dist-lib/swift-compile.mjs +6 -3
- 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 +1 -1
- package/dist-lib/vite-swift.cjs +6 -3
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +85 -0
- package/kotlin/AppSources/android/app/Application.kt +7 -0
- package/kotlin/AppSources/android/content/Context.kt +5 -0
- package/kotlin/AppSources/android/content/Intent.kt +16 -0
- package/kotlin/AppSources/android/os/Bundle.kt +5 -0
- package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
- package/kotlin/AppSources/android/view/View.kt +72 -0
- package/kotlin/AppSources/android/view/Window.kt +44 -0
- package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
- package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
- package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +316 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceViewDispatch.kt +13 -0
- package/kotlin/Sources/org/json/JSON.kt +6 -0
- package/kotlin/fixtures/counter/Counter.d.kt.ts +16 -0
- package/kotlin/fixtures/counter/Counter.kt +18 -0
- package/kotlin/fixtures/counter/CounterApp.native.tsx +34 -0
- package/kotlin/fixtures/counter/CounterApp.tsx +3 -0
- package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +7 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +18 -3
- package/kotlin/fixtures/counter/app/_layout.tsx +4 -0
- package/kotlin/fixtures/counter/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/counter/app/routes.d.ts +18 -0
- package/kotlin/fixtures/counter/package.json +7 -0
- package/kotlin/fixtures/counter/tsconfig.json +19 -0
- package/kotlin/fixtures/counter/vite.config.ts +10 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +5 -0
- package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +30 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/AndroidManifest.xml +13 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/MainActivity.kt +60 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/ProbeApplication.kt +18 -0
- package/kotlin/fixtures/kotlin-app/build.gradle.kts +5 -0
- package/kotlin/fixtures/kotlin-app/gradle.properties +4 -0
- package/kotlin/fixtures/kotlin-app/settings.gradle.kts +4 -0
- package/kotlin/fork/NOTICE +7 -5
- package/kotlin/fork/README.md +41 -49
- package/kotlin/fork/build.sh +3 -2
- package/kotlin/fork/compose-worker-globals.patch +22 -11
- package/kotlin/fork/fork.json +2 -2
- package/kotlin/fork/publish.ts +30 -15
- package/kotlin/fork/skiko-worker-clock.patch +38 -0
- package/kotlin/toolchain.json +9 -9
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +2 -1
- package/skills/peach-setup/SKILL.md +2 -2
- package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
- package/src/home-paths.ts +5 -3
- package/src/kotlin-app.ts +411 -0
- package/src/kotlin-host.ts +148 -25
- package/src/swift-compile.ts +7 -2
package/README.md
CHANGED
|
@@ -284,14 +284,15 @@ Peach sends no analytics, crash report, command event, app data, project data, o
|
|
|
284
284
|
identity by default. An unsigned-in interactive run may request the runtime it
|
|
285
285
|
is about to execute and the stable standalone CLI version document. The latter
|
|
286
286
|
runs after the command in a detached process at most once every 20 hours and can
|
|
287
|
-
be disabled with `PEACH_UPDATE_CHECK=off`.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
287
|
+
be disabled with `PEACH_UPDATE_CHECK=off`. Bounded diagnostics and automatic
|
|
288
|
+
crash reports are on by default; the first human run says once what they send,
|
|
289
|
+
and `peach config telemetry off` or `DO_NOT_TRACK=1` turns them off. Legacy
|
|
290
|
+
browser and Electron telemetry senders are removed rather than being enabled by
|
|
291
|
+
this default.
|
|
292
|
+
|
|
293
|
+
Diagnostics are limited to package name/version/unsupported API, error class
|
|
294
|
+
and first-line message, and the command name with its exit code; a crash report
|
|
295
|
+
files the same error fields in the issue queue. Peach never sends source, file paths, project names, app content, commands,
|
|
295
296
|
identity, screenshots, or recordings as diagnostics. Cloud commands such as
|
|
296
297
|
login, upload, billing, issue reporting, generated flows, and screenshot decks
|
|
297
298
|
contact their documented service only when explicitly invoked. The complete
|
|
@@ -362,7 +363,7 @@ peach open /settings # route via React Native Linking
|
|
|
362
363
|
peach describe # dump UI tree
|
|
363
364
|
peach get errors 5 # recent runtime failures
|
|
364
365
|
peach compat # scan native package compatibility
|
|
365
|
-
peach report
|
|
366
|
+
peach support report "…" # preview an opt-in compatibility report
|
|
366
367
|
peach find --testid cta # inspect one node deeply
|
|
367
368
|
peach debug enable animated,layout
|
|
368
369
|
peach debug trace shell on 240
|
package/cli/command-registry.ts
CHANGED
|
@@ -43,6 +43,7 @@ const NODE_COMMAND_NAMES = [
|
|
|
43
43
|
'remote',
|
|
44
44
|
'preview',
|
|
45
45
|
'swift',
|
|
46
|
+
'kotlin',
|
|
46
47
|
'box',
|
|
47
48
|
'list',
|
|
48
49
|
'use',
|
|
@@ -50,7 +51,7 @@ const NODE_COMMAND_NAMES = [
|
|
|
50
51
|
'close',
|
|
51
52
|
'compat',
|
|
52
53
|
'project-report',
|
|
53
|
-
'
|
|
54
|
+
'support',
|
|
54
55
|
'desktop',
|
|
55
56
|
'login',
|
|
56
57
|
'logout',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// what `peach swift` and `peach kotlin` share: a loopback-only guest host
|
|
2
|
+
// and the teardown of the simulator, driver and bridge their open started.
|
|
3
|
+
import { isLoopbackHost } from '../../src/backend-origin'
|
|
4
|
+
import { createBridge } from '../ws-bridge'
|
|
5
|
+
import { runCloseCommand, type OpenCommandResources } from './control'
|
|
6
|
+
import type http from 'node:http'
|
|
7
|
+
|
|
8
|
+
export function isLoopbackGuestRequest(
|
|
9
|
+
req: http.IncomingMessage,
|
|
10
|
+
origin: string,
|
|
11
|
+
): boolean {
|
|
12
|
+
if (req.headers.host !== new URL(origin).host) return false
|
|
13
|
+
if (!req.headers.origin) return true
|
|
14
|
+
try {
|
|
15
|
+
const requestOrigin = new URL(req.headers.origin)
|
|
16
|
+
return (
|
|
17
|
+
(requestOrigin.protocol === 'http:' || requestOrigin.protocol === 'https:') &&
|
|
18
|
+
isLoopbackHost(requestOrigin.hostname)
|
|
19
|
+
)
|
|
20
|
+
} catch {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// answers whether every owned resource closed
|
|
26
|
+
export async function closeGuestResources(
|
|
27
|
+
resources: OpenCommandResources,
|
|
28
|
+
label: string,
|
|
29
|
+
): Promise<boolean> {
|
|
30
|
+
let closed = true
|
|
31
|
+
if (resources.simId && resources.port) {
|
|
32
|
+
try {
|
|
33
|
+
await runCloseCommand(['--sim', resources.simId], { port: resources.port })
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(` ${label} cleanup failed: ${String(error)}`)
|
|
36
|
+
closed = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
resources.driver?.driverId === 'playwright' &&
|
|
41
|
+
resources.driver.launched &&
|
|
42
|
+
resources.driver.pid
|
|
43
|
+
) {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(resources.driver.pid, 'SIGTERM')
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH')) {
|
|
48
|
+
console.error(String(error))
|
|
49
|
+
closed = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (resources.bridge && !resources.bridgeReady) resources.bridge.kill('SIGTERM')
|
|
54
|
+
if (resources.bridge && resources.bridgeReady && resources.port) {
|
|
55
|
+
const bridge = createBridge(resources.port)
|
|
56
|
+
try {
|
|
57
|
+
if ((await bridge.listSims()).length === 0) resources.bridge.kill('SIGTERM')
|
|
58
|
+
else console.log(' preserving the bridge adopted by another simulator')
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(` cannot verify owned bridge cleanup: ${String(error)}`)
|
|
61
|
+
closed = false
|
|
62
|
+
} finally {
|
|
63
|
+
bridge.close()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return closed
|
|
67
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { DEFAULT_DEVICE_BY_PLATFORM, devices } from 'peach-engine/settings/devices'
|
|
5
|
+
import { WebSocket, WebSocketServer } from 'ws'
|
|
6
|
+
import { kotlinGuestBundle } from '../../src/guest-bundle'
|
|
7
|
+
import {
|
|
8
|
+
readKotlinApp,
|
|
9
|
+
type ComposeAndroidPins,
|
|
10
|
+
type KotlinAppProject,
|
|
11
|
+
} from '../../src/kotlin-app'
|
|
12
|
+
import { PeachExit } from '../run-rnx'
|
|
13
|
+
import { runOpenCommand, type OpenCommandResources } from './control'
|
|
14
|
+
import { closeGuestResources, isLoopbackGuestRequest } from './guest-host'
|
|
15
|
+
|
|
16
|
+
const GUEST_PATH = '/__peach/kotlin-guest.js'
|
|
17
|
+
const HMR_PATH = '/__peach/kotlin-hmr'
|
|
18
|
+
// peachsim's Kotlin module: two levels above this file in source (cli/commands)
|
|
19
|
+
// and in the built CLI (dist-cli/chunks)
|
|
20
|
+
const MODULE_ROOT = path.resolve(import.meta.dirname, '../../kotlin')
|
|
21
|
+
|
|
22
|
+
function composePins(): ComposeAndroidPins {
|
|
23
|
+
const toolchain: unknown = JSON.parse(
|
|
24
|
+
fs.readFileSync(path.join(MODULE_ROOT, 'toolchain.json'), 'utf8'),
|
|
25
|
+
)
|
|
26
|
+
const android =
|
|
27
|
+
typeof toolchain === 'object' && toolchain !== null
|
|
28
|
+
? Reflect.get(Reflect.get(toolchain, 'compose') ?? {}, 'android')
|
|
29
|
+
: undefined
|
|
30
|
+
const pin = (name: string) => {
|
|
31
|
+
const value: unknown = android ? Reflect.get(android, name) : undefined
|
|
32
|
+
if (typeof value !== 'string')
|
|
33
|
+
throw new Error(`Kotlin toolchain has no Compose ${name} pin`)
|
|
34
|
+
return value
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
ui: pin('ui'),
|
|
38
|
+
foundation: pin('foundation'),
|
|
39
|
+
runtime: pin('runtime'),
|
|
40
|
+
material3: pin('material3'),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// opens an Android app module the way the device launches it: the manifest's
|
|
45
|
+
// launcher activity, cold-started in Peach's Kotlin process. sources compile
|
|
46
|
+
// locally; an edit recompiles and starts a new process, as a rebuilt app does.
|
|
47
|
+
export async function runKotlin(
|
|
48
|
+
args: string[],
|
|
49
|
+
opts: { port?: number; device?: string; driver?: string; headless?: boolean } = {},
|
|
50
|
+
): Promise<number> {
|
|
51
|
+
const input = [...args]
|
|
52
|
+
const openArgs: string[] = []
|
|
53
|
+
let hostPort = 0
|
|
54
|
+
let activity: string | undefined
|
|
55
|
+
const valueFlags = new Set(['--driver', '--profile', '--cdp-port', '--base-url'])
|
|
56
|
+
const booleanFlags = new Set([
|
|
57
|
+
'--headless',
|
|
58
|
+
'--headed',
|
|
59
|
+
'--ephemeral',
|
|
60
|
+
'--quiet',
|
|
61
|
+
'--proof',
|
|
62
|
+
])
|
|
63
|
+
let modulePath = ''
|
|
64
|
+
for (let index = 0; index < input.length; index++) {
|
|
65
|
+
const arg = input[index]
|
|
66
|
+
if (
|
|
67
|
+
arg === '--host-port' ||
|
|
68
|
+
arg === '--activity' ||
|
|
69
|
+
arg === '--device' ||
|
|
70
|
+
valueFlags.has(arg)
|
|
71
|
+
) {
|
|
72
|
+
const value = input[++index]
|
|
73
|
+
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`)
|
|
74
|
+
if (arg === '--host-port') {
|
|
75
|
+
hostPort = Number(value)
|
|
76
|
+
if (!Number.isInteger(hostPort) || hostPort < 1 || hostPort > 65535)
|
|
77
|
+
throw new Error('--host-port must be a port from 1 to 65535')
|
|
78
|
+
} else if (arg === '--activity') activity = value
|
|
79
|
+
else if (arg === '--device') opts.device = value
|
|
80
|
+
else openArgs.push(arg, value)
|
|
81
|
+
} else if (booleanFlags.has(arg)) openArgs.push(arg)
|
|
82
|
+
else if (arg.startsWith('-')) throw new Error(`unknown peach kotlin flag: ${arg}`)
|
|
83
|
+
else if (modulePath)
|
|
84
|
+
throw new Error('peach kotlin expects one Android app module directory')
|
|
85
|
+
else modulePath = path.resolve(arg)
|
|
86
|
+
}
|
|
87
|
+
if (!modulePath) throw new Error('usage: peach kotlin <android-app-module>')
|
|
88
|
+
const device = opts.device ?? DEFAULT_DEVICE_BY_PLATFORM.android
|
|
89
|
+
const spec: unknown = Reflect.get(devices, device)
|
|
90
|
+
if (!spec || Reflect.get(spec, 'platform') !== 'android')
|
|
91
|
+
throw new Error(
|
|
92
|
+
`peach kotlin runs an Android app; ${device} is not an Android device`,
|
|
93
|
+
)
|
|
94
|
+
const pins = composePins()
|
|
95
|
+
let project: KotlinAppProject = readKotlinApp(modulePath, { activity, pins })
|
|
96
|
+
const host = await import('../../src/kotlin-host')
|
|
97
|
+
const controller = new AbortController()
|
|
98
|
+
const resources: OpenCommandResources = {}
|
|
99
|
+
const sockets = new WebSocketServer({ noServer: true })
|
|
100
|
+
let origin = ''
|
|
101
|
+
const routes = () => ({
|
|
102
|
+
root: project.projectDir,
|
|
103
|
+
moduleRoot: MODULE_ROOT,
|
|
104
|
+
origin,
|
|
105
|
+
hmrPath: HMR_PATH,
|
|
106
|
+
project,
|
|
107
|
+
})
|
|
108
|
+
const server = http.createServer((req, res) => {
|
|
109
|
+
if (!isLoopbackGuestRequest(req, origin)) {
|
|
110
|
+
res.statusCode = 403
|
|
111
|
+
res.end('forbidden Kotlin host origin')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
res.setHeader('access-control-allow-origin', req.headers.origin ?? '*')
|
|
115
|
+
res.setHeader('vary', 'origin')
|
|
116
|
+
res.setHeader('cache-control', 'no-store')
|
|
117
|
+
const pathname = new URL(req.url ?? '/', origin).pathname
|
|
118
|
+
if (pathname === GUEST_PATH) {
|
|
119
|
+
res.setHeader('content-type', 'application/javascript')
|
|
120
|
+
res.end(kotlinGuestBundle())
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
void host.serveKotlinRequest(pathname, res, routes()).then((served) => {
|
|
124
|
+
if (served) return
|
|
125
|
+
res.statusCode = 404
|
|
126
|
+
res.end('unknown Kotlin host asset')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
server.on('upgrade', (req, socket, head) => {
|
|
130
|
+
if (
|
|
131
|
+
!isLoopbackGuestRequest(req, origin) ||
|
|
132
|
+
new URL(req.url ?? '/', origin).pathname !== HMR_PATH
|
|
133
|
+
) {
|
|
134
|
+
socket.destroy()
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
sockets.handleUpgrade(req, socket, head, (client) =>
|
|
138
|
+
sockets.emit('connection', client, req),
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
// an edit rereads the module, rebuilds it, then tells the guest to start a
|
|
142
|
+
// new process; a failed build leaves the running process and prints why
|
|
143
|
+
let building: Promise<void> | undefined
|
|
144
|
+
let pending = false
|
|
145
|
+
const rebuild = () => {
|
|
146
|
+
pending = true
|
|
147
|
+
building ??= (async () => {
|
|
148
|
+
while (pending && !controller.signal.aborted) {
|
|
149
|
+
pending = false
|
|
150
|
+
try {
|
|
151
|
+
project = readKotlinApp(modulePath, { activity, pins })
|
|
152
|
+
const artifact = await host.kotlinAppArtifact(project, MODULE_ROOT, origin)
|
|
153
|
+
console.log(` Kotlin artifact: ${artifact.hash}`)
|
|
154
|
+
const message = JSON.stringify({ type: 'kotlin', packageName: 'kotlin' })
|
|
155
|
+
for (const client of sockets.clients)
|
|
156
|
+
if (client.readyState === WebSocket.OPEN) client.send(message)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})().finally(() => {
|
|
162
|
+
building = undefined
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
const stop = () => controller.abort()
|
|
166
|
+
process.once('SIGINT', stop)
|
|
167
|
+
process.once('SIGTERM', stop)
|
|
168
|
+
let watcher: fs.FSWatcher | undefined
|
|
169
|
+
let code = 0
|
|
170
|
+
try {
|
|
171
|
+
await new Promise<void>((resolve, reject) => {
|
|
172
|
+
server.once('error', reject)
|
|
173
|
+
server.listen(hostPort, '127.0.0.1', () => {
|
|
174
|
+
server.removeListener('error', reject)
|
|
175
|
+
const address = server.address()
|
|
176
|
+
if (!address || typeof address === 'string') {
|
|
177
|
+
reject(new Error('Kotlin host has no loopback port'))
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
origin = `http://127.0.0.1:${address.port}`
|
|
181
|
+
resolve()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
console.log(` Kotlin app: ${project.launcher.name} in ${project.moduleDir}`)
|
|
185
|
+
const artifact = await host.kotlinAppArtifact(project, MODULE_ROOT, origin)
|
|
186
|
+
console.log(` Kotlin artifact: ${artifact.hash}`)
|
|
187
|
+
watcher = fs.watch(project.moduleDir, { recursive: true }, (_event, file) => {
|
|
188
|
+
if (
|
|
189
|
+
file &&
|
|
190
|
+
file
|
|
191
|
+
.split(/[\\/]/)
|
|
192
|
+
.some((segment) => segment === 'build' || segment.startsWith('.'))
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
rebuild()
|
|
196
|
+
})
|
|
197
|
+
controller.signal.throwIfAborted()
|
|
198
|
+
openArgs.push('--device', device)
|
|
199
|
+
if (opts.driver) openArgs.push('--driver', opts.driver)
|
|
200
|
+
if (opts.headless) openArgs.push('--headless')
|
|
201
|
+
await runOpenCommand([`${origin}${GUEST_PATH}`, '--new', '--no-hmr', ...openArgs], {
|
|
202
|
+
port: opts.port,
|
|
203
|
+
resources,
|
|
204
|
+
signal: controller.signal,
|
|
205
|
+
ownerPid: process.pid,
|
|
206
|
+
})
|
|
207
|
+
await new Promise<void>((resolve) => {
|
|
208
|
+
if (controller.signal.aborted) resolve()
|
|
209
|
+
else controller.signal.addEventListener('abort', () => resolve(), { once: true })
|
|
210
|
+
})
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (!controller.signal.aborted) {
|
|
213
|
+
console.error(
|
|
214
|
+
` peach kotlin failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
215
|
+
)
|
|
216
|
+
code = error instanceof PeachExit ? error.code : 1
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
controller.abort()
|
|
220
|
+
process.removeListener('SIGINT', stop)
|
|
221
|
+
process.removeListener('SIGTERM', stop)
|
|
222
|
+
watcher?.close()
|
|
223
|
+
await building
|
|
224
|
+
for (const client of sockets.clients) client.terminate()
|
|
225
|
+
sockets.close()
|
|
226
|
+
server.closeAllConnections()
|
|
227
|
+
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
228
|
+
if (!(await closeGuestResources(resources, 'Kotlin app'))) code = 1
|
|
229
|
+
}
|
|
230
|
+
return code
|
|
231
|
+
}
|
|
@@ -352,7 +352,7 @@ export function formatProjectReport(report: ProjectReport): string {
|
|
|
352
352
|
lines.push(
|
|
353
353
|
...report.notes,
|
|
354
354
|
'',
|
|
355
|
-
'To send this report: peach report
|
|
355
|
+
'To send this report: peach support report "<description>"',
|
|
356
356
|
'Review the preview and explicitly confirm before sending.',
|
|
357
357
|
'',
|
|
358
358
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// peach report
|
|
1
|
+
// peach support report — submit an explicitly approved compatibility report.
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { resolve } from 'node:path'
|
|
@@ -28,6 +28,7 @@ type ReportIssueOptions = {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type ReportIssuePayload = {
|
|
31
|
+
kind: 'report'
|
|
31
32
|
note: string
|
|
32
33
|
timestamp: string
|
|
33
34
|
sootsim_version: string | null
|
|
@@ -114,12 +115,11 @@ export function parseReportIssueOptions(
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const installed = manifest ? installedManifest(options.appDir, manifest) : null
|
|
118
|
+
// the context every support request carries: CLI and runtime versions, the
|
|
119
|
+
// platform, and the dependency compatibility scan of the app directory
|
|
120
|
+
export async function toolchainContext(appDir: string) {
|
|
121
|
+
const manifest = readProjectManifest(appDir)
|
|
122
|
+
const installed = manifest ? installedManifest(appDir, manifest) : null
|
|
123
123
|
const safeVersions = (deps: Record<string, string> | undefined) =>
|
|
124
124
|
deps &&
|
|
125
125
|
Object.fromEntries(
|
|
@@ -140,37 +140,57 @@ export async function createReportIssuePayload(
|
|
|
140
140
|
const runtimeVersion = activeRuntime
|
|
141
141
|
? sanitizeProjectReportVersion(activeRuntime)
|
|
142
142
|
: null
|
|
143
|
+
return {
|
|
144
|
+
activeApp:
|
|
145
|
+
compatibility && compatibility.projectName !== 'unknown'
|
|
146
|
+
? { appName: compatibility.projectName }
|
|
147
|
+
: null,
|
|
148
|
+
reportSource: 'cli' as const,
|
|
149
|
+
runtimeVersion,
|
|
150
|
+
cliVersion,
|
|
151
|
+
platform: process.platform,
|
|
152
|
+
arch: process.arch,
|
|
153
|
+
compatibility,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
143
156
|
|
|
157
|
+
export async function createReportIssuePayload(
|
|
158
|
+
options: Pick<ReportIssueOptions, 'appDir' | 'description'> & { scopeToApp?: boolean },
|
|
159
|
+
now = new Date(),
|
|
160
|
+
): Promise<ReportIssuePayload> {
|
|
161
|
+
const context = await toolchainContext(options.appDir)
|
|
144
162
|
return {
|
|
163
|
+
kind: 'report',
|
|
145
164
|
note: options.description,
|
|
146
165
|
timestamp: now.toISOString(),
|
|
147
|
-
sootsim_version: runtimeVersion,
|
|
148
|
-
cli_version: cliVersion,
|
|
166
|
+
sootsim_version: context.runtimeVersion,
|
|
167
|
+
cli_version: context.cliVersion,
|
|
149
168
|
bundle: {
|
|
150
|
-
|
|
151
|
-
compatibility && compatibility.projectName !== 'unknown'
|
|
152
|
-
? { appName: compatibility.projectName }
|
|
153
|
-
: null,
|
|
154
|
-
reportSource: 'cli',
|
|
155
|
-
runtimeVersion,
|
|
156
|
-
cliVersion,
|
|
157
|
-
platform: process.platform,
|
|
158
|
-
arch: process.arch,
|
|
159
|
-
compatibility,
|
|
169
|
+
...context,
|
|
160
170
|
projectReport: createProjectReport(options.appDir, options.scopeToApp),
|
|
161
171
|
},
|
|
162
172
|
}
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
|
|
175
|
+
// where support requests go and who they come from: the login's origin and
|
|
176
|
+
// bearer, and the account a `peach login --account` sign-in chose. a report
|
|
177
|
+
// may go anonymously; an ask or a status read needs the sign-in.
|
|
178
|
+
export function supportEndpoint(): {
|
|
179
|
+
origin: string
|
|
180
|
+
headers: Record<string, string>
|
|
181
|
+
signedIn: boolean
|
|
182
|
+
} {
|
|
166
183
|
const auth = resolveCliAuth()
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const authorization = auth && auth.kind !== 'github' ? authHeaderValue(auth) : null
|
|
170
|
-
return {
|
|
171
|
-
url: `${origin}/api/sootsim/report-issue`,
|
|
172
|
-
authorization,
|
|
184
|
+
if (!auth || auth.kind === 'github') {
|
|
185
|
+
return { origin: DEFAULT_REPORT_ORIGIN, headers: {}, signedIn: false }
|
|
173
186
|
}
|
|
187
|
+
const headers: Record<string, string> = { authorization: authHeaderValue(auth) }
|
|
188
|
+
if (auth.kind === 'session' && auth.accountId) {
|
|
189
|
+
headers['x-rnx-account-id'] = auth.accountId
|
|
190
|
+
}
|
|
191
|
+
const origin =
|
|
192
|
+
auth.kind === 'session' ? auth.origin.replace(/\/+$/, '') : DEFAULT_REPORT_ORIGIN
|
|
193
|
+
return { origin, headers, signedIn: true }
|
|
174
194
|
}
|
|
175
195
|
|
|
176
196
|
function compatibilitySummary(scan: ScanResult | null): string {
|
|
@@ -197,7 +217,7 @@ export async function runReportIssue(
|
|
|
197
217
|
} catch (error) {
|
|
198
218
|
dependencies.stderr(
|
|
199
219
|
` ${error instanceof Error ? error.message : String(error)}\n` +
|
|
200
|
-
` usage: peach report
|
|
220
|
+
` usage: peach support report [--app <dir>] [--dry-run] [--yes] "<description>"\n`,
|
|
201
221
|
)
|
|
202
222
|
return 1
|
|
203
223
|
}
|
|
@@ -253,13 +273,12 @@ export async function runReportIssue(
|
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
|
|
256
|
-
const endpoint =
|
|
257
|
-
const headers
|
|
258
|
-
if (endpoint.authorization) headers.authorization = endpoint.authorization
|
|
276
|
+
const endpoint = supportEndpoint()
|
|
277
|
+
const headers = { ...endpoint.headers, 'content-type': 'application/json' }
|
|
259
278
|
|
|
260
279
|
let response: Response
|
|
261
280
|
try {
|
|
262
|
-
response = await dependencies.request(endpoint.
|
|
281
|
+
response = await dependencies.request(`${endpoint.origin}/api/sootsim/report-issue`, {
|
|
263
282
|
method: 'POST',
|
|
264
283
|
headers,
|
|
265
284
|
body: JSON.stringify(payload),
|
|
@@ -284,6 +303,10 @@ export async function runReportIssue(
|
|
|
284
303
|
|
|
285
304
|
const parsed: unknown = await response.json().catch(() => null)
|
|
286
305
|
const id = readResponseId(parsed)
|
|
287
|
-
dependencies.stdout(
|
|
306
|
+
dependencies.stdout(
|
|
307
|
+
id
|
|
308
|
+
? `\n sent. Report id: ${id}\n our replies and the fix show in \`peach support status\`.\n`
|
|
309
|
+
: `\n sent. Thank you.\n`,
|
|
310
|
+
)
|
|
288
311
|
return 0
|
|
289
312
|
}
|
package/cli/commands/setup.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from '../../src/home-paths'
|
|
18
18
|
import { rnxRuntime } from '../../src/runtime-delivery'
|
|
19
19
|
import { findDesktopCompanion } from '../desktop-companion'
|
|
20
|
-
import {
|
|
20
|
+
import { PRIVACY_NOTICE } from '../privacy'
|
|
21
21
|
import { confirm, select } from '../prompt'
|
|
22
22
|
import {
|
|
23
23
|
applyRepositorySetup,
|
|
@@ -159,8 +159,8 @@ export async function runSetup(
|
|
|
159
159
|
)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
const productAnalytics =
|
|
163
|
-
const crashReports =
|
|
162
|
+
const productAnalytics = decidePrivacy(options, privacy)
|
|
163
|
+
const crashReports = productAnalytics
|
|
164
164
|
|
|
165
165
|
const repositoryPlan = packageRoot
|
|
166
166
|
? planRepositorySetup({ appDir: packageRoot, cliVersion })
|
|
@@ -337,20 +337,16 @@ async function decideSimulatorDriver(
|
|
|
337
337
|
return choice === 0 ? 'playwright' : 'electron'
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
// diagnostics and crash reports are on unless turned off; setup says so once
|
|
341
|
+
// and records the choice
|
|
342
|
+
function decidePrivacy(
|
|
341
343
|
options: SetupOptions,
|
|
342
344
|
current: ReturnType<typeof readPrivacyPreferences>,
|
|
343
|
-
):
|
|
345
|
+
): boolean {
|
|
344
346
|
if (options.productAnalytics !== undefined) return options.productAnalytics
|
|
345
347
|
if (current.configured) return current.productAnalytics
|
|
346
|
-
if (options.
|
|
347
|
-
|
|
348
|
-
'Diagnostics stay off unless you opt in, including CI, pipes and agent runs. DO_NOT_TRACK overrides opt-in.',
|
|
349
|
-
'Reports include CLI/runtime versions, OS/arch, command name and a random install ID.',
|
|
350
|
-
'They include package incompatibilities, error classes and first-line error/warning messages (300 characters, paths redacted).',
|
|
351
|
-
'Reports exclude arguments, stacks, app source and structured app data.',
|
|
352
|
-
])
|
|
353
|
-
return confirm('Share bounded diagnostics to improve peach?', false)
|
|
348
|
+
if (!options.json) printPromptBlock('Diagnostics', PRIVACY_NOTICE)
|
|
349
|
+
return true
|
|
354
350
|
}
|
|
355
351
|
|
|
356
352
|
async function decideRepository(
|
|
@@ -414,7 +410,7 @@ choices:
|
|
|
414
410
|
--repo | --no-repo
|
|
415
411
|
|
|
416
412
|
automation:
|
|
417
|
-
--yes accept recommended machine defaults;
|
|
413
|
+
--yes accept recommended machine defaults; repo changes stay off
|
|
418
414
|
--ci never prompt or install a background service
|
|
419
415
|
--json emit runtime and cache metadata as JSON
|
|
420
416
|
--dry-run show choices and repository operations without changing anything
|