rnxsim 0.1.380 → 0.1.382
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 +43 -26
- package/cli/app-fonts.ts +1 -1
- package/cli/bin.ts +118 -26
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +9 -9
- package/cli/browser-evals.ts +3 -3
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +407 -0
- package/cli/cloud-dispatch.ts +165 -0
- package/cli/cloud-session.ts +156 -0
- package/cli/command-contract.ts +2 -2
- package/cli/command-registry.ts +9 -0
- package/cli/commands/agent-wrapper.ts +4 -4
- package/cli/commands/agent.ts +3 -3
- package/cli/commands/box/checkout-plane.ts +16 -8
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +123 -70
- package/cli/commands/config.ts +3 -2
- package/cli/commands/control.ts +6 -6
- package/cli/commands/cpu-profile.ts +2 -2
- package/cli/commands/daemon.ts +7 -7
- package/cli/commands/debug.ts +2 -2
- package/cli/commands/detox.ts +7 -7
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/electron.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +4 -4
- package/cli/commands/inspect/core.ts +58 -7
- package/cli/commands/inspect/env.ts +5 -3
- package/cli/commands/inspect/shared.ts +2 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-ready.ts +15 -5
- package/cli/commands/inspect.ts +31 -9
- package/cli/commands/install-desktop.ts +2 -2
- package/cli/commands/maestro.ts +3 -3
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +91 -0
- package/cli/commands/record.ts +3 -3
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/runtime.ts +1 -1
- package/cli/commands/screenshot-mode.ts +1 -1
- package/cli/commands/screenshot.ts +25 -0
- package/cli/commands/serve.ts +1 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/timeline.ts +1 -1
- package/cli/commands/upgrade.ts +8 -34
- package/cli/commands/upload.ts +3 -3
- package/cli/commands/version.ts +14 -3
- package/cli/commands/what-happened.ts +2 -2
- package/cli/current-sim.ts +43 -30
- package/cli/desktop-companion.ts +1 -1
- package/cli/drivers/electron.ts +1 -1
- package/cli/drivers/playwright.ts +3 -3
- package/cli/drivers/types.ts +4 -4
- package/cli/flow-file.ts +3 -3
- package/cli/help-core.ts +18 -11
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/hints.ts +2 -2
- package/cli/maestro-js.ts +2 -2
- package/cli/outbound-endpoints.ts +40 -6
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +2 -2
- package/cli/secure-file.ts +31 -0
- package/cli/shell-init.ts +98 -0
- package/cli/ws-bridge.ts +18 -8
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +3 -3
- package/detox/index.ts +3 -3
- 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 +15 -8
- package/dist-lib/bridge-contract-input.mjs +15 -8
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +107 -0
- package/dist-lib/capture-contract.mjs +80 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +32 -0
- package/dist-lib/cloud-contract.mjs +7 -0
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +4 -4
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +9 -1
- package/dist-lib/host/bridge-host.cjs +15 -10
- 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 +248 -73
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.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/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 +44 -6
- package/dist-lib/sdk.mjs +44 -6
- package/dist-lib/skills.cjs +435 -155
- package/dist-lib/vite.cjs +1 -1
- package/package.json +21 -5
- package/scripts/dev-server-scanner.ts +3 -3
- package/skills/contrast/SKILL.md +1 -1
- package/skills/rnx-perf/SKILL.md +68 -52
- package/src/app-url.ts +2 -3
- package/src/bridge-contract-input.ts +17 -6
- package/src/bridge-contract.ts +25 -4
- package/src/capture-contract.ts +423 -0
- package/src/cloud-contract.ts +15 -0
- package/src/home-paths.ts +9 -3
- package/src/host/agent-host.ts +1 -1
- package/src/host/bridge-host.ts +13 -11
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +21 -0
- package/src/native-dev-bundle-url.ts +2 -2
- package/src/screenshots/orchestrate.ts +1 -1
- package/src/screenshots/org-deck.ts +1 -1
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +4 -0
- package/src/vite-plugin-one.ts +2 -2
- package/src/vite-plugin.ts +1 -1
- package/cli/commands/box/cloud-box.ts +0 -294
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
interface CloudBoundaryInvocation {
|
|
2
|
+
command: string
|
|
3
|
+
args: string[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type CloudBoundaryResult = { handled: false } | { handled: true; code: number }
|
|
7
|
+
|
|
8
|
+
const CLOUD_TARGET_COMMANDS = new Set([
|
|
9
|
+
'assert',
|
|
10
|
+
'camera',
|
|
11
|
+
'claim',
|
|
12
|
+
'close',
|
|
13
|
+
'debug',
|
|
14
|
+
'describe',
|
|
15
|
+
'detox',
|
|
16
|
+
'do',
|
|
17
|
+
'film',
|
|
18
|
+
'find',
|
|
19
|
+
'get',
|
|
20
|
+
'logs',
|
|
21
|
+
'maestro',
|
|
22
|
+
'mode',
|
|
23
|
+
'network',
|
|
24
|
+
'perf',
|
|
25
|
+
'permissions',
|
|
26
|
+
'record',
|
|
27
|
+
'reset',
|
|
28
|
+
'screenshot',
|
|
29
|
+
'shell',
|
|
30
|
+
'state',
|
|
31
|
+
'storage',
|
|
32
|
+
'timeline',
|
|
33
|
+
'wait',
|
|
34
|
+
'what-happened',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
function firstVerb(positional: string[]): string | null {
|
|
38
|
+
return positional.find((value) => !value.startsWith('-')) ?? null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function supportsCloudRead(
|
|
42
|
+
command: string,
|
|
43
|
+
args: string[],
|
|
44
|
+
positional: string[],
|
|
45
|
+
): boolean {
|
|
46
|
+
if (command === 'describe' || command === 'find') return true
|
|
47
|
+
if (command === 'state') return firstVerb(positional) === null
|
|
48
|
+
if (command === 'get') {
|
|
49
|
+
const verb = firstVerb(positional)
|
|
50
|
+
return verb === 'tree' || verb === 'node' || verb === 'count'
|
|
51
|
+
}
|
|
52
|
+
if (command === 'wait') {
|
|
53
|
+
const verb = firstVerb(positional)
|
|
54
|
+
return verb === 'ready' || verb === 'selector'
|
|
55
|
+
}
|
|
56
|
+
if (command === 'logs') {
|
|
57
|
+
const verb = firstVerb(positional)
|
|
58
|
+
return (
|
|
59
|
+
!args.includes('--tail') &&
|
|
60
|
+
!args.includes('-f') &&
|
|
61
|
+
verb !== 'tail' &&
|
|
62
|
+
verb !== 'clear' &&
|
|
63
|
+
verb !== 'purge'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
if (command === 'screenshot') {
|
|
67
|
+
return !args.some((arg) =>
|
|
68
|
+
[
|
|
69
|
+
'--area',
|
|
70
|
+
'--gallery',
|
|
71
|
+
'--id',
|
|
72
|
+
'--no-shell',
|
|
73
|
+
'--shell-only',
|
|
74
|
+
'--text',
|
|
75
|
+
'--themes',
|
|
76
|
+
'--with-frame',
|
|
77
|
+
].includes(arg),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function refuseCloudInvocation(
|
|
84
|
+
command: string,
|
|
85
|
+
positional: string[],
|
|
86
|
+
): CloudBoundaryResult {
|
|
87
|
+
const verb = firstVerb(positional)
|
|
88
|
+
const name = verb ? `${command} ${verb}` : command
|
|
89
|
+
console.error(` rnx ${name} is not available for an RNX Cloud simulator`)
|
|
90
|
+
return { handled: true, code: 1 }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function dispatchCloudBoundary({
|
|
94
|
+
command,
|
|
95
|
+
args,
|
|
96
|
+
}: CloudBoundaryInvocation): Promise<CloudBoundaryResult> {
|
|
97
|
+
if (command === 'ios' && args.includes('--cloud')) return { handled: false }
|
|
98
|
+
if (command === 'android' && args.includes('--cloud')) {
|
|
99
|
+
console.error(
|
|
100
|
+
' rnx android --cloud is not available; RNX Cloud currently supports iOS',
|
|
101
|
+
)
|
|
102
|
+
return { handled: true, code: 1 }
|
|
103
|
+
}
|
|
104
|
+
if (!CLOUD_TARGET_COMMANDS.has(command)) return { handled: false }
|
|
105
|
+
|
|
106
|
+
const [{ parseBridgeCliArgs }, cloudClient, cloudSession] = await Promise.all([
|
|
107
|
+
import('./ws-bridge'),
|
|
108
|
+
import('./cloud-client'),
|
|
109
|
+
import('./cloud-session'),
|
|
110
|
+
])
|
|
111
|
+
const parsed = parseBridgeCliArgs(args)
|
|
112
|
+
const positionalTarget =
|
|
113
|
+
command === 'claim' || command === 'close' ? firstVerb(parsed.positional) : null
|
|
114
|
+
const target: Pick<typeof parsed, 'simId' | 'simIdSource'> = positionalTarget
|
|
115
|
+
? { simId: positionalTarget, simIdSource: 'flag' }
|
|
116
|
+
: parsed
|
|
117
|
+
let session
|
|
118
|
+
try {
|
|
119
|
+
session = cloudClient.resolveCloudSessionForParsed(target)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`)
|
|
122
|
+
return { handled: true, code: 1 }
|
|
123
|
+
}
|
|
124
|
+
if (!session) return { handled: false }
|
|
125
|
+
|
|
126
|
+
if (command === 'claim') {
|
|
127
|
+
try {
|
|
128
|
+
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
129
|
+
const claimed = await cloudClient.claimCloudSimulator(session, {
|
|
130
|
+
force: args.includes('--force'),
|
|
131
|
+
})
|
|
132
|
+
cloudSession.publishCloudSessionToShell(claimed.session, descriptor)
|
|
133
|
+
console.log(` claimed: ${session.simId}`)
|
|
134
|
+
console.log(` expires: ${claimed.claim.expiresAt}`)
|
|
135
|
+
return { handled: true, code: 0 }
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(
|
|
138
|
+
` claim failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
+
)
|
|
140
|
+
return { handled: true, code: 1 }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (command === 'close') {
|
|
145
|
+
if (args.includes('--all') || args.includes('--others')) {
|
|
146
|
+
return refuseCloudInvocation(command, parsed.positional)
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
150
|
+
await cloudClient.closeCloudSimulator(session)
|
|
151
|
+
cloudSession.publishCloudSessionToShell(null, descriptor)
|
|
152
|
+
console.log(` closed: ${session.simId}`)
|
|
153
|
+
return { handled: true, code: 0 }
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(
|
|
156
|
+
` close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
157
|
+
)
|
|
158
|
+
return { handled: true, code: 1 }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return supportsCloudRead(command, args, parsed.positional)
|
|
163
|
+
? { handled: false }
|
|
164
|
+
: refuseCloudInvocation(command, parsed.positional)
|
|
165
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { fstatSync, writeSync } from 'node:fs'
|
|
2
|
+
import { isLoopbackHost } from '../src/backend-origin'
|
|
3
|
+
|
|
4
|
+
const CLOUD_SESSION_VERSION = 1 as const
|
|
5
|
+
const SHELL_UPDATE_FD_ENV = 'RNX_SHELL_UPDATE_FD'
|
|
6
|
+
const MAX_ENCODED_SESSION_BYTES = 32 * 1024
|
|
7
|
+
|
|
8
|
+
export const RNX_CLOUD_SESSION_ENV = 'RNX_CLOUD_SESSION'
|
|
9
|
+
export const RNX_SHELL_UPDATE_PREFIX = '__RNX_SHELL_UPDATE_V1__'
|
|
10
|
+
export const RNX_SHELL_UPDATE_SET_PREFIX = `${RNX_SHELL_UPDATE_PREFIX}set:`
|
|
11
|
+
export const RNX_SHELL_UPDATE_UNSET = `${RNX_SHELL_UPDATE_PREFIX}unset`
|
|
12
|
+
|
|
13
|
+
export interface CloudSession {
|
|
14
|
+
version: 1
|
|
15
|
+
simId: string
|
|
16
|
+
apiOrigin: string
|
|
17
|
+
claimId: string
|
|
18
|
+
token: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeCloudOrigin(source: string): string {
|
|
26
|
+
let parsed: URL
|
|
27
|
+
try {
|
|
28
|
+
parsed = new URL(source)
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(`invalid RNX Cloud origin: ${source}`)
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
(parsed.protocol !== 'https:' && parsed.protocol !== 'http:') ||
|
|
34
|
+
parsed.username ||
|
|
35
|
+
parsed.password ||
|
|
36
|
+
parsed.pathname !== '/' ||
|
|
37
|
+
parsed.search ||
|
|
38
|
+
parsed.hash
|
|
39
|
+
) {
|
|
40
|
+
throw new Error(`RNX Cloud origin must be an HTTP(S) origin: ${source}`)
|
|
41
|
+
}
|
|
42
|
+
if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {
|
|
43
|
+
throw new Error(`RNX Cloud origin requires HTTPS outside loopback: ${source}`)
|
|
44
|
+
}
|
|
45
|
+
return parsed.origin
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createCloudSession(input: {
|
|
49
|
+
simId: string
|
|
50
|
+
apiOrigin: string
|
|
51
|
+
claimId: string
|
|
52
|
+
token: string
|
|
53
|
+
}): CloudSession {
|
|
54
|
+
const session = {
|
|
55
|
+
version: CLOUD_SESSION_VERSION,
|
|
56
|
+
simId: input.simId.trim(),
|
|
57
|
+
apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
|
|
58
|
+
claimId: input.claimId.trim(),
|
|
59
|
+
token: input.token,
|
|
60
|
+
} satisfies CloudSession
|
|
61
|
+
if (!session.simId || !session.claimId || !session.token) {
|
|
62
|
+
throw new Error('cloud session requires simulator, origin, claim, and token')
|
|
63
|
+
}
|
|
64
|
+
return session
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function serializeCloudSession(session: CloudSession): string {
|
|
68
|
+
return Buffer.from(JSON.stringify(createCloudSession(session)), 'utf8').toString(
|
|
69
|
+
'base64url',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseCloudSession(encoded: string): CloudSession {
|
|
74
|
+
const value = encoded.trim()
|
|
75
|
+
if (
|
|
76
|
+
!value ||
|
|
77
|
+
value.length > MAX_ENCODED_SESSION_BYTES ||
|
|
78
|
+
!/^[A-Za-z0-9_-]+$/.test(value)
|
|
79
|
+
) {
|
|
80
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
81
|
+
}
|
|
82
|
+
let parsed: unknown
|
|
83
|
+
try {
|
|
84
|
+
const bytes = Buffer.from(value, 'base64url')
|
|
85
|
+
if (bytes.toString('base64url') !== value) {
|
|
86
|
+
throw new Error('non-canonical cloud session')
|
|
87
|
+
}
|
|
88
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
91
|
+
}
|
|
92
|
+
if (!isRecord(parsed) || parsed.version !== CLOUD_SESSION_VERSION) {
|
|
93
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
94
|
+
}
|
|
95
|
+
const simId = parsed.simId
|
|
96
|
+
const apiOrigin = parsed.apiOrigin
|
|
97
|
+
const claimId = parsed.claimId
|
|
98
|
+
const token = parsed.token
|
|
99
|
+
if (
|
|
100
|
+
typeof simId !== 'string' ||
|
|
101
|
+
typeof apiOrigin !== 'string' ||
|
|
102
|
+
typeof claimId !== 'string' ||
|
|
103
|
+
typeof token !== 'string'
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return createCloudSession({ simId, apiOrigin, claimId, token })
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readCloudSession(
|
|
115
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
116
|
+
): CloudSession | null {
|
|
117
|
+
const encoded = environment[RNX_CLOUD_SESSION_ENV]
|
|
118
|
+
return encoded?.trim() ? parseCloudSession(encoded) : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function shellUpdateFd(
|
|
122
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
123
|
+
): number | null {
|
|
124
|
+
const source = environment[SHELL_UPDATE_FD_ENV]
|
|
125
|
+
if (!source) return null
|
|
126
|
+
const descriptor = Number(source)
|
|
127
|
+
if (!Number.isSafeInteger(descriptor) || descriptor < 3) {
|
|
128
|
+
throw new Error('RNX shell integration provided an invalid update channel')
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
fstatSync(descriptor)
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error('RNX shell integration update channel is unavailable')
|
|
134
|
+
}
|
|
135
|
+
return descriptor
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function requireCloudSessionShellUpdate(): number {
|
|
139
|
+
const descriptor = shellUpdateFd()
|
|
140
|
+
if (descriptor === null) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'RNX shell integration is required so this cloud session stays in the current shell; restart the shell after installing RNX',
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return descriptor
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function publishCloudSessionToShell(
|
|
149
|
+
session: CloudSession | null,
|
|
150
|
+
descriptor = requireCloudSessionShellUpdate(),
|
|
151
|
+
): void {
|
|
152
|
+
const update = session
|
|
153
|
+
? `${RNX_SHELL_UPDATE_SET_PREFIX}${serializeCloudSession(session)}`
|
|
154
|
+
: RNX_SHELL_UPDATE_UNSET
|
|
155
|
+
writeSync(descriptor, `${update}\n`)
|
|
156
|
+
}
|
package/cli/command-contract.ts
CHANGED
|
@@ -67,8 +67,8 @@ export interface RnxParsedCommandInput {
|
|
|
67
67
|
|
|
68
68
|
export interface RnxPreviewInvocation extends RnxInvocationBase {
|
|
69
69
|
family: 'preview'
|
|
70
|
-
command: 'web' | 'ios'
|
|
71
|
-
platform: 'web' | 'ios'
|
|
70
|
+
command: 'web' | 'ios' | 'android'
|
|
71
|
+
platform: 'web' | 'ios' | 'android'
|
|
72
72
|
selection: 'select-or-ensure-existing'
|
|
73
73
|
}
|
|
74
74
|
|
package/cli/command-registry.ts
CHANGED
|
@@ -74,6 +74,7 @@ const NODE_COMMAND_NAMES = [
|
|
|
74
74
|
const BROWSER_FAMILIES = new Map<string, RnxBrowserCommandFamily>([
|
|
75
75
|
['web', 'preview'],
|
|
76
76
|
['ios', 'preview'],
|
|
77
|
+
['android', 'preview'],
|
|
77
78
|
['list', 'runtime'],
|
|
78
79
|
['use', 'runtime'],
|
|
79
80
|
['state', 'runtime'],
|
|
@@ -248,6 +249,14 @@ export function createRnxCommandInvocation(
|
|
|
248
249
|
platform: 'ios',
|
|
249
250
|
selection: 'select-or-ensure-existing',
|
|
250
251
|
}
|
|
252
|
+
case 'android':
|
|
253
|
+
return {
|
|
254
|
+
...common,
|
|
255
|
+
family: 'preview',
|
|
256
|
+
command: 'android',
|
|
257
|
+
platform: 'android',
|
|
258
|
+
selection: 'select-or-ensure-existing',
|
|
259
|
+
}
|
|
251
260
|
case 'describe':
|
|
252
261
|
return { ...common, family: 'tree' }
|
|
253
262
|
case 'find':
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// agent-wrapper — long-running bridge process between
|
|
1
|
+
// agent-wrapper — long-running bridge process between rnx desktop and a
|
|
2
2
|
// local coding agent (codex / claude).
|
|
3
3
|
//
|
|
4
|
-
// shipped as an internal
|
|
4
|
+
// shipped as an internal rnx CLI subcommand so it survives `bun build
|
|
5
5
|
// --compile` bundling. callers spawn it as:
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// rnx agent-wrapper \
|
|
8
8
|
// --session-id <id> --project-id <id> --provider codex \
|
|
9
9
|
// --cwd <path> --prompt-in <fifo> --events-out <fifo> \
|
|
10
10
|
// [--transcript <file>]
|
|
@@ -407,7 +407,7 @@ export async function runAgentWrapper(argv: string[]): Promise<number> {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
// open prompt.in with O_RDWR so our own write end keeps the FIFO alive
|
|
410
|
-
// across external writer open/close cycles. `
|
|
410
|
+
// across external writer open/close cycles. `rnx agent prompt` opens,
|
|
411
411
|
// writes one line, closes — without a self-writer the reader would EOF
|
|
412
412
|
// after the first prompt and subsequent prompts would be lost.
|
|
413
413
|
const promptFd = openSync(promptIn, fsConstants.O_RDWR)
|
package/cli/commands/agent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// user-facing `
|
|
1
|
+
// user-facing `rnx agent` command tree.
|
|
2
2
|
//
|
|
3
3
|
// session-lifecycle commands (start, prompt, end, watch) route through
|
|
4
4
|
// the bridge daemon — the daemon owns the single FIFO reader per
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// disk-only inspection commands (projects, sessions, paths, transcript,
|
|
10
10
|
// attach, project show/remove) read the attached-projects JSON directly —
|
|
11
|
-
// no daemon needed, so `
|
|
11
|
+
// no daemon needed, so `rnx agent projects` stays fast when no
|
|
12
12
|
// wrapper is live.
|
|
13
13
|
|
|
14
14
|
import fs from 'node:fs'
|
|
@@ -96,7 +96,7 @@ async function withDaemon<T>(fn: (client: AgentDaemonClient) => Promise<T>): Pro
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function printHelp(): void {
|
|
99
|
-
// delegate to the meta-driven renderer so bare `
|
|
99
|
+
// delegate to the meta-driven renderer so bare `rnx agent`, `--help`,
|
|
100
100
|
// and `unknown subcommand` paths all show the same rich help (header,
|
|
101
101
|
// options, examples) instead of a sparse hand-coded summary.
|
|
102
102
|
printCommandHelp('agent')
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { execFileSync } from 'node:child_process'
|
|
13
13
|
import fs from 'node:fs'
|
|
14
14
|
import path from 'node:path'
|
|
15
|
-
import type { ProjectFileMeta, ProjectFilePlane } from '@
|
|
15
|
+
import type { ProjectFileMeta, ProjectFilePlane } from '@rnx/box/plane'
|
|
16
16
|
|
|
17
17
|
export class CheckoutRequiresGitError extends Error {}
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ export class CheckoutRequiresGitError extends Error {}
|
|
|
20
20
|
// rather than acted on here: moving a binary file is byte-exact and perfectly
|
|
21
21
|
// fine, and so is deleting one, but a shell command that produced TEXT cannot
|
|
22
22
|
// be allowed to overwrite it, because that text is a lossy decode of the
|
|
23
|
-
// file's own bytes.
|
|
23
|
+
// file's own bytes. BoxProjectFs owns that refusal. detecting it at write
|
|
24
24
|
// time instead is not an option: just-bash's `cat` discards every readFile
|
|
25
25
|
// error and reports "No such file or directory" whatever the error says, so
|
|
26
26
|
// the fact has to be in the index where the filesystem layer can read it.
|
|
@@ -88,10 +88,15 @@ export class CheckoutFilePlane implements ProjectFilePlane {
|
|
|
88
88
|
// long a wedged git is waited on, never whether it is waited on, so proving
|
|
89
89
|
// the kill does not have to cost the production deadline.
|
|
90
90
|
private readonly listTimeoutMs: number
|
|
91
|
+
private readonly trackedOnly: boolean
|
|
91
92
|
|
|
92
|
-
constructor(
|
|
93
|
+
constructor(
|
|
94
|
+
root: string,
|
|
95
|
+
options: { listTimeoutMs?: number; trackedOnly?: boolean } = {},
|
|
96
|
+
) {
|
|
93
97
|
this.root = path.resolve(root)
|
|
94
98
|
this.listTimeoutMs = options.listTimeoutMs ?? LIST_FILES_TIMEOUT_MS
|
|
99
|
+
this.trackedOnly = options.trackedOnly ?? false
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
index(): ReadonlyMap<string, ProjectFileMeta> {
|
|
@@ -101,10 +106,13 @@ export class CheckoutFilePlane implements ProjectFilePlane {
|
|
|
101
106
|
async refresh(): Promise<void> {
|
|
102
107
|
let stdout: string
|
|
103
108
|
try {
|
|
104
|
-
// -z
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
109
|
+
// -z keeps paths containing spaces or newlines intact. a local Box sees
|
|
110
|
+
// tracked plus visible untracked files, while a durable cloud import is
|
|
111
|
+
// deliberately restricted to the checkout's tracked source.
|
|
112
|
+
const args = this.trackedOnly
|
|
113
|
+
? ['ls-files', '-z']
|
|
114
|
+
: ['ls-files', '-co', '--exclude-standard', '-z']
|
|
115
|
+
stdout = execFileSync('git', args, {
|
|
108
116
|
cwd: this.root,
|
|
109
117
|
encoding: 'utf8',
|
|
110
118
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -137,7 +145,7 @@ export class CheckoutFilePlane implements ProjectFilePlane {
|
|
|
137
145
|
// than a broken box, and nothing would say so.
|
|
138
146
|
if (stdout.length === 0) {
|
|
139
147
|
throw new CheckoutRequiresGitError(
|
|
140
|
-
`git listed no files in ${this.root}. either
|
|
148
|
+
`git listed no ${this.trackedOnly ? 'tracked ' : ''}files in ${this.root}. either the checkout contains no eligible files, or this machine could not read the child process's output.`,
|
|
141
149
|
)
|
|
142
150
|
}
|
|
143
151
|
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process'
|
|
10
10
|
import path from 'node:path'
|
|
11
|
-
import { defineCommand, type CustomCommand } from '@
|
|
12
|
-
import { PROJECT_MOUNT } from '@
|
|
11
|
+
import { defineCommand, type CustomCommand } from '@rnx/box/shell'
|
|
12
|
+
import { PROJECT_MOUNT } from '@rnx/box/shell'
|
|
13
13
|
import { rnxPublicBrand } from '../../../src/public-brand'
|
|
14
14
|
import { RNX_PLACEMENT_ENV } from '../../ws-bridge'
|
|
15
15
|
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
// whose "no match here" had to be merged.
|
|
33
33
|
|
|
34
34
|
import { spawn } from 'node:child_process'
|
|
35
|
-
import { defineCommand, type CustomCommand } from '@
|
|
36
|
-
import { PROJECT_MOUNT } from '@
|
|
35
|
+
import { defineCommand, type CustomCommand } from '@rnx/box/shell'
|
|
36
|
+
import { PROJECT_MOUNT } from '@rnx/box/shell'
|
|
37
37
|
|
|
38
38
|
// generous next to a 414ms p95: this bounds a wedged child, it does not cut off
|
|
39
39
|
// a large search.
|