peachsim 0.1.649 → 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 +93 -28
- 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 +15 -26
- 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 +58 -37
- 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 +139 -32
- package/src/swift-compile.ts +7 -2
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// peach support — ask for help, report a problem, and read our replies
|
|
2
|
+
// (plans/peach-cloud/partner-support.md). every item belongs to the account the
|
|
3
|
+
// CLI login names, so `status` lists them without the caller keeping ids.
|
|
4
|
+
//
|
|
5
|
+
// peach support ask "<question>" [--no-wait] [--wait <minutes>] [--json]
|
|
6
|
+
// peach support report [--app <dir>] [--dry-run] [--yes] "<description>"
|
|
7
|
+
// peach support status [--new] [--json]
|
|
8
|
+
// peach support fund [--amount <usd>]
|
|
9
|
+
|
|
10
|
+
import { resolve } from 'node:path'
|
|
11
|
+
import { TOPUP_DEFAULT_USD } from 'peach-engine/billing/plans'
|
|
12
|
+
import {
|
|
13
|
+
readSharedDesktopAuthSession,
|
|
14
|
+
writeSharedDesktopAuthSession,
|
|
15
|
+
} from '../../src/auth/shared-session'
|
|
16
|
+
import { readSharedConfig, writeSharedConfig } from '../../src/home-paths'
|
|
17
|
+
import { openUrl } from '../open-url'
|
|
18
|
+
import { runReportIssue, supportEndpoint, toolchainContext } from './report-issue'
|
|
19
|
+
|
|
20
|
+
const POLL_MS = 10_000
|
|
21
|
+
const FUND_POLL_MS = 3_000
|
|
22
|
+
const FUND_WAIT_MS = 30 * 60_000
|
|
23
|
+
const DEFAULT_WAIT_MINUTES = 15
|
|
24
|
+
const SEEN_KEY = 'supportSeenAt'
|
|
25
|
+
|
|
26
|
+
type Reply = { body: string; createdAt: number }
|
|
27
|
+
|
|
28
|
+
type Item = {
|
|
29
|
+
id: string
|
|
30
|
+
kind: 'ask' | 'report'
|
|
31
|
+
note: string | null
|
|
32
|
+
status: string
|
|
33
|
+
createdAt: number
|
|
34
|
+
updatedAt: number
|
|
35
|
+
replies: Reply[]
|
|
36
|
+
fixCommit: string | null
|
|
37
|
+
release: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const USAGE =
|
|
41
|
+
' usage:\n' +
|
|
42
|
+
' peach support ask "<question>" [--no-wait] [--wait <minutes>] [--json]\n' +
|
|
43
|
+
' peach support report [--app <dir>] [--dry-run] [--yes] "<description>"\n' +
|
|
44
|
+
' peach support status [--new] [--json]\n' +
|
|
45
|
+
' peach support fund [--amount <usd>]\n'
|
|
46
|
+
|
|
47
|
+
function signInHint(): number {
|
|
48
|
+
process.stderr.write(' sign in first with `peach login`, or set PEACH_API_KEY.\n')
|
|
49
|
+
return 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function field(value: unknown, key: string): unknown {
|
|
53
|
+
return value && typeof value === 'object' ? Reflect.get(value, key) : undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toItem(value: unknown): Item | null {
|
|
57
|
+
const id = field(value, 'id')
|
|
58
|
+
const kind = field(value, 'kind')
|
|
59
|
+
const status = field(value, 'status')
|
|
60
|
+
const createdAt = field(value, 'createdAt')
|
|
61
|
+
const updatedAt = field(value, 'updatedAt')
|
|
62
|
+
const replies = field(value, 'replies')
|
|
63
|
+
if (
|
|
64
|
+
typeof id !== 'string' ||
|
|
65
|
+
(kind !== 'ask' && kind !== 'report') ||
|
|
66
|
+
typeof status !== 'string' ||
|
|
67
|
+
typeof createdAt !== 'number' ||
|
|
68
|
+
typeof updatedAt !== 'number' ||
|
|
69
|
+
!Array.isArray(replies)
|
|
70
|
+
) {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
const note = field(value, 'note')
|
|
74
|
+
const fixCommit = field(value, 'fixCommit')
|
|
75
|
+
const release = field(value, 'release')
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
kind,
|
|
79
|
+
note: typeof note === 'string' ? note : null,
|
|
80
|
+
status,
|
|
81
|
+
createdAt,
|
|
82
|
+
updatedAt,
|
|
83
|
+
fixCommit: typeof fixCommit === 'string' ? fixCommit : null,
|
|
84
|
+
release: typeof release === 'string' ? release : null,
|
|
85
|
+
replies: replies.flatMap((reply) => {
|
|
86
|
+
const body = field(reply, 'body')
|
|
87
|
+
const at = field(reply, 'createdAt')
|
|
88
|
+
return typeof body === 'string' && typeof at === 'number'
|
|
89
|
+
? [{ body, createdAt: at }]
|
|
90
|
+
: []
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// the server's error message, never an html error page
|
|
96
|
+
async function failure(response: Response): Promise<string> {
|
|
97
|
+
// support routes answer { error }, billing routes { message }
|
|
98
|
+
const body: unknown = await response.json().catch(() => null)
|
|
99
|
+
const message = field(body, 'error') ?? field(body, 'message')
|
|
100
|
+
return `${response.status} ${typeof message === 'string' ? message : response.statusText}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function fetchItems(id?: string): Promise<Item[]> {
|
|
104
|
+
const target = supportEndpoint()
|
|
105
|
+
const url = new URL('/api/sootsim/support', target.origin)
|
|
106
|
+
if (id) url.searchParams.set('id', id)
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
headers: target.headers,
|
|
109
|
+
signal: AbortSignal.timeout(15_000),
|
|
110
|
+
})
|
|
111
|
+
if (!response.ok) throw new Error(await failure(response))
|
|
112
|
+
const items = field(await response.json(), 'items')
|
|
113
|
+
return Array.isArray(items) ? items.flatMap((item) => toItem(item) ?? []) : []
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// an ask is done once it is answered or closed without one
|
|
117
|
+
function settled(item: Item): boolean {
|
|
118
|
+
return !['received', 'answering', 'waitlisted'].includes(item.status)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function indent(text: string, prefix = ' '): string {
|
|
122
|
+
return text
|
|
123
|
+
.trim()
|
|
124
|
+
.split('\n')
|
|
125
|
+
.map((line) => `${prefix}${line}`)
|
|
126
|
+
.join('\n')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function summary(note: string | null): string {
|
|
130
|
+
const line = (note ?? '').trim().split('\n')[0] ?? ''
|
|
131
|
+
return line.length > 90 ? `${line.slice(0, 89)}…` : line
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatItem(item: Item, seenAt: number): string {
|
|
135
|
+
const fresh = item.updatedAt > seenAt || item.replies.some((r) => r.createdAt > seenAt)
|
|
136
|
+
const lines = [
|
|
137
|
+
` ${fresh ? '* ' : ''}${item.kind} ${item.id} ${item.status} ${new Date(item.createdAt).toISOString().slice(0, 16).replace('T', ' ')}`,
|
|
138
|
+
` ${summary(item.note)}`,
|
|
139
|
+
]
|
|
140
|
+
if (item.fixCommit) {
|
|
141
|
+
lines.push(
|
|
142
|
+
item.release
|
|
143
|
+
? ` fixed in ${item.fixCommit.slice(0, 10)}, shipped in runtime v${item.release} (\`peach upgrade\` picks it up)`
|
|
144
|
+
: ` fixed in ${item.fixCommit.slice(0, 10)}, not in a runtime release yet`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
for (const reply of item.replies) {
|
|
148
|
+
lines.push(
|
|
149
|
+
` reply ${new Date(reply.createdAt).toISOString().slice(0, 16).replace('T', ' ')}${reply.createdAt > seenAt ? ' (new)' : ''}:`,
|
|
150
|
+
indent(reply.body, ' '),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function flagValue(args: string[], flag: string): string | null {
|
|
157
|
+
const index = args.indexOf(flag)
|
|
158
|
+
if (index === -1) return null
|
|
159
|
+
const value = args[index + 1]
|
|
160
|
+
if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`)
|
|
161
|
+
return value
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// an ask sends the question and the toolchain context: CLI and runtime
|
|
165
|
+
// versions and the dependency compatibility scan of the app directory. never
|
|
166
|
+
// source, env, logs, or screenshots.
|
|
167
|
+
async function askPayload(question: string, appDir: string) {
|
|
168
|
+
const context = await toolchainContext(appDir)
|
|
169
|
+
return {
|
|
170
|
+
kind: 'ask',
|
|
171
|
+
note: question,
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
sootsim_version: context.runtimeVersion,
|
|
174
|
+
cli_version: context.cliVersion,
|
|
175
|
+
bundle: context,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function runAsk(args: string[]): Promise<number> {
|
|
180
|
+
const json = args.includes('--json')
|
|
181
|
+
const noWait = args.includes('--no-wait')
|
|
182
|
+
let waitMinutes = DEFAULT_WAIT_MINUTES
|
|
183
|
+
let app: string | null
|
|
184
|
+
try {
|
|
185
|
+
const wait = flagValue(args, '--wait')
|
|
186
|
+
if (wait) waitMinutes = Number(wait)
|
|
187
|
+
app = flagValue(args, '--app')
|
|
188
|
+
} catch (error) {
|
|
189
|
+
process.stderr.write(` ${error instanceof Error ? error.message : error}\n${USAGE}`)
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
if (!Number.isFinite(waitMinutes) || waitMinutes < 0) {
|
|
193
|
+
process.stderr.write(` --wait takes minutes\n${USAGE}`)
|
|
194
|
+
return 1
|
|
195
|
+
}
|
|
196
|
+
const valued = new Set(['--wait', '--app'])
|
|
197
|
+
const words = args.filter(
|
|
198
|
+
(arg, i) => !arg.startsWith('-') && !valued.has(args[i - 1] ?? ''),
|
|
199
|
+
)
|
|
200
|
+
const question = words.join(' ').trim()
|
|
201
|
+
if (!question) {
|
|
202
|
+
process.stderr.write(` ask a question\n${USAGE}`)
|
|
203
|
+
return 1
|
|
204
|
+
}
|
|
205
|
+
const target = supportEndpoint()
|
|
206
|
+
if (!target.signedIn) return signInHint()
|
|
207
|
+
|
|
208
|
+
const response = await fetch(new URL('/api/sootsim/report-issue', target.origin), {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { ...target.headers, 'content-type': 'application/json' },
|
|
211
|
+
body: JSON.stringify(await askPayload(question, resolve(app ?? '.'))),
|
|
212
|
+
signal: AbortSignal.timeout(15_000),
|
|
213
|
+
}).catch((error: unknown) => error)
|
|
214
|
+
if (!(response instanceof Response)) {
|
|
215
|
+
process.stderr.write(` ask failed: ${String(response)}\n`)
|
|
216
|
+
return 1
|
|
217
|
+
}
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
process.stderr.write(` ask failed: ${await failure(response)}\n`)
|
|
220
|
+
return 1
|
|
221
|
+
}
|
|
222
|
+
const body: unknown = await response.json().catch(() => null)
|
|
223
|
+
const id = field(body, 'id')
|
|
224
|
+
if (typeof id !== 'string') {
|
|
225
|
+
process.stderr.write(' ask failed: the server returned no id\n')
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
// an ask past the account's unapproved allowance is kept, queued until approval
|
|
229
|
+
if (field(body, 'status') === 'waitlisted') {
|
|
230
|
+
const message = String(field(body, 'message') ?? '')
|
|
231
|
+
if (json)
|
|
232
|
+
process.stdout.write(`${JSON.stringify({ id, status: 'waitlisted', message })}\n`)
|
|
233
|
+
else process.stdout.write(`\n asked (${id}), queued: ${message}.\n\n`)
|
|
234
|
+
return 0
|
|
235
|
+
}
|
|
236
|
+
if (!json) {
|
|
237
|
+
process.stdout.write(
|
|
238
|
+
`\n asked (${id}). Sent the question, CLI and runtime versions, and the\n` +
|
|
239
|
+
` dependency compatibility scan; no source, env, logs or screenshots.\n`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
if (noWait || waitMinutes === 0) {
|
|
243
|
+
if (json) process.stdout.write(`${JSON.stringify({ id, status: 'received' })}\n`)
|
|
244
|
+
else process.stdout.write(` the answer shows in \`peach support status\`.\n\n`)
|
|
245
|
+
return 0
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!json)
|
|
249
|
+
process.stdout.write(` waiting up to ${waitMinutes} minutes for the answer…\n`)
|
|
250
|
+
const deadline = Date.now() + waitMinutes * 60_000
|
|
251
|
+
let last: Item | null = null
|
|
252
|
+
while (Date.now() < deadline) {
|
|
253
|
+
await new Promise((done) => setTimeout(done, POLL_MS))
|
|
254
|
+
last = (await fetchItems(id).catch(() => []))[0] ?? last
|
|
255
|
+
if (last && settled(last)) break
|
|
256
|
+
}
|
|
257
|
+
if (json) {
|
|
258
|
+
process.stdout.write(`${JSON.stringify(last ?? { id, status: 'received' })}\n`)
|
|
259
|
+
return 0
|
|
260
|
+
}
|
|
261
|
+
if (!last || !settled(last)) {
|
|
262
|
+
process.stdout.write(
|
|
263
|
+
` still working on it. \`peach support status\` shows the answer when it lands.\n\n`,
|
|
264
|
+
)
|
|
265
|
+
return 0
|
|
266
|
+
}
|
|
267
|
+
const answer = last.replies.at(-1)?.body
|
|
268
|
+
process.stdout.write(answer ? `\n${indent(answer, ' ')}\n\n` : ` ${last.status}\n\n`)
|
|
269
|
+
return 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// opens a prepaid top-up checkout in the default browser, waits for it to be
|
|
273
|
+
// paid, and binds the funded account to this login
|
|
274
|
+
async function runFund(args: string[]): Promise<number> {
|
|
275
|
+
const at = args.indexOf('--amount')
|
|
276
|
+
const amountUsd = at >= 0 ? Number(args[at + 1]) : TOPUP_DEFAULT_USD
|
|
277
|
+
if (!Number.isFinite(amountUsd) || amountUsd <= 0) {
|
|
278
|
+
process.stderr.write(` --amount takes dollars\n${USAGE}`)
|
|
279
|
+
return 1
|
|
280
|
+
}
|
|
281
|
+
const target = supportEndpoint()
|
|
282
|
+
if (!target.signedIn) return signInHint()
|
|
283
|
+
const created = await fetch(new URL('/api/sootsim/billing/checkout', target.origin), {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { ...target.headers, 'content-type': 'application/json' },
|
|
286
|
+
body: JSON.stringify({ amountUsd }),
|
|
287
|
+
signal: AbortSignal.timeout(15_000),
|
|
288
|
+
}).catch((error: unknown) => error)
|
|
289
|
+
if (!(created instanceof Response)) {
|
|
290
|
+
process.stderr.write(` fund failed: ${String(created)}\n`)
|
|
291
|
+
return 1
|
|
292
|
+
}
|
|
293
|
+
if (!created.ok) {
|
|
294
|
+
process.stderr.write(` fund failed: ${await failure(created)}\n`)
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
const body: unknown = await created.json().catch(() => null)
|
|
298
|
+
const url = field(body, 'url')
|
|
299
|
+
const id = field(body, 'id')
|
|
300
|
+
if (typeof url !== 'string' || typeof id !== 'string') {
|
|
301
|
+
process.stderr.write(' fund failed: the server returned no checkout\n')
|
|
302
|
+
return 1
|
|
303
|
+
}
|
|
304
|
+
process.stdout.write(`\n opening the payment page in your browser:\n ${url}\n`)
|
|
305
|
+
await openUrl(url, { background: false }).catch(() => {
|
|
306
|
+
process.stdout.write(' could not open a browser; open the link above.\n')
|
|
307
|
+
})
|
|
308
|
+
process.stdout.write(' waiting for the payment to finish…\n')
|
|
309
|
+
|
|
310
|
+
const statusUrl = new URL('/api/sootsim/billing/checkout-status', target.origin)
|
|
311
|
+
statusUrl.searchParams.set('id', id)
|
|
312
|
+
const deadline = Date.now() + FUND_WAIT_MS
|
|
313
|
+
while (Date.now() < deadline) {
|
|
314
|
+
await new Promise((done) => setTimeout(done, FUND_POLL_MS))
|
|
315
|
+
const response = await fetch(statusUrl, {
|
|
316
|
+
headers: target.headers,
|
|
317
|
+
signal: AbortSignal.timeout(15_000),
|
|
318
|
+
}).catch(() => null)
|
|
319
|
+
if (!response?.ok) continue
|
|
320
|
+
const state: unknown = await response.json().catch(() => null)
|
|
321
|
+
if (field(state, 'status') === 'expired') {
|
|
322
|
+
process.stdout.write(' the payment page expired without a payment.\n\n')
|
|
323
|
+
return 1
|
|
324
|
+
}
|
|
325
|
+
if (field(state, 'paid') !== true) continue
|
|
326
|
+
const accountId = field(state, 'accountId')
|
|
327
|
+
const session = readSharedDesktopAuthSession()
|
|
328
|
+
if (typeof accountId === 'string' && session && session.accountId !== accountId) {
|
|
329
|
+
writeSharedDesktopAuthSession({ ...session, accountId })
|
|
330
|
+
}
|
|
331
|
+
process.stdout.write(' funded. this login now uses the funded account.\n\n')
|
|
332
|
+
return 0
|
|
333
|
+
}
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
' no payment yet. run `peach support fund` again once it is done.\n\n',
|
|
336
|
+
)
|
|
337
|
+
return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function runStatus(args: string[]): Promise<number> {
|
|
341
|
+
if (!supportEndpoint().signedIn) return signInHint()
|
|
342
|
+
let items: Item[]
|
|
343
|
+
try {
|
|
344
|
+
items = await fetchItems()
|
|
345
|
+
} catch (error) {
|
|
346
|
+
process.stderr.write(
|
|
347
|
+
` status failed: ${error instanceof Error ? error.message : error}\n`,
|
|
348
|
+
)
|
|
349
|
+
return 1
|
|
350
|
+
}
|
|
351
|
+
const seen = readSharedConfig()[SEEN_KEY]
|
|
352
|
+
const seenAt = typeof seen === 'number' ? seen : 0
|
|
353
|
+
const isNew = (item: Item) =>
|
|
354
|
+
item.updatedAt > seenAt || item.replies.some((reply) => reply.createdAt > seenAt)
|
|
355
|
+
const shown = args.includes('--new') ? items.filter(isNew) : items
|
|
356
|
+
writeSharedConfig({ [SEEN_KEY]: Date.now() })
|
|
357
|
+
if (args.includes('--json')) {
|
|
358
|
+
process.stdout.write(
|
|
359
|
+
`${JSON.stringify({ items: shown.map((item) => ({ ...item, new: isNew(item) })) }, null, 2)}\n`,
|
|
360
|
+
)
|
|
361
|
+
return 0
|
|
362
|
+
}
|
|
363
|
+
if (shown.length === 0) {
|
|
364
|
+
process.stdout.write(
|
|
365
|
+
args.includes('--new')
|
|
366
|
+
? '\n nothing new since the last check.\n\n'
|
|
367
|
+
: '\n no asks or reports yet. `peach support ask "<question>"` asks for help.\n\n',
|
|
368
|
+
)
|
|
369
|
+
return 0
|
|
370
|
+
}
|
|
371
|
+
process.stdout.write(
|
|
372
|
+
`\n${shown.map((item) => formatItem(item, seenAt)).join('\n\n')}\n\n` +
|
|
373
|
+
` * changed since the last check\n\n`,
|
|
374
|
+
)
|
|
375
|
+
return 0
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function runSupport(args: string[]): Promise<number> {
|
|
379
|
+
const [sub, ...rest] = args
|
|
380
|
+
switch (sub) {
|
|
381
|
+
case 'ask':
|
|
382
|
+
return runAsk(rest)
|
|
383
|
+
case 'report':
|
|
384
|
+
return runReportIssue(rest)
|
|
385
|
+
case 'status':
|
|
386
|
+
return runStatus(rest)
|
|
387
|
+
case 'fund':
|
|
388
|
+
return runFund(rest)
|
|
389
|
+
case undefined:
|
|
390
|
+
case 'help':
|
|
391
|
+
case '--help':
|
|
392
|
+
case '-h':
|
|
393
|
+
process.stdout.write(USAGE)
|
|
394
|
+
return 0
|
|
395
|
+
default:
|
|
396
|
+
process.stderr.write(` unknown support command: ${sub}\n${USAGE}`)
|
|
397
|
+
return 1
|
|
398
|
+
}
|
|
399
|
+
}
|
package/cli/commands/swift.ts
CHANGED
|
@@ -2,13 +2,13 @@ import fs from 'node:fs'
|
|
|
2
2
|
import http from 'node:http'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { WebSocket, WebSocketServer } from 'ws'
|
|
5
|
-
import {
|
|
6
|
-
import { swiftGuestBundle } from '../../src/
|
|
5
|
+
import { resolveContrastBackendOrigin } from '../../src/backend-origin'
|
|
6
|
+
import { swiftGuestBundle } from '../../src/guest-bundle'
|
|
7
7
|
import { SWIFT_HMR_PATH, SWIFT_PACKAGE_PATH } from '../../src/swift-submit'
|
|
8
8
|
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
9
9
|
import { PeachExit } from '../run-rnx'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { runOpenCommand, type OpenCommandResources } from './control'
|
|
11
|
+
import { closeGuestResources, isLoopbackGuestRequest } from './guest-host'
|
|
12
12
|
import type { SwiftEntryModuleArgs } from '../../../sootsim-swift/src/entryModule'
|
|
13
13
|
|
|
14
14
|
export async function runSwift(
|
|
@@ -134,19 +134,7 @@ export async function runSwift(
|
|
|
134
134
|
})
|
|
135
135
|
return building
|
|
136
136
|
}
|
|
137
|
-
const validRequest = (req: http.IncomingMessage)
|
|
138
|
-
if (req.headers.host !== new URL(origin).host) return false
|
|
139
|
-
if (!req.headers.origin) return true
|
|
140
|
-
try {
|
|
141
|
-
const requestOrigin = new URL(req.headers.origin)
|
|
142
|
-
return (
|
|
143
|
-
(requestOrigin.protocol === 'http:' || requestOrigin.protocol === 'https:') &&
|
|
144
|
-
isLoopbackHost(requestOrigin.hostname)
|
|
145
|
-
)
|
|
146
|
-
} catch {
|
|
147
|
-
return false
|
|
148
|
-
}
|
|
149
|
-
}
|
|
137
|
+
const validRequest = (req: http.IncomingMessage) => isLoopbackGuestRequest(req, origin)
|
|
150
138
|
const server = http.createServer((req, res) => {
|
|
151
139
|
if (!validRequest(req)) {
|
|
152
140
|
res.statusCode = 403
|
|
@@ -285,41 +273,7 @@ export async function runSwift(
|
|
|
285
273
|
sockets.close()
|
|
286
274
|
server.closeAllConnections()
|
|
287
275
|
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
288
|
-
if (resources
|
|
289
|
-
try {
|
|
290
|
-
await runCloseCommand(['--sim', resources.simId], { port: resources.port })
|
|
291
|
-
} catch (error) {
|
|
292
|
-
console.error(` Swift preview cleanup failed: ${String(error)}`)
|
|
293
|
-
code = 1
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (
|
|
297
|
-
resources.driver?.driverId === 'playwright' &&
|
|
298
|
-
resources.driver.launched &&
|
|
299
|
-
resources.driver.pid
|
|
300
|
-
) {
|
|
301
|
-
try {
|
|
302
|
-
process.kill(resources.driver.pid, 'SIGTERM')
|
|
303
|
-
} catch (error) {
|
|
304
|
-
if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH')) {
|
|
305
|
-
console.error(String(error))
|
|
306
|
-
code = 1
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (resources.bridge && !resources.bridgeReady) resources.bridge.kill('SIGTERM')
|
|
311
|
-
if (resources.bridge && resources.bridgeReady && resources.port) {
|
|
312
|
-
const bridge = createBridge(resources.port)
|
|
313
|
-
try {
|
|
314
|
-
if ((await bridge.listSims()).length === 0) resources.bridge.kill('SIGTERM')
|
|
315
|
-
else console.log(' preserving the bridge adopted by another simulator')
|
|
316
|
-
} catch (error) {
|
|
317
|
-
console.error(` cannot verify owned bridge cleanup: ${String(error)}`)
|
|
318
|
-
code = 1
|
|
319
|
-
} finally {
|
|
320
|
-
bridge.close()
|
|
321
|
-
}
|
|
322
|
-
}
|
|
276
|
+
if (!(await closeGuestResources(resources, 'Swift preview'))) code = 1
|
|
323
277
|
host.disposeSwiftProject(packagePath)
|
|
324
278
|
}
|
|
325
279
|
return code
|
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
// `peach config telemetry on|off` —
|
|
2
|
-
//
|
|
1
|
+
// `peach config telemetry on|off` — the bounded diagnostics and automatic
|
|
2
|
+
// crash report preference shown by guided setup and Settings. both are on until
|
|
3
|
+
// turned off here.
|
|
3
4
|
//
|
|
4
5
|
// this is the process-wide preference; `DO_NOT_TRACK=1` still forces opt-out
|
|
5
|
-
// for a single run regardless of stored
|
|
6
|
+
// for a single run regardless of the stored choice, including CI and agents.
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
configFilePath,
|
|
9
10
|
readPrivacyPreferences,
|
|
10
11
|
writePrivacyPreferences,
|
|
11
12
|
} from '../../src/home-paths'
|
|
13
|
+
import { PRIVACY_NOTICE } from '../privacy'
|
|
12
14
|
import { rnxExit } from '../run-rnx'
|
|
13
15
|
|
|
14
16
|
function printStatus(): void {
|
|
15
17
|
const preferences = readPrivacyPreferences()
|
|
16
|
-
const enabled = preferences.configured && preferences.productAnalytics
|
|
17
|
-
console.log(` bounded diagnostics are ${enabled ? 'ON' : 'OFF'}`)
|
|
18
|
-
console.log(' fields: CLI/runtime versions, OS/arch, command name, random install ID')
|
|
19
18
|
console.log(
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
console.log(
|
|
23
|
-
' explicit opt-in also applies in CI, pipes and agent runs; DO_NOT_TRACK wins',
|
|
19
|
+
` bounded diagnostics are ${preferences.productAnalytics ? 'ON' : 'OFF'}, automatic crash reports are ${preferences.crashReports ? 'ON' : 'OFF'}`,
|
|
24
20
|
)
|
|
21
|
+
for (const line of PRIVACY_NOTICE) console.log(` ${line}`)
|
|
25
22
|
console.log(` config: ${configFilePath()}`)
|
|
26
23
|
const doNotTrack = process.env.DO_NOT_TRACK
|
|
27
24
|
if (doNotTrack === '1' || doNotTrack === 'true') {
|
|
@@ -41,14 +38,14 @@ export async function runTelemetry(args: string[]): Promise<void> {
|
|
|
41
38
|
return
|
|
42
39
|
|
|
43
40
|
case 'on':
|
|
44
|
-
writePrivacyPreferences({ productAnalytics: true, crashReports:
|
|
45
|
-
console.log('
|
|
41
|
+
writePrivacyPreferences({ productAnalytics: true, crashReports: true })
|
|
42
|
+
console.log(' diagnostics and crash reports ON')
|
|
46
43
|
console.log(` config: ${configFilePath()}`)
|
|
47
44
|
return
|
|
48
45
|
|
|
49
46
|
case 'off':
|
|
50
47
|
writePrivacyPreferences({ productAnalytics: false, crashReports: false })
|
|
51
|
-
console.log('
|
|
48
|
+
console.log(' diagnostics and crash reports OFF')
|
|
52
49
|
console.log(` config: ${configFilePath()}`)
|
|
53
50
|
return
|
|
54
51
|
|
package/cli/main.ts
CHANGED
|
@@ -23,9 +23,9 @@ import { IS_STANDALONE } from './standalone'
|
|
|
23
23
|
// rejections that don't match this shape still get the verbose stack so we
|
|
24
24
|
// don't paper over real bugs.
|
|
25
25
|
async function handleTopLevelError(err: unknown): Promise<never> {
|
|
26
|
-
const { trackCliError, flushCliTelemetry } = await import('./telemetry')
|
|
26
|
+
const { trackCliError, flushCliTelemetry, reportCliCrash } = await import('./telemetry')
|
|
27
27
|
trackCliError(err)
|
|
28
|
-
await flushCliTelemetry()
|
|
28
|
+
await flushCliTelemetry(1)
|
|
29
29
|
const msg = err instanceof Error ? err.message : String(err)
|
|
30
30
|
// peach's known top-level errors are all single-line, narrow strings.
|
|
31
31
|
// everything else gets the full stack so unexpected crashes still surface.
|
|
@@ -48,6 +48,8 @@ async function handleTopLevelError(err: unknown): Promise<never> {
|
|
|
48
48
|
process.stderr.write(` ${msg}\n`)
|
|
49
49
|
process.exit(1)
|
|
50
50
|
}
|
|
51
|
+
// an unexpected crash is ours to fix: it goes to the issue queue on its own
|
|
52
|
+
await reportCliCrash(err)
|
|
51
53
|
// bun can render an async-thrown error's stack with a bare `Error` header, so
|
|
52
54
|
// the message always goes first.
|
|
53
55
|
process.stderr.write(` ${msg}\n`)
|
|
@@ -114,8 +116,8 @@ if (
|
|
|
114
116
|
parsed.command !== 'setup' &&
|
|
115
117
|
parsed.command !== 'telemetry'
|
|
116
118
|
) {
|
|
117
|
-
const {
|
|
118
|
-
|
|
119
|
+
const { noticePrivacyDefaults } = await import('./privacy')
|
|
120
|
+
noticePrivacyDefaults()
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
// one-shot "engine upgraded in the background" banner. the daemon's hourly
|
|
@@ -229,7 +231,7 @@ async function scheduleAutomaticCliUpdate(exitCode: number): Promise<void> {
|
|
|
229
231
|
|
|
230
232
|
async function exitWithFlush(code: number): Promise<never> {
|
|
231
233
|
const { flushCliTelemetry } = await import('./telemetry')
|
|
232
|
-
await flushCliTelemetry()
|
|
234
|
+
await flushCliTelemetry(code)
|
|
233
235
|
await scheduleAutomaticCliUpdate(code)
|
|
234
236
|
await scheduleAutomaticCleanup()
|
|
235
237
|
process.exit(code)
|
|
@@ -562,6 +564,17 @@ try {
|
|
|
562
564
|
)
|
|
563
565
|
}
|
|
564
566
|
|
|
567
|
+
case 'kotlin': {
|
|
568
|
+
if (globalSimTarget) {
|
|
569
|
+
console.error(' peach kotlin owns a new preview and cannot target --sim')
|
|
570
|
+
await exitWithFlush(1)
|
|
571
|
+
}
|
|
572
|
+
const { runKotlin } = await import('./commands/kotlin')
|
|
573
|
+
await exitWithFlush(
|
|
574
|
+
await runKotlin(parsed.commandArgs, { port, device, driver, headless }),
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
565
578
|
case 'preview': {
|
|
566
579
|
const { runPreview } = await import('./commands/preview')
|
|
567
580
|
await exitWithFlush(await runPreview(parsed.commandArgs))
|
|
@@ -614,10 +627,9 @@ try {
|
|
|
614
627
|
await exitWithFlush(runProjectReport(parsed.commandArgs))
|
|
615
628
|
}
|
|
616
629
|
|
|
617
|
-
case '
|
|
618
|
-
const {
|
|
619
|
-
|
|
620
|
-
await exitWithFlush(code)
|
|
630
|
+
case 'support': {
|
|
631
|
+
const { runSupport } = await import('./commands/support')
|
|
632
|
+
await exitWithFlush(await runSupport(parsed.commandArgs))
|
|
621
633
|
}
|
|
622
634
|
|
|
623
635
|
case 'desktop': {
|
|
@@ -734,6 +746,6 @@ try {
|
|
|
734
746
|
}
|
|
735
747
|
|
|
736
748
|
const { flushCliTelemetry } = await import('./telemetry')
|
|
737
|
-
await flushCliTelemetry()
|
|
749
|
+
await flushCliTelemetry(0)
|
|
738
750
|
await scheduleAutomaticCliUpdate(0)
|
|
739
751
|
await scheduleAutomaticCleanup()
|
|
@@ -44,9 +44,16 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
44
44
|
bounded_diagnostics: {
|
|
45
45
|
category: 'bounded-diagnostics',
|
|
46
46
|
destination: 'configured PostHog ingestion host',
|
|
47
|
-
preConsent:
|
|
47
|
+
preConsent: true,
|
|
48
48
|
payload:
|
|
49
|
-
'anonymous install id, CLI/runtime versions, OS/arch, command name, package compatibility fields, error class and bounded path-redacted first-line error/warning messages',
|
|
49
|
+
'anonymous install id, CLI/runtime versions, OS/arch, command name, exit code and duration bucket, package compatibility fields, error class and bounded path-redacted first-line error/warning messages; on until `peach config telemetry off` or DO_NOT_TRACK',
|
|
50
|
+
},
|
|
51
|
+
crash_report: {
|
|
52
|
+
category: 'bounded-diagnostics',
|
|
53
|
+
destination: 'configured Contrast origin, /api/sootsim/report-issue',
|
|
54
|
+
preConsent: true,
|
|
55
|
+
payload:
|
|
56
|
+
'an unexpected CLI crash: error class, bounded path-redacted first-line message, command name, CLI/runtime versions, OS/arch and a dedupe signature, at most once a day per signature; on until `peach config telemetry off` or DO_NOT_TRACK',
|
|
50
57
|
},
|
|
51
58
|
contrast_user_action: {
|
|
52
59
|
category: 'explicit-user-action',
|
|
@@ -544,6 +551,18 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
544
551
|
category: 'contrast_user_action',
|
|
545
552
|
count: 1,
|
|
546
553
|
},
|
|
554
|
+
"packages/peachsim/cli/commands/support.ts :: fetch :: new URL('/api/sootsim/billing/checkout', target.origin)":
|
|
555
|
+
{ category: 'contrast_user_action', count: 1 },
|
|
556
|
+
"packages/peachsim/cli/commands/support.ts :: fetch :: new URL('/api/sootsim/report-issue', target.origin)":
|
|
557
|
+
{ category: 'contrast_user_action', count: 1 },
|
|
558
|
+
'packages/peachsim/cli/commands/support.ts :: fetch :: statusUrl': {
|
|
559
|
+
category: 'contrast_user_action',
|
|
560
|
+
count: 1,
|
|
561
|
+
},
|
|
562
|
+
'packages/peachsim/cli/commands/support.ts :: fetch :: url': {
|
|
563
|
+
category: 'contrast_user_action',
|
|
564
|
+
count: 1,
|
|
565
|
+
},
|
|
547
566
|
'packages/peachsim/cli/commands/runtime.ts :: websocket :: `ws://127.0.0.1:${port}`': {
|
|
548
567
|
category: 'local_development',
|
|
549
568
|
count: 1,
|
|
@@ -578,6 +597,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
578
597
|
category: 'release_notes',
|
|
579
598
|
count: 1,
|
|
580
599
|
},
|
|
600
|
+
'packages/peachsim/cli/telemetry.ts :: fetch :: `${endpoint.origin}/api/sootsim/report-issue`':
|
|
601
|
+
{ category: 'crash_report', count: 1 },
|
|
581
602
|
'packages/peachsim/cli/telemetry.ts :: fetch :: `${config.host}/batch/`': {
|
|
582
603
|
category: 'bounded_diagnostics',
|
|
583
604
|
count: 1,
|