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.
Files changed (138) hide show
  1. package/README.md +43 -26
  2. package/cli/app-fonts.ts +1 -1
  3. package/cli/bin.ts +118 -26
  4. package/cli/bridge-diagnostics.ts +1 -1
  5. package/cli/bridge-flow-runner.ts +9 -9
  6. package/cli/browser-evals.ts +3 -3
  7. package/cli/cli-update.ts +261 -0
  8. package/cli/cloud-client.ts +407 -0
  9. package/cli/cloud-dispatch.ts +165 -0
  10. package/cli/cloud-session.ts +156 -0
  11. package/cli/command-contract.ts +2 -2
  12. package/cli/command-registry.ts +9 -0
  13. package/cli/commands/agent-wrapper.ts +4 -4
  14. package/cli/commands/agent.ts +3 -3
  15. package/cli/commands/box/checkout-plane.ts +16 -8
  16. package/cli/commands/box/rnx-command.ts +2 -2
  17. package/cli/commands/box/search-command.ts +2 -2
  18. package/cli/commands/box.ts +123 -70
  19. package/cli/commands/config.ts +3 -2
  20. package/cli/commands/control.ts +6 -6
  21. package/cli/commands/cpu-profile.ts +2 -2
  22. package/cli/commands/daemon.ts +7 -7
  23. package/cli/commands/debug.ts +2 -2
  24. package/cli/commands/detox.ts +7 -7
  25. package/cli/commands/diagnose.ts +1 -1
  26. package/cli/commands/electron.ts +1 -1
  27. package/cli/commands/film.ts +1 -1
  28. package/cli/commands/flow.ts +4 -4
  29. package/cli/commands/inspect/core.ts +58 -7
  30. package/cli/commands/inspect/env.ts +5 -3
  31. package/cli/commands/inspect/shared.ts +2 -1
  32. package/cli/commands/inspect/wait-event.ts +1 -1
  33. package/cli/commands/inspect/wait-ready.ts +15 -5
  34. package/cli/commands/inspect.ts +31 -9
  35. package/cli/commands/install-desktop.ts +2 -2
  36. package/cli/commands/maestro.ts +3 -3
  37. package/cli/commands/permissions.ts +1 -1
  38. package/cli/commands/platform.ts +91 -0
  39. package/cli/commands/record.ts +3 -3
  40. package/cli/commands/report-issue.ts +1 -1
  41. package/cli/commands/runtime.ts +1 -1
  42. package/cli/commands/screenshot-mode.ts +1 -1
  43. package/cli/commands/screenshot.ts +25 -0
  44. package/cli/commands/serve.ts +1 -1
  45. package/cli/commands/skills.ts +1 -1
  46. package/cli/commands/timeline.ts +1 -1
  47. package/cli/commands/upgrade.ts +8 -34
  48. package/cli/commands/upload.ts +3 -3
  49. package/cli/commands/version.ts +14 -3
  50. package/cli/commands/what-happened.ts +2 -2
  51. package/cli/current-sim.ts +43 -30
  52. package/cli/desktop-companion.ts +1 -1
  53. package/cli/drivers/electron.ts +1 -1
  54. package/cli/drivers/playwright.ts +3 -3
  55. package/cli/drivers/types.ts +4 -4
  56. package/cli/flow-file.ts +3 -3
  57. package/cli/help-core.ts +18 -11
  58. package/cli/hidden-runtime-alias.ts +1 -1
  59. package/cli/hints.ts +2 -2
  60. package/cli/maestro-js.ts +2 -2
  61. package/cli/outbound-endpoints.ts +40 -6
  62. package/cli/parse-args.ts +1 -1
  63. package/cli/registry.ts +1 -1
  64. package/cli/resolve-assets.ts +2 -2
  65. package/cli/secure-file.ts +31 -0
  66. package/cli/shell-init.ts +98 -0
  67. package/cli/ws-bridge.ts +18 -8
  68. package/detox/expectations.ts +1 -1
  69. package/detox/gestures.ts +3 -3
  70. package/detox/index.ts +3 -3
  71. package/dist-lib/agent-daemon-client.cjs +1 -1
  72. package/dist-lib/agent-events.cjs +1 -1
  73. package/dist-lib/agent-identity.cjs +1 -1
  74. package/dist-lib/agent-sessions.cjs +1 -1
  75. package/dist-lib/attached-projects.cjs +1 -1
  76. package/dist-lib/auth/shared-session.cjs +1 -1
  77. package/dist-lib/backend-origin.cjs +1 -1
  78. package/dist-lib/beta.cjs +1 -1
  79. package/dist-lib/beta.mjs +1 -1
  80. package/dist-lib/bridge-constants.cjs +1 -1
  81. package/dist-lib/bridge-contract-input.cjs +15 -8
  82. package/dist-lib/bridge-contract-input.mjs +15 -8
  83. package/dist-lib/bridge-contract.cjs +1 -1
  84. package/dist-lib/bridge-contract.mjs +1 -1
  85. package/dist-lib/capture-contract.cjs +107 -0
  86. package/dist-lib/capture-contract.mjs +80 -0
  87. package/dist-lib/cli-constants.cjs +1 -1
  88. package/dist-lib/cloud-contract.cjs +32 -0
  89. package/dist-lib/cloud-contract.mjs +7 -0
  90. package/dist-lib/config.cjs +1 -1
  91. package/dist-lib/detox/index.cjs +4 -4
  92. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  93. package/dist-lib/home-paths.cjs +9 -1
  94. package/dist-lib/host/bridge-host.cjs +15 -10
  95. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  96. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  97. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  98. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  99. package/dist-lib/host/websocket-proxy.cjs +1 -1
  100. package/dist-lib/index.cjs +248 -73
  101. package/dist-lib/jump-to-source-babel.cjs +1 -1
  102. package/dist-lib/menu.cjs +1 -1
  103. package/dist-lib/menu.mjs +1 -1
  104. package/dist-lib/metro-production-bundle.cjs +1 -1
  105. package/dist-lib/metro-production-bundle.mjs +1 -1
  106. package/dist-lib/metro.cjs +1 -1
  107. package/dist-lib/profiles.cjs +1 -1
  108. package/dist-lib/public-brand.cjs +1 -1
  109. package/dist-lib/react-native-host-modules.cjs +1 -1
  110. package/dist-lib/react-native-host-modules.mjs +1 -1
  111. package/dist-lib/render-mode.cjs +1 -1
  112. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  113. package/dist-lib/sdk.cjs +44 -6
  114. package/dist-lib/sdk.mjs +44 -6
  115. package/dist-lib/skills.cjs +435 -155
  116. package/dist-lib/vite.cjs +1 -1
  117. package/package.json +21 -5
  118. package/scripts/dev-server-scanner.ts +3 -3
  119. package/skills/contrast/SKILL.md +1 -1
  120. package/skills/rnx-perf/SKILL.md +68 -52
  121. package/src/app-url.ts +2 -3
  122. package/src/bridge-contract-input.ts +17 -6
  123. package/src/bridge-contract.ts +25 -4
  124. package/src/capture-contract.ts +423 -0
  125. package/src/cloud-contract.ts +15 -0
  126. package/src/home-paths.ts +9 -3
  127. package/src/host/agent-host.ts +1 -1
  128. package/src/host/bridge-host.ts +13 -11
  129. package/src/host/plane-host.ts +2 -2
  130. package/src/index.ts +21 -0
  131. package/src/native-dev-bundle-url.ts +2 -2
  132. package/src/screenshots/orchestrate.ts +1 -1
  133. package/src/screenshots/org-deck.ts +1 -1
  134. package/src/settings-bundle.ts +1 -1
  135. package/src/sim-client.ts +4 -0
  136. package/src/vite-plugin-one.ts +2 -2
  137. package/src/vite-plugin.ts +1 -1
  138. 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
+ }
@@ -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
 
@@ -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 sootsim desktop and a
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 sootsim CLI subcommand so it survives `bun build
4
+ // shipped as an internal rnx CLI subcommand so it survives `bun build
5
5
  // --compile` bundling. callers spawn it as:
6
6
  //
7
- // sootsim agent-wrapper \
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. `sootsim agent prompt` opens,
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)
@@ -1,4 +1,4 @@
1
- // user-facing `sootsim agent` command tree.
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 `sootsim agent projects` stays fast when no
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 `sootsim agent`, `--help`,
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 '@contrast/harness/plane'
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. HarnessProjectFs owns that refusal. detecting it at write
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(root: string, options: { listTimeoutMs?: number } = {}) {
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 so paths containing spaces or newlines survive; -co --exclude-standard
105
- // is tracked plus untracked-not-ignored, which is what a person means by
106
- // "the files in my project".
107
- stdout = execFileSync('git', ['ls-files', '-co', '--exclude-standard', '-z'], {
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 git tracks nothing there and nothing untracked is visible to it, or this machine could not read the child process's output.`,
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 '@contrast/harness/shell'
12
- import { PROJECT_MOUNT } from '@contrast/harness/shell'
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 '@contrast/harness/shell'
36
- import { PROJECT_MOUNT } from '@contrast/harness/shell'
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.