peachsim 0.1.668 → 0.1.670
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/cli/commands/control.ts +8 -5
- package/cli/commands/inspect/core.ts +7 -5
- package/cli/commands/inspect/wait-selector.ts +2 -11
- package/cli/commands/inspect.ts +6 -59
- package/cli/commands/kotlin.ts +6 -4
- package/cli/commands/skills.ts +28 -7
- package/cli/commands/upgrade.ts +18 -3
- package/cli/main.ts +21 -0
- package/cli/output-redaction.ts +182 -0
- package/cli/parse-args.ts +1 -0
- 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 +1 -1
- package/dist-lib/host/bridge-host.cjs +29 -19
- 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 +260 -17
- 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 +2 -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 +259 -16
- 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 +115 -0
- package/dist-lib/native-firebase-metadata.mjs +86 -0
- 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 +2 -2
- package/dist-lib/sdk.mjs +2 -2
- package/dist-lib/skills.cjs +479 -204
- 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.cjs +1 -1
- package/dist-lib/vite-swift.cjs +21 -13
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/androidx/activity/ActivityFlags.kt +9 -0
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +25 -4
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +15 -4
- package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +12 -8
- package/kotlin/AppSources/androidx/activity/ExperimentalActivityApi.kt +6 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +50 -9
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +67 -89
- package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -24
- package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerCompat.kt +38 -0
- package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerDispatcherCompat.kt +27 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +5 -5
- package/kotlin/ComposeSources/PeachComposeHost.kt +31 -7
- package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +1 -1
- package/kotlin/fixtures/kotlin-app-variants/Browse.kt +73 -0
- package/kotlin/fixtures/kotlin-app-variants/Focus.kt +62 -0
- package/kotlin/fixtures/kotlin-app-variants/Lists.kt +57 -0
- package/kotlin/fixtures/kotlin-app-variants/Nav.kt +47 -0
- package/kotlin/fixtures/kotlin-app-variants/Nested.kt +64 -0
- package/kotlin/fixtures/kotlin-app-variants/Pan.kt +38 -0
- package/kotlin/fixtures/kotlin-app-variants/Pickers.kt +44 -0
- package/kotlin/fixtures/kotlin-app-variants/Press.kt +43 -0
- package/kotlin/fixtures/kotlin-app-variants/TextWidth.kt +45 -0
- package/kotlin/fixtures/kotlin-app-variants/Transition.kt +69 -0
- package/kotlin/fixtures/kotlin-app-variants/ViewConfig.kt +22 -0
- package/kotlin/toolchain.json +30 -2
- package/package.json +8 -1
- package/skills/contrast/SKILL.md +2 -1
- package/skills/peach-debug/SKILL.md +2 -2
- package/skills/peach-test/SKILL.md +3 -3
- package/src/expo-swift-host.ts +13 -4
- package/src/kotlin-app.ts +35 -16
- package/src/kotlin-host.ts +1 -0
- package/src/metro-plugin.ts +24 -0
- package/src/native-firebase-config.ts +202 -21
- package/src/native-firebase-metadata.ts +114 -0
- package/src/swift-host.ts +30 -15
- package/src/vite-plugin.ts +46 -2
- package/kotlin/AppSources/androidx/activity/Cancellable.kt +0 -5
package/cli/commands/control.ts
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
} from '../ws-bridge'
|
|
93
93
|
import { inspectDescribe, inspectReadySnapshot, waitReadyReason } from './inspect/core'
|
|
94
94
|
import { isAgentEnv } from './inspect/env'
|
|
95
|
+
import { hasInstalledPeachSkills } from './skills'
|
|
95
96
|
import type { DriverId, DriverLaunchResult } from '../drivers/types'
|
|
96
97
|
|
|
97
98
|
interface ControlCommandOptions {
|
|
@@ -1329,9 +1330,11 @@ export function printOpenedSim(
|
|
|
1329
1330
|
console.log(JSON.stringify({ simId: sim.id, url: printableSimUrl }, null, 2))
|
|
1330
1331
|
}
|
|
1331
1332
|
|
|
1332
|
-
function
|
|
1333
|
-
if (
|
|
1334
|
-
|
|
1333
|
+
function printAgentHints(quiet: boolean): void {
|
|
1334
|
+
if (quiet) return
|
|
1335
|
+
console.log(' inspect messages: run `peach agent listen` when you finish your turn')
|
|
1336
|
+
if (!hasInstalledPeachSkills()) {
|
|
1337
|
+
console.log(' driving peach from an agent? `peach skill install` adds its workflows')
|
|
1335
1338
|
}
|
|
1336
1339
|
}
|
|
1337
1340
|
|
|
@@ -1968,7 +1971,7 @@ export async function runOpenCommand(
|
|
|
1968
1971
|
quiet,
|
|
1969
1972
|
)
|
|
1970
1973
|
await maybeDescribeAfterOpen(parsed.wsPort, targetSim.id, args)
|
|
1971
|
-
|
|
1974
|
+
printAgentHints(quiet)
|
|
1972
1975
|
return { simId: targetSim.id, launched: false }
|
|
1973
1976
|
}
|
|
1974
1977
|
} finally {
|
|
@@ -2055,7 +2058,7 @@ export async function runOpenCommand(
|
|
|
2055
2058
|
signalDriverLaunchConnected(result)
|
|
2056
2059
|
printOpenedSim(openUrl, match, `${driver.name} driver`, quiet)
|
|
2057
2060
|
await maybeDescribeAfterOpen(parsed.wsPort, match.id, args)
|
|
2058
|
-
|
|
2061
|
+
printAgentHints(quiet)
|
|
2059
2062
|
return { simId: match.id, launched: true }
|
|
2060
2063
|
}
|
|
2061
2064
|
|
|
@@ -40,12 +40,14 @@ import type {
|
|
|
40
40
|
} from '@peach/globals'
|
|
41
41
|
import type { KeyboardLayoutSnapshot } from 'peach-engine/render-worker/native-ui-protocol'
|
|
42
42
|
|
|
43
|
-
// resolve a `--max-ms <n>`
|
|
44
|
-
// actually type (`--maxMs`, `--maxms`, `--max_ms`).
|
|
45
|
-
//
|
|
46
|
-
//
|
|
43
|
+
// resolve a wait's `--max-ms <n>` budget, tolerating the spellings agents and
|
|
44
|
+
// humans actually type (`--maxMs`, `--maxms`, `--max_ms`). `--timeout`, the
|
|
45
|
+
// global per-command budget, means the same thing for a command whose whole
|
|
46
|
+
// job is to wait. a silently-ignored timeout flag is a sharp edge: the call
|
|
47
|
+
// doesn't error, it just uses the default, so the operator thinks their bound
|
|
48
|
+
// applied when it didn't.
|
|
47
49
|
export function resolveMaxMsFlag(args: string[], fallbackMs: number): number {
|
|
48
|
-
const aliases = ['--max-ms', '--maxMs', '--maxms', '--max_ms']
|
|
50
|
+
const aliases = ['--max-ms', '--maxMs', '--maxms', '--max_ms', '--timeout']
|
|
49
51
|
for (const flag of aliases) {
|
|
50
52
|
const i = args.indexOf(flag)
|
|
51
53
|
if (i >= 0 && args[i + 1]) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rnxExit } from '../../run-rnx'
|
|
2
|
-
import { inspectWaitSelector } from './core'
|
|
2
|
+
import { inspectWaitSelector, resolveMaxMsFlag } from './core'
|
|
3
3
|
import type { WsBridge } from '../../ws-bridge'
|
|
4
4
|
|
|
5
5
|
// blocks until a node with the given testID is visible and laid out, or — with
|
|
@@ -27,16 +27,7 @@ export async function runWaitSelectorSubcommand(opts: {
|
|
|
27
27
|
rnxExit(1)
|
|
28
28
|
}
|
|
29
29
|
const testId = rawTestId.replace(/^#/, '')
|
|
30
|
-
|
|
31
|
-
// per-command budget) means the same thing for a command whose whole job is
|
|
32
|
-
// to wait — without this it parsed away and the wait silently stayed at 5s,
|
|
33
|
-
// reporting a false timeout.
|
|
34
|
-
const maxMsIdx = args.indexOf('--max-ms')
|
|
35
|
-
const budgetIdx = maxMsIdx >= 0 ? maxMsIdx : args.indexOf('--timeout')
|
|
36
|
-
const timeoutMs =
|
|
37
|
-
budgetIdx >= 0 && args[budgetIdx + 1]
|
|
38
|
-
? Math.max(100, Number(args[budgetIdx + 1]))
|
|
39
|
-
: 5000
|
|
30
|
+
const timeoutMs = resolveMaxMsFlag(args, 5000)
|
|
40
31
|
const gone = args.includes('--gone')
|
|
41
32
|
const structural = args.includes('--structural')
|
|
42
33
|
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -2003,10 +2003,15 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2003
2003
|
break
|
|
2004
2004
|
}
|
|
2005
2005
|
|
|
2006
|
+
// `tap-id` is `tap` with an exact testID, so a numeric id stays an id.
|
|
2007
|
+
case 'tap-id':
|
|
2006
2008
|
case 'tap': {
|
|
2007
2009
|
let x = Number(positional[1])
|
|
2008
2010
|
let y = Number(positional[2])
|
|
2009
|
-
const target =
|
|
2011
|
+
const target =
|
|
2012
|
+
subcommand === 'tap-id'
|
|
2013
|
+
? readTargetFlag(['--testid', positional[1] ?? ''])
|
|
2014
|
+
: readTargetFlag(args, positional)
|
|
2010
2015
|
if (target) {
|
|
2011
2016
|
const outcome = await tapResolvedTarget(bridge, {
|
|
2012
2017
|
agent: isAgentEnv(),
|
|
@@ -2460,64 +2465,6 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2460
2465
|
break
|
|
2461
2466
|
}
|
|
2462
2467
|
|
|
2463
|
-
case 'tap-id': {
|
|
2464
|
-
const query = positional[1]
|
|
2465
|
-
if (!query) {
|
|
2466
|
-
console.error(inspectUsage('tap-id', '<id>'))
|
|
2467
|
-
rnxExit(1)
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
const outcome = await tapById(bridge, query, { agent: isAgentEnv() })
|
|
2471
|
-
const payload = outcome.payload
|
|
2472
|
-
if (!payload || typeof payload.cx !== 'number') {
|
|
2473
|
-
console.error(` not found: ${query}`)
|
|
2474
|
-
// when the testID didn't match, fall back to a fuzzy list of
|
|
2475
|
-
// nearby testIDs in the live tree so the agent can pick the
|
|
2476
|
-
// intended one without dumping the whole tree first.
|
|
2477
|
-
await printSimilarTestIds(bridge, query)
|
|
2478
|
-
rnxExit(1)
|
|
2479
|
-
}
|
|
2480
|
-
if (!isTapSuccess(outcome.result)) {
|
|
2481
|
-
printTapFailure(`id "${query}"`, outcome)
|
|
2482
|
-
rnxExit(1)
|
|
2483
|
-
}
|
|
2484
|
-
const candidate = buildTapCandidateFromNode(
|
|
2485
|
-
query,
|
|
2486
|
-
{
|
|
2487
|
-
id: payload.target?.id ?? null,
|
|
2488
|
-
testID: payload.target?.testID ?? null,
|
|
2489
|
-
type: payload.target?.type ?? null,
|
|
2490
|
-
cx: payload.cx,
|
|
2491
|
-
cy: payload.cy,
|
|
2492
|
-
},
|
|
2493
|
-
'id',
|
|
2494
|
-
)
|
|
2495
|
-
await recordInspectAction('inspect tap-id', candidate.step, candidate.summary)
|
|
2496
|
-
console.log(
|
|
2497
|
-
JSON.stringify(
|
|
2498
|
-
{
|
|
2499
|
-
matched: payload.match,
|
|
2500
|
-
tapped: {
|
|
2501
|
-
nodeId: payload.target?.nodeId ?? null,
|
|
2502
|
-
id: payload.target?.id ?? null,
|
|
2503
|
-
testID: payload.target?.testID ?? null,
|
|
2504
|
-
type: payload.target?.type ?? null,
|
|
2505
|
-
cx: payload.cx,
|
|
2506
|
-
cy: payload.cy,
|
|
2507
|
-
},
|
|
2508
|
-
...(payload.strategy && payload.strategy !== 'matched-node'
|
|
2509
|
-
? { strategy: payload.strategy }
|
|
2510
|
-
: {}),
|
|
2511
|
-
...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
|
|
2512
|
-
result: outcome.result,
|
|
2513
|
-
},
|
|
2514
|
-
null,
|
|
2515
|
-
2,
|
|
2516
|
-
),
|
|
2517
|
-
)
|
|
2518
|
-
break
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
2468
|
case 'type-into': {
|
|
2522
2469
|
const targetId = positional[1]
|
|
2523
2470
|
const text = positional.slice(2).join(' ')
|
package/cli/commands/kotlin.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { WebSocket, WebSocketServer } from 'ws'
|
|
|
6
6
|
import { kotlinGuestBundle } from '../../src/guest-bundle'
|
|
7
7
|
import {
|
|
8
8
|
readKotlinApp,
|
|
9
|
-
type
|
|
9
|
+
type AndroidPins,
|
|
10
10
|
type KotlinAppProject,
|
|
11
11
|
} from '../../src/kotlin-app'
|
|
12
12
|
import { PeachExit } from '../run-rnx'
|
|
@@ -19,7 +19,7 @@ const HMR_PATH = '/__peach/kotlin-hmr'
|
|
|
19
19
|
// and in the built CLI (dist-cli/chunks)
|
|
20
20
|
const MODULE_ROOT = path.resolve(import.meta.dirname, '../../kotlin')
|
|
21
21
|
|
|
22
|
-
function
|
|
22
|
+
function androidPins(): AndroidPins {
|
|
23
23
|
const toolchain: unknown = JSON.parse(
|
|
24
24
|
fs.readFileSync(path.join(MODULE_ROOT, 'toolchain.json'), 'utf8'),
|
|
25
25
|
)
|
|
@@ -30,7 +30,7 @@ function composePins(): ComposeAndroidPins {
|
|
|
30
30
|
const pin = (name: string) => {
|
|
31
31
|
const value: unknown = android ? Reflect.get(android, name) : undefined
|
|
32
32
|
if (typeof value !== 'string')
|
|
33
|
-
throw new Error(`Kotlin toolchain has no
|
|
33
|
+
throw new Error(`Kotlin toolchain has no Android ${name} pin`)
|
|
34
34
|
return value
|
|
35
35
|
}
|
|
36
36
|
return {
|
|
@@ -38,6 +38,8 @@ function composePins(): ComposeAndroidPins {
|
|
|
38
38
|
foundation: pin('foundation'),
|
|
39
39
|
runtime: pin('runtime'),
|
|
40
40
|
material3: pin('material3'),
|
|
41
|
+
navigation: pin('navigation'),
|
|
42
|
+
activity: pin('activity'),
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -91,7 +93,7 @@ export async function runKotlin(
|
|
|
91
93
|
throw new Error(
|
|
92
94
|
`peach kotlin runs an Android app; ${device} is not an Android device`,
|
|
93
95
|
)
|
|
94
|
-
const pins =
|
|
96
|
+
const pins = androidPins()
|
|
95
97
|
let project: KotlinAppProject = readKotlinApp(modulePath, { activity, pins })
|
|
96
98
|
const host = await import('../../src/kotlin-host')
|
|
97
99
|
const controller = new AbortController()
|
package/cli/commands/skills.ts
CHANGED
|
@@ -40,7 +40,13 @@ function getSkillsDir(): string {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// resolved on first use: the standalone binary writes its skills when the
|
|
44
|
+
// directory is resolved, and importing this module must never touch disk.
|
|
45
|
+
let skillsDirPath: string | undefined
|
|
46
|
+
function skillsDir(): string {
|
|
47
|
+
skillsDirPath ??= getSkillsDir()
|
|
48
|
+
return skillsDirPath
|
|
49
|
+
}
|
|
44
50
|
const AGENT_TARGET_IDS = new Set(['all', 'codex', 'claude'])
|
|
45
51
|
|
|
46
52
|
interface SkillInfo {
|
|
@@ -109,6 +115,21 @@ function defaultAgentTargets(): InstallTarget[] {
|
|
|
109
115
|
]
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
// whether any bundled skill is installed in a default agent skills directory.
|
|
119
|
+
// reads the names without materializing the binary's skills, so a command
|
|
120
|
+
// that only prints a hint writes nothing.
|
|
121
|
+
export function hasInstalledPeachSkills(): boolean {
|
|
122
|
+
const names =
|
|
123
|
+
typeof __PEACH_BUNDLED_SKILLS__ !== 'undefined'
|
|
124
|
+
? Object.keys(__PEACH_BUNDLED_SKILLS__).flatMap(
|
|
125
|
+
(file) => /^([^/]+)\/SKILL\.md$/.exec(file)?.[1] ?? [],
|
|
126
|
+
)
|
|
127
|
+
: listSkills().map((skill) => skill.name)
|
|
128
|
+
return defaultAgentTargets().some(({ dir }) =>
|
|
129
|
+
names.some((name) => fs.existsSync(path.join(dir, name, 'SKILL.md'))),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
function agentTarget(target: string): InstallTarget[] {
|
|
113
134
|
if (target === 'all') return defaultAgentTargets()
|
|
114
135
|
if (target === 'codex') {
|
|
@@ -173,10 +194,10 @@ function readFrontmatterField(frontmatter: string, field: string): string | null
|
|
|
173
194
|
// bundled skills live in the standard agent layout: skills/<name>/SKILL.md.
|
|
174
195
|
// the directory name is the skill name; frontmatter is the display metadata.
|
|
175
196
|
function listSkills(): SkillInfo[] {
|
|
176
|
-
if (!fs.existsSync(
|
|
197
|
+
if (!fs.existsSync(skillsDir())) return []
|
|
177
198
|
const skills: SkillInfo[] = []
|
|
178
|
-
for (const entry of fs.readdirSync(
|
|
179
|
-
const skillMd = path.join(
|
|
199
|
+
for (const entry of fs.readdirSync(skillsDir())) {
|
|
200
|
+
const skillMd = path.join(skillsDir(), entry, 'SKILL.md')
|
|
180
201
|
if (!fs.existsSync(skillMd)) continue
|
|
181
202
|
const meta = parseSkillFrontmatter(fs.readFileSync(skillMd, 'utf8'))
|
|
182
203
|
if (meta) {
|
|
@@ -266,7 +287,7 @@ function isOwnLegacyInstall(stat: fs.Stats, dest: string, srcDir: string): boole
|
|
|
266
287
|
}
|
|
267
288
|
|
|
268
289
|
function linkSkill(skill: SkillInfo, targetDir: string, force: boolean): LinkResult {
|
|
269
|
-
const src = path.join(
|
|
290
|
+
const src = path.join(skillsDir(), skill.name)
|
|
270
291
|
const dest = path.join(targetDir, skill.name)
|
|
271
292
|
fs.mkdirSync(targetDir, { recursive: true })
|
|
272
293
|
|
|
@@ -306,7 +327,7 @@ function removeLegacyBundledSkills(targetDir: string) {
|
|
|
306
327
|
if (!stat) continue
|
|
307
328
|
if (
|
|
308
329
|
stat.isSymbolicLink() ||
|
|
309
|
-
isOwnLegacyInstall(stat, dest, path.join(
|
|
330
|
+
isOwnLegacyInstall(stat, dest, path.join(skillsDir(), name))
|
|
310
331
|
) {
|
|
311
332
|
fs.rmSync(dest, { recursive: true, force: true })
|
|
312
333
|
}
|
|
@@ -435,7 +456,7 @@ function runShow(args: string[]) {
|
|
|
435
456
|
return
|
|
436
457
|
}
|
|
437
458
|
const [skill] = matchSkills(skills, [name])
|
|
438
|
-
const content = fs.readFileSync(path.join(
|
|
459
|
+
const content = fs.readFileSync(path.join(skillsDir(), skill.name, 'SKILL.md'), 'utf8')
|
|
439
460
|
console.log(content)
|
|
440
461
|
}
|
|
441
462
|
|
package/cli/commands/upgrade.ts
CHANGED
|
@@ -15,17 +15,21 @@ import {
|
|
|
15
15
|
fetchCliReleaseFile,
|
|
16
16
|
refreshCliUpdateCache,
|
|
17
17
|
} from '../cli-update'
|
|
18
|
+
import { rnxExit } from '../run-rnx'
|
|
18
19
|
import { fetchRuntimeReleaseNotes, formatRuntimeReleaseNotes } from '../runtime-notes'
|
|
19
20
|
import { IS_STANDALONE } from '../standalone'
|
|
20
21
|
import { runRuntime } from './runtime'
|
|
21
22
|
|
|
22
23
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
|
24
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/
|
|
23
25
|
|
|
24
26
|
export interface UpgradeOptions {
|
|
25
27
|
cliExecutablePath?: string
|
|
26
28
|
cliReleaseOrigin?: string
|
|
27
29
|
standalone?: boolean
|
|
28
30
|
includeReleaseNotes?: boolean
|
|
31
|
+
// install exactly this cli release, newer or older, instead of the latest.
|
|
32
|
+
cliVersion?: string
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export async function runUpgrade(
|
|
@@ -35,6 +39,12 @@ export async function runUpgrade(
|
|
|
35
39
|
const channelIdx = args.indexOf('--channel')
|
|
36
40
|
const channel =
|
|
37
41
|
channelIdx >= 0 && args[channelIdx + 1] ? args[channelIdx + 1] : undefined
|
|
42
|
+
const toIdx = args.indexOf('--to')
|
|
43
|
+
const cliVersion = toIdx >= 0 ? args[toIdx + 1]?.replace(/^v/, '') : undefined
|
|
44
|
+
if (toIdx >= 0 && !(cliVersion && VERSION_PATTERN.test(cliVersion))) {
|
|
45
|
+
console.error(' peach upgrade: --to takes a CLI version such as 0.1.669')
|
|
46
|
+
rnxExit(1)
|
|
47
|
+
}
|
|
38
48
|
|
|
39
49
|
console.log('peach upgrade\n')
|
|
40
50
|
|
|
@@ -79,7 +89,7 @@ export async function runUpgrade(
|
|
|
79
89
|
: null
|
|
80
90
|
|
|
81
91
|
console.log('\ncli:')
|
|
82
|
-
await upgradeCli(options)
|
|
92
|
+
await upgradeCli({ ...options, cliVersion })
|
|
83
93
|
|
|
84
94
|
if (installedRuntime && options.includeReleaseNotes !== false) {
|
|
85
95
|
console.log(`\n${formatRuntimeReleaseNotes(installedRuntime, releaseNotes)}`)
|
|
@@ -103,8 +113,13 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
|
|
|
103
113
|
'',
|
|
104
114
|
)
|
|
105
115
|
const current = getCliVersion()
|
|
106
|
-
const
|
|
107
|
-
|
|
116
|
+
const latest =
|
|
117
|
+
options.cliVersion ?? (await refreshCliUpdateCache({ releaseOrigin })).latestVersion
|
|
118
|
+
if (options.cliVersion === current) {
|
|
119
|
+
console.log(` v${current} (requested)`)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
if (!options.cliVersion && compareSemver(latest, current) <= 0) {
|
|
108
123
|
console.log(` v${current} (latest)`)
|
|
109
124
|
return
|
|
110
125
|
}
|
package/cli/main.ts
CHANGED
|
@@ -74,6 +74,18 @@ process.on('unhandledRejection', handleTopLevelError)
|
|
|
74
74
|
process.on('uncaughtException', handleTopLevelError)
|
|
75
75
|
|
|
76
76
|
let parsed = parseArgs(process.argv)
|
|
77
|
+
// `--redact` reads the same before or after the command name, and masks from
|
|
78
|
+
// the first line printed, parser warnings included.
|
|
79
|
+
const redact =
|
|
80
|
+
parsed.globalFlags['redact'] === true || parsed.commandArgs.includes('--redact')
|
|
81
|
+
if (redact) {
|
|
82
|
+
parsed = {
|
|
83
|
+
...parsed,
|
|
84
|
+
commandArgs: parsed.commandArgs.filter((arg) => arg !== '--redact'),
|
|
85
|
+
}
|
|
86
|
+
const { redactProcessOutput } = await import('./output-redaction')
|
|
87
|
+
redactProcessOutput()
|
|
88
|
+
}
|
|
77
89
|
for (const warning of parsed.warnings) process.stderr.write(`${warning}\n`)
|
|
78
90
|
|
|
79
91
|
// runtime actions and reads also work as hidden root aliases. resolve only
|
|
@@ -83,6 +95,15 @@ if (!parsed.command && parsed.commandArgs.length > 0) {
|
|
|
83
95
|
const { resolveHiddenRuntimeAlias } = await import('./hidden-runtime-alias')
|
|
84
96
|
parsed = resolveHiddenRuntimeAlias(parsed)
|
|
85
97
|
}
|
|
98
|
+
if (redact) {
|
|
99
|
+
const { REDACTABLE_COMMANDS } = await import('./output-redaction')
|
|
100
|
+
if (!REDACTABLE_COMMANDS.has(parsed.command ?? '')) {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
` --redact applies to ${[...REDACTABLE_COMMANDS].join(', ')}; this command can print output it cannot mask\n`,
|
|
103
|
+
)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
86
107
|
|
|
87
108
|
const isAutomaticCleanupWorker =
|
|
88
109
|
parsed.command === 'cleanup' &&
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// `--redact` masks secrets and account identifiers in what a command prints,
|
|
2
|
+
// so logs, network dumps and timelines can be pasted where others read them.
|
|
3
|
+
// each value becomes `<redacted:xxxx>`, a short digest, so one value still
|
|
4
|
+
// matches itself across lines without revealing it.
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { format, inspect } from 'node:util'
|
|
8
|
+
|
|
9
|
+
// the commands that print only through this process's console and streams.
|
|
10
|
+
// anything else may hand its output to a child process, which no wrapper here
|
|
11
|
+
// can see, so it refuses the flag instead of printing secrets it claims to hide.
|
|
12
|
+
export const REDACTABLE_COMMANDS = new Set([
|
|
13
|
+
'logs',
|
|
14
|
+
'network',
|
|
15
|
+
'what-happened',
|
|
16
|
+
'history',
|
|
17
|
+
'get',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
// names whose values grant access or identify an account, in json, js object
|
|
21
|
+
// output, query strings and headers. operational ids stay readable: a bare
|
|
22
|
+
// `id` addresses `network get <id>`, a sim's `sessionId` names the sim, and a
|
|
23
|
+
// keyboard `key` is the key pressed.
|
|
24
|
+
const SECRET_NAME =
|
|
25
|
+
/^(?:[\w-]*(?:api[-_]?key|token|secret|password|passwd|authorization|cookie|signature|credential)s?|auth|sig|email|uid|[\w-]*(?:user|account|local|device|installation|client|app|project|sender|measurement|tenant)[-_]?id)$/i
|
|
26
|
+
|
|
27
|
+
const JWT = /\beyJ[\w-]{6,}\.[\w-]{6,}\.[\w-]{6,}/g
|
|
28
|
+
const GOOGLE_API_KEY = /\bAIza[\w-]{30,}/g
|
|
29
|
+
const BEARER = /\b(Bearer|Basic)\s+[\w.~+/=-]{8,}/gi
|
|
30
|
+
const EMAIL = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/g
|
|
31
|
+
// a name and its value: a quoted json name or a bare js object key, then a
|
|
32
|
+
// json or js string, a whole number token, or an array of strings. a string is
|
|
33
|
+
// matched at its own escaping level only; a json document escaped inside it is
|
|
34
|
+
// reached by decoding the string, since escaped quotes cannot show where an
|
|
35
|
+
// escaped value ends.
|
|
36
|
+
const STRING = String.raw`(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')`
|
|
37
|
+
const NUMBER = String.raw`-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?`
|
|
38
|
+
const PAIR = new RegExp(
|
|
39
|
+
String.raw`((?:"([\w-]+)"|\b([\w-]+))\s*:\s*)(${STRING}|${NUMBER}\b|\[\s*${STRING}(?:\s*,\s*${STRING})*\s*\])`,
|
|
40
|
+
'g',
|
|
41
|
+
)
|
|
42
|
+
const STRING_TOKEN = new RegExp(STRING, 'g')
|
|
43
|
+
const QUERY_PAIR = /([?&;]([\w.%-]+)=)([^&#\s"']+)/g
|
|
44
|
+
const HEADER_LINE = /^(\s*([\w-]+)\s*:\s+)(\S.*)$/gm
|
|
45
|
+
const MASKED = /^<redacted:[0-9a-f]{4}>$/
|
|
46
|
+
|
|
47
|
+
// a value an earlier rule already masked keeps its digest.
|
|
48
|
+
function mask(value: string): string {
|
|
49
|
+
if (MASKED.test(value)) return value
|
|
50
|
+
return `<redacted:${createHash('sha256').update(value).digest('hex').slice(0, 4)}>`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// masks a quoted value inside its own quotes, so json stays json.
|
|
54
|
+
function maskQuoted(quoted: string): string {
|
|
55
|
+
return `${quoted[0]}${mask(quoted.slice(1, -1))}${quoted[0]}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// a string can carry a document of its own: json escaped inside a json string,
|
|
59
|
+
// or json inside a js string. its decoded contents are redacted and encoded
|
|
60
|
+
// back the same way; each level is shorter, so this ends.
|
|
61
|
+
function redactString(quoted: string): string {
|
|
62
|
+
if (quoted.startsWith("'")) {
|
|
63
|
+
const inner = quoted.slice(1, -1)
|
|
64
|
+
const redacted = redactForSharing(inner)
|
|
65
|
+
return redacted === inner ? quoted : `'${redacted}'`
|
|
66
|
+
}
|
|
67
|
+
let decoded: string
|
|
68
|
+
try {
|
|
69
|
+
decoded = JSON.parse(quoted)
|
|
70
|
+
} catch {
|
|
71
|
+
decoded = quoted.slice(1, -1).replace(/\\(["\\/])/g, '$1')
|
|
72
|
+
}
|
|
73
|
+
const redacted = redactForSharing(decoded)
|
|
74
|
+
return redacted === decoded ? quoted : JSON.stringify(redacted)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decodeOrSelf(value: string): string {
|
|
78
|
+
try {
|
|
79
|
+
return decodeURIComponent(value)
|
|
80
|
+
} catch {
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// a non-secret string, or each string of a non-secret array, is searched
|
|
86
|
+
// for pairs of its own.
|
|
87
|
+
function redactPair(
|
|
88
|
+
pair: string,
|
|
89
|
+
prefix: string,
|
|
90
|
+
quotedName: string | undefined,
|
|
91
|
+
bareName: string,
|
|
92
|
+
value: string,
|
|
93
|
+
): string {
|
|
94
|
+
if (!SECRET_NAME.test(quotedName ?? bareName)) {
|
|
95
|
+
if (!/^["'[]/.test(value)) return pair
|
|
96
|
+
const inner = value.startsWith('[')
|
|
97
|
+
? value.replace(STRING_TOKEN, redactString)
|
|
98
|
+
: redactString(value)
|
|
99
|
+
return inner === value ? pair : `${prefix}${inner}`
|
|
100
|
+
}
|
|
101
|
+
if (value.startsWith('[')) return `${prefix}${value.replace(STRING_TOKEN, maskQuoted)}`
|
|
102
|
+
return `${prefix}${/^["']/.test(value) ? maskQuoted(value) : `"${mask(value)}"`}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function redactForSharing(text: string): string {
|
|
106
|
+
return text
|
|
107
|
+
.replace(JWT, mask)
|
|
108
|
+
.replace(GOOGLE_API_KEY, mask)
|
|
109
|
+
.replace(BEARER, (match, scheme: string) => `${scheme} ${mask(match)}`)
|
|
110
|
+
.replace(PAIR, redactPair)
|
|
111
|
+
.replace(QUERY_PAIR, (pair, prefix: string, name: string, value: string) => {
|
|
112
|
+
if (SECRET_NAME.test(decodeOrSelf(name))) return `${prefix}${mask(value)}`
|
|
113
|
+
// a launch url can carry a whole config object, or another url with its
|
|
114
|
+
// own query, as one encoded value; each decode is shorter, so this ends.
|
|
115
|
+
if (!/%[0-9a-f]{2}/i.test(value)) return pair
|
|
116
|
+
const decoded = decodeOrSelf(value)
|
|
117
|
+
const redacted = redactForSharing(decoded)
|
|
118
|
+
return redacted === decoded ? pair : `${prefix}${encodeURIComponent(redacted)}`
|
|
119
|
+
})
|
|
120
|
+
.replace(HEADER_LINE, (line, prefix: string, name: string, value: string) =>
|
|
121
|
+
SECRET_NAME.test(name) ? `${prefix}${mask(value)}` : line,
|
|
122
|
+
)
|
|
123
|
+
.replace(EMAIL, mask)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true })
|
|
127
|
+
|
|
128
|
+
// applies to both streams and the console for the rest of the process; bun's
|
|
129
|
+
// console writes without passing through the streams. text is masked a whole
|
|
130
|
+
// line at a time, so a value split across writes is still seen whole; binary
|
|
131
|
+
// output passes through untouched.
|
|
132
|
+
export function redactProcessOutput(): void {
|
|
133
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
134
|
+
const write = stream.write.bind(stream)
|
|
135
|
+
let pending = ''
|
|
136
|
+
stream.write = (
|
|
137
|
+
chunk: string | Uint8Array,
|
|
138
|
+
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
|
|
139
|
+
callback?: (error?: Error | null) => void,
|
|
140
|
+
): boolean => {
|
|
141
|
+
const done =
|
|
142
|
+
typeof encodingOrCallback === 'function' ? encodingOrCallback : callback
|
|
143
|
+
let text: string
|
|
144
|
+
if (typeof chunk === 'string') text = chunk
|
|
145
|
+
else {
|
|
146
|
+
try {
|
|
147
|
+
text = utf8.decode(chunk)
|
|
148
|
+
} catch {
|
|
149
|
+
return write(chunk, done)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const buffered = pending + text
|
|
153
|
+
const end = buffered.lastIndexOf('\n') + 1
|
|
154
|
+
pending = buffered.slice(end)
|
|
155
|
+
if (end === 0) {
|
|
156
|
+
done?.()
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
return write(redactForSharing(buffered.slice(0, end)), done)
|
|
160
|
+
}
|
|
161
|
+
process.on('exit', () => {
|
|
162
|
+
if (pending) write(redactForSharing(pending))
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
const methods: ('log' | 'info' | 'debug' | 'warn' | 'error' | 'trace')[] = [
|
|
166
|
+
'log',
|
|
167
|
+
'info',
|
|
168
|
+
'debug',
|
|
169
|
+
'warn',
|
|
170
|
+
'error',
|
|
171
|
+
'trace',
|
|
172
|
+
]
|
|
173
|
+
for (const method of methods) {
|
|
174
|
+
const print = console[method].bind(console)
|
|
175
|
+
console[method] = (...args: unknown[]) => print(redactForSharing(format(...args)))
|
|
176
|
+
}
|
|
177
|
+
console.dir = (value: unknown, options?: object) =>
|
|
178
|
+
console.log(redactForSharing(inspect(value, options)))
|
|
179
|
+
console.assert = (condition?: boolean, ...args: unknown[]) => {
|
|
180
|
+
if (!condition) console.error('Assertion failed:', ...args)
|
|
181
|
+
}
|
|
182
|
+
}
|
package/cli/parse-args.ts
CHANGED
|
@@ -29,6 +29,7 @@ const GLOBAL_FLAGS: Record<
|
|
|
29
29
|
// `--session <id>`). normalized to the sim target in bin.ts.
|
|
30
30
|
'--session': { type: 'string' },
|
|
31
31
|
'--tab': { type: 'string' },
|
|
32
|
+
'--redact': { type: 'boolean' },
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// every spelling parseRnxArgs consumes before the command name, mapped to
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.670 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|