peachsim 0.1.899 → 0.1.908
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 +8 -0
- package/cli/app-fonts.ts +27 -2
- package/cli/commands/box/bundle-plane.ts +13 -4
- package/cli/commands/box.ts +22 -1
- package/cli/commands/control.ts +29 -16
- package/cli/commands/flow.ts +8 -1
- package/cli/commands/platform.ts +15 -2
- package/cli/commands/skills.ts +125 -10
- package/cli/commands/upload.ts +53 -39
- package/cli/flow-live-status.ts +11 -6
- package/detox/expectations.ts +22 -15
- package/detox/index.ts +4 -19
- 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/bundle-channel.cjs +8 -5
- 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 +15 -18
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +8 -5
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +8 -5
- 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 +1 -1
- 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/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.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 +136 -25
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/kotlin/fork/NOTICE +3 -0
- package/kotlin/fork/README.md +10 -2
- package/kotlin/fork/build.sh +14 -14
- package/kotlin/fork/compose-worker-globals.patch +105 -0
- package/kotlin/fork/fork.json +3 -3
- package/kotlin/fork/publish.ts +8 -1
- package/kotlin/fork/skia-android-shaping.patch +30 -0
- package/kotlin/fork/skiko-local.init.gradle +10 -0
- package/kotlin/fork/skiko-worker-clock.patch +17 -0
- package/kotlin/toolchain.json +9 -9
- package/package.json +1 -1
- package/skills/peach-debug/SKILL.md +15 -153
- package/skills/peach-debug/references/accessibility.md +102 -0
- package/skills/peach-debug/references/commands.md +56 -0
- package/src/expo-swift-host.ts +15 -4
- package/src/vite-plugin.ts +3 -3
package/README.md
CHANGED
|
@@ -215,6 +215,14 @@ bridge always addresses the **shell worker**, never an undifferentiated sim.
|
|
|
215
215
|
own dev server. It is used to build the engine/shell and to load external RN
|
|
216
216
|
apps — it is **not** the published `peachsim/vite` export.
|
|
217
217
|
|
|
218
|
+
Preview uploads retain one response snapshot in the simulator and transfer
|
|
219
|
+
its base64 bodies in bounded slices, including authenticated API responses.
|
|
220
|
+
The CLI removes that temporary snapshot after transfer. A completed upload
|
|
221
|
+
publishes its trace URL through a fresh bridge connection because compression
|
|
222
|
+
and HTTP upload can outlive the interactive socket's idle lifetime.
|
|
223
|
+
The finished test duration uses the last execution status timestamp; publishing
|
|
224
|
+
the later trace URL notifies viewers without extending that duration.
|
|
225
|
+
|
|
218
226
|
## Key components
|
|
219
227
|
|
|
220
228
|
| component | role | key files |
|
package/cli/app-fonts.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import {
|
|
6
6
|
expoFontPathsFromPlugins,
|
|
7
|
+
nativeLinkedFontSpecsFromPaths,
|
|
7
8
|
metroFontSpecsFromPaths,
|
|
8
9
|
type ConfigPluginFontSpec,
|
|
9
10
|
} from 'peach-engine/engine/config-plugin-fonts'
|
|
@@ -27,7 +28,14 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
27
28
|
}): DiscoveredAppFontSpec[] {
|
|
28
29
|
const metroOrigin = loopbackOriginOf(opts.bundleUrl)
|
|
29
30
|
if (!metroOrigin) return []
|
|
31
|
+
return metroFontSpecsFromPaths(discoverNativeLinkedAppFontPaths(opts), metroOrigin)
|
|
32
|
+
}
|
|
30
33
|
|
|
34
|
+
function discoverNativeLinkedAppFontPaths(opts: {
|
|
35
|
+
projectDir: string
|
|
36
|
+
platform?: string
|
|
37
|
+
bundleUrl?: string
|
|
38
|
+
}): string[] {
|
|
31
39
|
const project = resolveAppProject(opts.projectDir)
|
|
32
40
|
const selectedAppDir = project?.appDir ?? null
|
|
33
41
|
if (!selectedAppDir) return []
|
|
@@ -142,7 +150,7 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
142
150
|
// the bundle path is the entry relative to metro's root, so a bundle at
|
|
143
151
|
// /apps/app/index.bundle served for /repo/apps/app puts that root at /repo.
|
|
144
152
|
const bundleDir = path.posix
|
|
145
|
-
.dirname(new URL(opts.bundleUrl).pathname)
|
|
153
|
+
.dirname(opts.bundleUrl ? new URL(opts.bundleUrl).pathname : '/')
|
|
146
154
|
.replace(/^\/+/, '')
|
|
147
155
|
const appRelativeDir = bundleDir.split('/').join(path.sep)
|
|
148
156
|
const metroRootCandidates = [selectedAppDir]
|
|
@@ -258,7 +266,24 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
258
266
|
} catch {}
|
|
259
267
|
}
|
|
260
268
|
|
|
261
|
-
return
|
|
269
|
+
return [...new Set(fontPaths)]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// prebuilt guests cannot discover files linked into their native binary.
|
|
273
|
+
// carry the same declarations as self-contained asset urls beside their JS.
|
|
274
|
+
export function readNativeLinkedAppFontsWire(opts: {
|
|
275
|
+
projectDir: string
|
|
276
|
+
platform?: string
|
|
277
|
+
}): string {
|
|
278
|
+
if (!resolveAppProject(opts.projectDir)) {
|
|
279
|
+
throw new Error(`native font project is unavailable: ${opts.projectDir}`)
|
|
280
|
+
}
|
|
281
|
+
return encodeAppFontsWire(
|
|
282
|
+
nativeLinkedFontSpecsFromPaths(discoverNativeLinkedAppFontPaths(opts), (fontPath) => {
|
|
283
|
+
const bytes = fs.readFileSync(path.resolve(opts.projectDir, fontPath))
|
|
284
|
+
return `data:${fontContentType(fontPath)};base64,${bytes.toString('base64')}`
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
262
287
|
}
|
|
263
288
|
|
|
264
289
|
// resolve a metro dev server's project root. Expo's middleware advertises it
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
PREBUILT_BUNDLE_DEVICE_PATH,
|
|
13
|
+
prebuiltAppFontsPartPath,
|
|
13
14
|
PREBUILT_BUNDLE_PART_BYTES,
|
|
14
15
|
prebuiltBundlePartPath,
|
|
15
16
|
} from '@peach/box/template'
|
|
@@ -19,16 +20,24 @@ export class BundleFilePlane implements ProjectFilePlane {
|
|
|
19
20
|
private readonly parts = new Map<string, Uint8Array>()
|
|
20
21
|
private readonly entries = new Map<string, ProjectFileMeta>()
|
|
21
22
|
|
|
22
|
-
constructor(bundle: Uint8Array, device: string | null) {
|
|
23
|
+
constructor(bundle: Uint8Array, device: string | null, appFonts = '') {
|
|
23
24
|
if (bundle.byteLength === 0) throw new Error('a Box bundle cannot be empty')
|
|
25
|
+
if (appFonts)
|
|
26
|
+
this.recordParts(new TextEncoder().encode(appFonts), prebuiltAppFontsPartPath)
|
|
24
27
|
if (device) this.record(PREBUILT_BUNDLE_DEVICE_PATH, new TextEncoder().encode(device))
|
|
28
|
+
this.recordParts(bundle, prebuiltBundlePartPath)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private recordParts(content: Uint8Array, partPath: (index: number) => string): void {
|
|
25
32
|
for (
|
|
26
33
|
let offset = 0, index = 0;
|
|
27
|
-
offset <
|
|
34
|
+
offset < content.byteLength;
|
|
28
35
|
offset += PREBUILT_BUNDLE_PART_BYTES, index++
|
|
29
36
|
) {
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
this.record(
|
|
38
|
+
partPath(index),
|
|
39
|
+
content.subarray(offset, offset + PREBUILT_BUNDLE_PART_BYTES),
|
|
40
|
+
)
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
|
package/cli/commands/box.ts
CHANGED
|
@@ -30,8 +30,10 @@ import {
|
|
|
30
30
|
heartbeatSession,
|
|
31
31
|
resolveCloudBoxReference,
|
|
32
32
|
} from 'peach-cloud-box/client'
|
|
33
|
+
import { getRuntimePlatform, isDeviceModel } from 'peach-engine/settings/devices'
|
|
33
34
|
import { readDefaultBox, writeDefaultBox } from '../../src/home-paths'
|
|
34
35
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
36
|
+
import { readNativeLinkedAppFontsWire } from '../app-fonts'
|
|
35
37
|
import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
|
|
36
38
|
import { produceCloudBoxArtifact, type CloudBoxArtifactProduction } from '../cloud-client'
|
|
37
39
|
import { openUrl } from '../open-url'
|
|
@@ -523,6 +525,7 @@ function cloudBoxClient(options: {
|
|
|
523
525
|
*/
|
|
524
526
|
export async function createPrebuiltBundleBox(options: {
|
|
525
527
|
bundle: Uint8Array
|
|
528
|
+
nativeProjectDir?: string
|
|
526
529
|
// the simulator's device model, or null for the Box page's default
|
|
527
530
|
device: string | null
|
|
528
531
|
name: string
|
|
@@ -536,7 +539,19 @@ export async function createPrebuiltBundleBox(options: {
|
|
|
536
539
|
name: options.name,
|
|
537
540
|
stack: prebuiltBundleBoxTemplate.stack,
|
|
538
541
|
template: BOX_TEMPLATES.resolve({ name: prebuiltBundleBoxTemplate.name }).provenance,
|
|
539
|
-
source: new BundleFilePlane(
|
|
542
|
+
source: new BundleFilePlane(
|
|
543
|
+
options.bundle,
|
|
544
|
+
options.device,
|
|
545
|
+
options.nativeProjectDir
|
|
546
|
+
? readNativeLinkedAppFontsWire({
|
|
547
|
+
projectDir: options.nativeProjectDir,
|
|
548
|
+
platform:
|
|
549
|
+
options.device && isDeviceModel(options.device)
|
|
550
|
+
? getRuntimePlatform(options.device)
|
|
551
|
+
: 'ios',
|
|
552
|
+
})
|
|
553
|
+
: '',
|
|
554
|
+
),
|
|
540
555
|
...(options.secrets ? { secrets: options.secrets } : {}),
|
|
541
556
|
})
|
|
542
557
|
}
|
|
@@ -576,6 +591,11 @@ async function runCloudBoxCreate(
|
|
|
576
591
|
// --bundle IS the template choice: the box runs an app that is already
|
|
577
592
|
// built, so naming a second template would ask for two different boxes.
|
|
578
593
|
const bundlePath = flag(args, 'bundle')
|
|
594
|
+
const nativeProjectDir = flag(args, 'native-project')
|
|
595
|
+
if (nativeProjectDir && !bundlePath) {
|
|
596
|
+
console.error(` ${peach} box create: --native-project requires --bundle.`)
|
|
597
|
+
return 1
|
|
598
|
+
}
|
|
579
599
|
const namedTemplate = flag(args, 'template')
|
|
580
600
|
if (bundlePath && namedTemplate) {
|
|
581
601
|
console.error(
|
|
@@ -687,6 +707,7 @@ async function runCloudBoxCreate(
|
|
|
687
707
|
? await createPrebuiltBundleBox({
|
|
688
708
|
...connection,
|
|
689
709
|
bundle: source.bundle,
|
|
710
|
+
nativeProjectDir,
|
|
690
711
|
device: null,
|
|
691
712
|
name,
|
|
692
713
|
...(secrets ? { secrets } : {}),
|
package/cli/commands/control.ts
CHANGED
|
@@ -241,19 +241,30 @@ export async function ensureBridgeForOpen(
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
detached: true,
|
|
249
|
-
stdio: 'ignore',
|
|
250
|
-
// this bridge does not supply the shell, so it needs no runtime updates.
|
|
251
|
-
env:
|
|
252
|
-
runtimeSource === 'external'
|
|
253
|
-
? { ...process.env, PEACH_UPDATE_CHECK: 'off' }
|
|
254
|
-
: process.env,
|
|
255
|
-
},
|
|
244
|
+
// the bridge outlives open, so it must not hold the command's captured stderr.
|
|
245
|
+
const bridgeLogPath = path.join(
|
|
246
|
+
os.tmpdir(),
|
|
247
|
+
`peach-bridge-${Date.now()}-${process.pid}.log`,
|
|
256
248
|
)
|
|
249
|
+
const bridgeLogFd = fs.openSync(bridgeLogPath, 'a')
|
|
250
|
+
let child: ReturnType<typeof spawn>
|
|
251
|
+
try {
|
|
252
|
+
child = spawn(
|
|
253
|
+
executable,
|
|
254
|
+
[...prefixArgs, 'serve', '--quiet', '--port', String(preferredPort)],
|
|
255
|
+
{
|
|
256
|
+
detached: true,
|
|
257
|
+
stdio: ['ignore', 'ignore', bridgeLogFd],
|
|
258
|
+
// this bridge does not supply the shell, so it needs no runtime updates.
|
|
259
|
+
env:
|
|
260
|
+
runtimeSource === 'external'
|
|
261
|
+
? { ...process.env, PEACH_UPDATE_CHECK: 'off' }
|
|
262
|
+
: process.env,
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
} finally {
|
|
266
|
+
fs.closeSync(bridgeLogFd)
|
|
267
|
+
}
|
|
257
268
|
if (resources) {
|
|
258
269
|
resources.bridge = child
|
|
259
270
|
resources.port = preferredPort
|
|
@@ -298,14 +309,16 @@ export async function ensureBridgeForOpen(
|
|
|
298
309
|
}
|
|
299
310
|
return selectedPort
|
|
300
311
|
}
|
|
301
|
-
if (childExit)
|
|
302
|
-
throw new Error(`peach bridge exited before becoming ready (exit ${childExit})`)
|
|
303
|
-
}
|
|
312
|
+
if (childExit) break
|
|
304
313
|
await sleep(150)
|
|
305
314
|
}
|
|
306
315
|
|
|
316
|
+
const reason = childExit
|
|
317
|
+
? `peach bridge exited before becoming ready (exit ${childExit})`
|
|
318
|
+
: `peach bridge did not start within ${Math.round(OPEN_BRIDGE_SPAWN_TIMEOUT_MS / 1000)}s`
|
|
319
|
+
const bridgeLog = fs.readFileSync(bridgeLogPath, 'utf8').trim().slice(-2000)
|
|
307
320
|
throw new Error(
|
|
308
|
-
|
|
321
|
+
`${reason}${bridgeLog ? `\n${bridgeLog}` : ''}\nbridge log: ${bridgeLogPath}`,
|
|
309
322
|
)
|
|
310
323
|
}
|
|
311
324
|
|
package/cli/commands/flow.ts
CHANGED
|
@@ -955,7 +955,14 @@ flow extension:
|
|
|
955
955
|
}
|
|
956
956
|
const { runUpload } = await import('./upload')
|
|
957
957
|
lastPreviewUploadResult = await runUpload(uploadArgs, {})
|
|
958
|
-
|
|
958
|
+
// upload can outlive the command socket's idle lifetime. publish its
|
|
959
|
+
// completed trace over a fresh connection to the same bridge world.
|
|
960
|
+
const resultBridge = createBridgeFromParsed(parsedBridgeArgs)
|
|
961
|
+
try {
|
|
962
|
+
await driver.liveStatus?.preview(lastPreviewUploadResult.previewUrl, resultBridge)
|
|
963
|
+
} finally {
|
|
964
|
+
resultBridge.close()
|
|
965
|
+
}
|
|
959
966
|
} catch (err: any) {
|
|
960
967
|
lastPreviewUploadError =
|
|
961
968
|
err instanceof PeachExit && err.reason ? err.reason : String(err?.message || err)
|
package/cli/commands/platform.ts
CHANGED
|
@@ -29,7 +29,12 @@ function isDeviceModel(value: string): value is DeviceModel {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// flags this command reads for itself rather than treating as the bundle.
|
|
32
|
-
const REMOTE_VALUE_FLAGS = new Set([
|
|
32
|
+
const REMOTE_VALUE_FLAGS = new Set([
|
|
33
|
+
'--device',
|
|
34
|
+
'--name',
|
|
35
|
+
'--endpoint',
|
|
36
|
+
'--native-project',
|
|
37
|
+
])
|
|
33
38
|
|
|
34
39
|
function remoteInputFromArgs(
|
|
35
40
|
args: string[],
|
|
@@ -37,11 +42,13 @@ function remoteInputFromArgs(
|
|
|
37
42
|
): {
|
|
38
43
|
bundlePath: string
|
|
39
44
|
name: string | null
|
|
45
|
+
nativeProjectDir: string | undefined
|
|
40
46
|
reuse: boolean
|
|
41
47
|
fresh: boolean
|
|
42
48
|
} {
|
|
43
49
|
const positional: string[] = []
|
|
44
50
|
let name: string | null = null
|
|
51
|
+
let nativeProjectDir: string | undefined
|
|
45
52
|
for (let index = 0; index < args.length; index++) {
|
|
46
53
|
const arg = args[index]
|
|
47
54
|
if (arg === '--remote' || arg === '--nano' || arg === '--reuse' || arg === '--new') {
|
|
@@ -51,6 +58,7 @@ function remoteInputFromArgs(
|
|
|
51
58
|
const value = args[++index] ?? null
|
|
52
59
|
if (!value) throw new Error(`peach remote ${platform} ${arg} requires a value`)
|
|
53
60
|
if (arg === '--name') name = value
|
|
61
|
+
if (arg === '--native-project') nativeProjectDir = value
|
|
54
62
|
continue
|
|
55
63
|
}
|
|
56
64
|
const assigned = arg.match(/^(--[a-z-]+)=(.*)$/)
|
|
@@ -59,6 +67,7 @@ function remoteInputFromArgs(
|
|
|
59
67
|
if (!value)
|
|
60
68
|
throw new Error(`peach remote ${platform} ${assigned[1]} requires a value`)
|
|
61
69
|
if (assigned[1] === '--name') name = value
|
|
70
|
+
if (assigned[1] === '--native-project') nativeProjectDir = value
|
|
62
71
|
continue
|
|
63
72
|
}
|
|
64
73
|
if (
|
|
@@ -88,7 +97,7 @@ function remoteInputFromArgs(
|
|
|
88
97
|
if (reuse && fresh) {
|
|
89
98
|
throw new Error(`peach remote ${platform} cannot combine --reuse and --new`)
|
|
90
99
|
}
|
|
91
|
-
return { bundlePath: positional[0], name, reuse, fresh }
|
|
100
|
+
return { bundlePath: positional[0], name, nativeProjectDir, reuse, fresh }
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
/**
|
|
@@ -242,6 +251,8 @@ async function runRemoteCreate(
|
|
|
242
251
|
selectedDevice: string,
|
|
243
252
|
): Promise<number> {
|
|
244
253
|
const input = remoteInputFromArgs(args, platform)
|
|
254
|
+
if (input.nativeProjectDir)
|
|
255
|
+
throw new Error('--native-project requires a prebuilt micro Box')
|
|
245
256
|
const [
|
|
246
257
|
{ authHeaderOrExit, cloudAccountIdOrExit },
|
|
247
258
|
{
|
|
@@ -404,6 +415,7 @@ async function runRemoteBox(
|
|
|
404
415
|
|
|
405
416
|
const existingSession = readCloudSession()
|
|
406
417
|
if (
|
|
418
|
+
!input.nativeProjectDir &&
|
|
407
419
|
existingSession?.service === 'box' &&
|
|
408
420
|
!input.fresh &&
|
|
409
421
|
existingSession.apiOrigin === endpoint &&
|
|
@@ -454,6 +466,7 @@ async function runRemoteBox(
|
|
|
454
466
|
}
|
|
455
467
|
const created = await box.createPrebuiltBundleBox({
|
|
456
468
|
bundle: new Uint8Array(artifact.bytes),
|
|
469
|
+
nativeProjectDir: input.nativeProjectDir,
|
|
457
470
|
device: selectedDevice,
|
|
458
471
|
name: input.name ?? boxNameForBundle(input.bundlePath),
|
|
459
472
|
endpoint,
|
package/cli/commands/skills.ts
CHANGED
|
@@ -137,9 +137,117 @@ function targetFromHome(envName: string, fallbackDir: string): string {
|
|
|
137
137
|
return path.join(home, 'skills')
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function codexTargets(): InstallTarget[] {
|
|
141
|
+
const homes = new Map<string, string>()
|
|
142
|
+
const userHome = process.env.HOME || os.homedir()
|
|
143
|
+
|
|
144
|
+
// 1. Default CODEX_HOME or ~/.codex
|
|
145
|
+
const primaryCodexHome = process.env.CODEX_HOME || path.join(userHome, '.codex')
|
|
146
|
+
homes.set(path.resolve(primaryCodexHome), 'codex')
|
|
147
|
+
|
|
148
|
+
// 2. TM_CODEX_HOMES env var
|
|
149
|
+
const tmCodexHomes = process.env.TM_CODEX_HOMES
|
|
150
|
+
if (tmCodexHomes) {
|
|
151
|
+
for (const h of tmCodexHomes.split(path.delimiter)) {
|
|
152
|
+
if (h.trim()) {
|
|
153
|
+
const resolved = path.resolve(h.trim())
|
|
154
|
+
if (!homes.has(resolved)) {
|
|
155
|
+
homes.set(resolved, path.basename(resolved).replace(/^\./, ''))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Engine configuration from ~/.team-machine/engine.yaml or $TM_HOME/engine.yaml
|
|
162
|
+
const tmHome = process.env.TM_HOME || path.join(userHome, '.team-machine')
|
|
163
|
+
const enginePath = path.join(tmHome, 'engine.yaml')
|
|
164
|
+
if (fs.existsSync(enginePath)) {
|
|
165
|
+
try {
|
|
166
|
+
const content = fs.readFileSync(enginePath, 'utf8')
|
|
167
|
+
let inCodex = false
|
|
168
|
+
let inAccounts = false
|
|
169
|
+
let currentAccount = ''
|
|
170
|
+
for (const line of content.split('\n')) {
|
|
171
|
+
const indent = line.search(/\S/)
|
|
172
|
+
const trimmed = line.trim()
|
|
173
|
+
if (indent === -1) continue
|
|
174
|
+
|
|
175
|
+
if (indent === 4) {
|
|
176
|
+
inCodex = trimmed === 'codex:'
|
|
177
|
+
inAccounts = false
|
|
178
|
+
currentAccount = ''
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!inCodex) continue
|
|
183
|
+
|
|
184
|
+
if (indent === 6 && trimmed === 'accounts:') {
|
|
185
|
+
inAccounts = true
|
|
186
|
+
currentAccount = ''
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!inAccounts) continue
|
|
191
|
+
|
|
192
|
+
if (indent === 8 && trimmed.endsWith(':')) {
|
|
193
|
+
currentAccount = trimmed.slice(0, -1).trim()
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (indent >= 10 && currentAccount) {
|
|
198
|
+
const match = /^home:\s*(.+)$/.exec(trimmed)
|
|
199
|
+
if (match) {
|
|
200
|
+
let homeDir = match[1].trim()
|
|
201
|
+
if (homeDir.startsWith('~')) {
|
|
202
|
+
homeDir = path.join(userHome, homeDir.slice(1))
|
|
203
|
+
}
|
|
204
|
+
const resolved = path.resolve(homeDir)
|
|
205
|
+
if (!homes.has(resolved)) {
|
|
206
|
+
homes.set(resolved, `codex-${currentAccount}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// ignore read error
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 4. Native Codex profile directories in home matching .codex-*
|
|
217
|
+
try {
|
|
218
|
+
for (const entry of fs.readdirSync(userHome)) {
|
|
219
|
+
if (entry.startsWith('.codex-')) {
|
|
220
|
+
const fullPath = path.join(userHome, entry)
|
|
221
|
+
try {
|
|
222
|
+
const stat = fs.statSync(fullPath)
|
|
223
|
+
if (stat.isDirectory()) {
|
|
224
|
+
const resolved = path.resolve(fullPath)
|
|
225
|
+
if (!homes.has(resolved)) {
|
|
226
|
+
homes.set(resolved, entry.replace(/^\./, ''))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// ignore
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const targets: InstallTarget[] = []
|
|
239
|
+
for (const [homePath, label] of homes) {
|
|
240
|
+
targets.push({
|
|
241
|
+
label,
|
|
242
|
+
dir: path.join(homePath, 'skills'),
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
return targets
|
|
246
|
+
}
|
|
247
|
+
|
|
140
248
|
function defaultAgentTargets(): InstallTarget[] {
|
|
141
249
|
return [
|
|
142
|
-
|
|
250
|
+
...codexTargets(),
|
|
143
251
|
{ label: 'claude', dir: targetFromHome('CLAUDE_CONFIG_DIR', '.claude') },
|
|
144
252
|
]
|
|
145
253
|
}
|
|
@@ -162,7 +270,7 @@ export function hasInstalledPeachSkills(): boolean {
|
|
|
162
270
|
function agentTarget(target: string): InstallTarget[] {
|
|
163
271
|
if (target === 'all') return defaultAgentTargets()
|
|
164
272
|
if (target === 'codex') {
|
|
165
|
-
return
|
|
273
|
+
return codexTargets()
|
|
166
274
|
}
|
|
167
275
|
if (target === 'claude') {
|
|
168
276
|
return [{ label: 'claude', dir: targetFromHome('CLAUDE_CONFIG_DIR', '.claude') }]
|
|
@@ -289,14 +397,21 @@ function symlinkPointsTo(dest: string, src: string): boolean {
|
|
|
289
397
|
// holding a byte-identical copy of the skill. only our installer creates
|
|
290
398
|
// those shapes, so replacing them can't lose user work.
|
|
291
399
|
function isOwnLegacyInstall(stat: fs.Stats, dest: string, srcDir: string): boolean {
|
|
292
|
-
// a dangling link into a removed checkout's skills dir is our own install too
|
|
400
|
+
// a dangling link into a removed checkout's skills dir or a link into an obsolete worktree is our own install too
|
|
293
401
|
if (stat.isSymbolicLink()) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
|
|
402
|
+
try {
|
|
403
|
+
const link = fs.readlinkSync(dest)
|
|
404
|
+
const isPeachSkill = link.endsWith(
|
|
405
|
+
`${path.sep}skills${path.sep}${path.basename(srcDir)}`,
|
|
406
|
+
)
|
|
407
|
+
if (isPeachSkill) {
|
|
408
|
+
if (!fs.existsSync(dest)) return true
|
|
409
|
+
if (link.includes(`${path.sep}.worktrees${path.sep}`)) return true
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
return false
|
|
413
|
+
}
|
|
414
|
+
return false
|
|
300
415
|
}
|
|
301
416
|
if (!stat.isDirectory()) return false
|
|
302
417
|
let entries: string[]
|
|
@@ -350,7 +465,7 @@ const LEGACY_BUNDLED_SKILL_DIRS = [
|
|
|
350
465
|
'sootsim-debug',
|
|
351
466
|
'sootsim-perf',
|
|
352
467
|
'sootsim-test',
|
|
353
|
-
'
|
|
468
|
+
'sootsim-visual',
|
|
354
469
|
'rnx-setup',
|
|
355
470
|
'rnx-debug',
|
|
356
471
|
'rnx-perf',
|
package/cli/commands/upload.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// uploads are unlisted-public (unguessable id) but require a signed-in
|
|
6
6
|
// desktop session so the share is associated with the user's account.
|
|
7
7
|
|
|
8
|
-
import { createHash } from 'crypto'
|
|
8
|
+
import { createHash, randomUUID } from 'crypto'
|
|
9
9
|
import { readFileSync } from 'fs'
|
|
10
10
|
import path from 'path'
|
|
11
11
|
import { gzipSync, gunzipSync } from 'zlib'
|
|
@@ -609,45 +609,59 @@ export async function runUpload(
|
|
|
609
609
|
` ${crossOriginStats.count} recorded cross-origin responses ` +
|
|
610
610
|
`(${(crossOriginStats.totalBytes / 1024).toFixed(1)} KiB)`,
|
|
611
611
|
)
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
612
|
+
// retain the snapshot in the page and pull bodies in bounded slices.
|
|
613
|
+
// a complete base64 dump can exceed the bridge's 100 mib frame cap.
|
|
614
|
+
const cacheKey = `__peachCliUploadFetches_${randomUUID()}`
|
|
615
|
+
try {
|
|
616
|
+
const entries = await evalInBridge<
|
|
617
|
+
Array<RecordedEntry & { index: number; bodyBase64Length: number }>
|
|
618
|
+
>(
|
|
619
|
+
bridge,
|
|
620
|
+
`(async () => {
|
|
621
|
+
const rec = window.__sootsimPreviewRecorder
|
|
622
|
+
const workerDump = window.__sootsimDumpWorkerFetchRecorder
|
|
623
|
+
const bundleUrl = ${JSON.stringify(snapshot.bundleUrl)}
|
|
624
|
+
await rec?.flush?.()
|
|
625
|
+
const keep = (r) => {
|
|
626
|
+
try {
|
|
627
|
+
const u = new URL(r.url)
|
|
628
|
+
if (u.origin !== ${JSON.stringify(bundleOrigin)}) return true
|
|
629
|
+
return r.url !== bundleUrl && u.pathname.startsWith('/api/')
|
|
630
|
+
} catch { return false }
|
|
631
|
+
}
|
|
632
|
+
const records = [
|
|
633
|
+
...(rec?.dump ? rec.dump().filter(keep) : []),
|
|
634
|
+
...(typeof workerDump === 'function'
|
|
635
|
+
? (await workerDump()).filter(keep)
|
|
636
|
+
: []),
|
|
637
|
+
]
|
|
638
|
+
window[${JSON.stringify(cacheKey)}] = records
|
|
639
|
+
return records.map(({ bodyBase64, ...entry }, index) => ({
|
|
640
|
+
...entry, index, bodyBase64Length: bodyBase64.length,
|
|
641
|
+
}))
|
|
642
|
+
})()`,
|
|
643
|
+
)
|
|
644
|
+
const dumped: RecordedFetch[] = []
|
|
645
|
+
const chunkChars = 1_000_000
|
|
646
|
+
for (const { index, bodyBase64Length, ...entry } of entries) {
|
|
647
|
+
let bodyBase64 = ''
|
|
648
|
+
for (let offset = 0; offset < bodyBase64Length; offset += chunkChars) {
|
|
649
|
+
const end = Math.min(offset + chunkChars, bodyBase64Length)
|
|
650
|
+
const chunk = await evalInBridge(
|
|
651
|
+
bridge,
|
|
652
|
+
`window[${JSON.stringify(cacheKey)}]?.[${index}]?.bodyBase64.slice(${offset}, ${end})`,
|
|
653
|
+
)
|
|
654
|
+
if (typeof chunk !== 'string' || chunk.length !== end - offset) {
|
|
655
|
+
throw new Error('recorded response snapshot changed during upload')
|
|
656
|
+
}
|
|
657
|
+
bodyBase64 += chunk
|
|
647
658
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
659
|
+
dumped.push({ ...entry, bodyBase64 })
|
|
660
|
+
}
|
|
661
|
+
recordedFetches = mergeRecordedEntries(dumped)
|
|
662
|
+
} finally {
|
|
663
|
+
await evalInBridge(bridge, `delete window[${JSON.stringify(cacheKey)}]`)
|
|
664
|
+
}
|
|
651
665
|
if (assetsOnly) {
|
|
652
666
|
const before = recordedFetches.length
|
|
653
667
|
recordedFetches = recordedFetches.filter((r) =>
|
package/cli/flow-live-status.ts
CHANGED
|
@@ -47,8 +47,9 @@ export class FlowLiveStatusReporter {
|
|
|
47
47
|
private async send(
|
|
48
48
|
payload: Record<string, unknown>,
|
|
49
49
|
simId: string | undefined,
|
|
50
|
+
bridge: WsBridge = this.bridge,
|
|
50
51
|
): Promise<unknown> {
|
|
51
|
-
const reply: unknown = await
|
|
52
|
+
const reply: unknown = await bridge.send({
|
|
52
53
|
type: 'flowStatus',
|
|
53
54
|
simId,
|
|
54
55
|
status: { runId: this.runId, flowName: this.flowName, ...payload },
|
|
@@ -57,7 +58,10 @@ export class FlowLiveStatusReporter {
|
|
|
57
58
|
return reply
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
private async push(
|
|
61
|
+
private async push(
|
|
62
|
+
payload: Record<string, unknown>,
|
|
63
|
+
bridge: WsBridge = this.bridge,
|
|
64
|
+
): Promise<unknown> {
|
|
61
65
|
if (this.disabled) return undefined
|
|
62
66
|
try {
|
|
63
67
|
const simId = this.getSimId?.()
|
|
@@ -75,13 +79,14 @@ export class FlowLiveStatusReporter {
|
|
|
75
79
|
source: this.flowSource,
|
|
76
80
|
},
|
|
77
81
|
simId,
|
|
82
|
+
bridge,
|
|
78
83
|
)
|
|
79
84
|
for (const step of this.completedSteps.values()) {
|
|
80
|
-
await this.send({ phase: 'step', state: 'running', step }, simId)
|
|
85
|
+
await this.send({ phase: 'step', state: 'running', step }, simId, bridge)
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
this.lastSimId = simId
|
|
84
|
-
return await this.send(payload, simId)
|
|
89
|
+
return await this.send(payload, simId, bridge)
|
|
85
90
|
} catch (error) {
|
|
86
91
|
this.disabled = true
|
|
87
92
|
console.log(
|
|
@@ -165,8 +170,8 @@ export class FlowLiveStatusReporter {
|
|
|
165
170
|
|
|
166
171
|
// the finished run's cloud trace url, once --preview has uploaded it. the
|
|
167
172
|
// devtools test pane's Open Trace button opens it.
|
|
168
|
-
async preview(previewUrl: string): Promise<void> {
|
|
169
|
-
await this.push({ phase: 'preview', previewUrl })
|
|
173
|
+
async preview(previewUrl: string, bridge: WsBridge): Promise<void> {
|
|
174
|
+
await this.push({ phase: 'preview', previewUrl }, bridge)
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
// park while the devtools pause toggle is on. checked between top-level
|