@felipe-software/react-native-gpui 0.0.0-stage → 1.0.1

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 (72) hide show
  1. package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
  2. package/LICENSE +21 -0
  3. package/README.md +657 -2
  4. package/cli/bin.ts +379 -0
  5. package/cli/bundle.ts +32 -0
  6. package/cli/commands/do.ts +213 -0
  7. package/cli/commands/flow.ts +283 -0
  8. package/cli/commands/get.ts +584 -0
  9. package/cli/commands/shot.ts +312 -0
  10. package/cli/commands/trace.ts +293 -0
  11. package/cli/host.ts +714 -0
  12. package/cli/hotReload.ts +210 -0
  13. package/cli/run.ts +66 -0
  14. package/cli/selectors.ts +205 -0
  15. package/dist/Animated.d.ts +120 -0
  16. package/dist/ChatList.d.ts +28 -0
  17. package/dist/Dimensions.d.ts +31 -0
  18. package/dist/StyleSheet.d.ts +18 -0
  19. package/dist/Window.d.ts +33 -0
  20. package/dist/apis.d.ts +165 -0
  21. package/dist/app-state.d.ts +27 -0
  22. package/dist/chat-list-types.d.ts +136 -0
  23. package/dist/colors.d.ts +32 -0
  24. package/dist/commands.d.ts +230 -0
  25. package/dist/components.d.ts +419 -0
  26. package/dist/hotModules.d.ts +1 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.development.js +14876 -0
  29. package/dist/index.js +12 -0
  30. package/dist/keyboard.d.ts +140 -0
  31. package/dist/native-menu.d.ts +7 -0
  32. package/dist/platform-driver.d.ts +32 -0
  33. package/dist/platform-services.d.ts +35 -0
  34. package/dist/portal.d.ts +43 -0
  35. package/dist/raf.d.ts +4 -0
  36. package/dist/reanimated/colors-processor-shim.d.ts +10 -0
  37. package/dist/reanimated/reanimated-host.d.ts +10 -0
  38. package/dist/reanimated/seam.d.ts +34 -0
  39. package/dist/reanimated/ui-entry.d.ts +2 -0
  40. package/dist/reanimated/worklet-channel.d.ts +10 -0
  41. package/dist/reanimated/worklet-runtime.d.ts +466 -0
  42. package/dist/reanimated/worklets.d.ts +199 -0
  43. package/dist/reconciler.d.ts +102 -0
  44. package/dist/refresh.d.ts +1 -0
  45. package/dist/render.d.ts +37 -0
  46. package/dist/runtime.d.ts +235 -0
  47. package/dist/surfaces.d.ts +51 -0
  48. package/dist/types.d.ts +276 -0
  49. package/dist/ui-runtime.js +42276 -0
  50. package/dist/window-context.d.ts +7 -0
  51. package/dist/window-host.d.ts +41 -0
  52. package/dist/wire-delta.d.ts +20 -0
  53. package/package.json +176 -5
  54. package/scripts/bundle-app.mjs +88 -0
  55. package/scripts/conformance-utils.mjs +293 -0
  56. package/scripts/hot-update-alias-plugin.mjs +88 -0
  57. package/scripts/native-package-exports-plugin.mjs +235 -0
  58. package/scripts/pixel.mjs +152 -0
  59. package/scripts/png.mjs +198 -0
  60. package/scripts/react-refresh-swc.mjs +35 -0
  61. package/scripts/reanimated-bun-plugin.mjs +188 -0
  62. package/scripts/service-binary.d.mts +27 -0
  63. package/scripts/service-binary.mjs +100 -0
  64. package/src/reanimated/colors-processor-shim.ts +119 -0
  65. package/src/reanimated/reanimated-host.ts +69 -0
  66. package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
  67. package/src/reanimated/seam.ts +566 -0
  68. package/src/reanimated/ui-entry.ts +80 -0
  69. package/src/reanimated/worklet-channel.ts +64 -0
  70. package/src/reanimated/worklet-runtime.ts +2569 -0
  71. package/src/reanimated/worklets.ts +1334 -0
  72. package/src/refresh.ts +33 -0
package/cli/bin.ts ADDED
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env bun
2
+ // `rngpui` — the react-native-gpui developer CLI. A get/do devtool over a running OR
3
+ // launched offscreen rngpui app, modeled on soot's sootsim CLI.
4
+ //
5
+ // rngpui get describe composer:input --launch examples/superconductor.tsx
6
+ // rngpui get color 200,300 --attach
7
+ // rngpui do tap "Run" --launch examples/kitchen-sink.tsx
8
+ //
9
+ // GET commands introspect (tree / stats / webviews / describe / layout / style / color / point); DO
10
+ // commands drive (tap / type / key / scroll / drag). Selectors substring-match testID /
11
+ // identifier / nativeID / label / text / type, or `#<globalId>`, or `x,y`. `--json`
12
+ // for machine output.
13
+
14
+ import { attachHost, attachSession, closeSession, launchHost, scopeToWindow, type AttachedHost, type LaunchedHost } from "./host";
15
+ import { runGet } from "./commands/get";
16
+ import { runDo } from "./commands/do";
17
+ import { runFlow } from "./commands/flow";
18
+ import { runTrace } from "./commands/trace";
19
+ import { runDiff, runShot, type ShotFlags } from "./commands/shot";
20
+ import { runHotReload } from "./hotReload";
21
+ import { bundleApp } from "./bundle";
22
+ import { runApp } from "./run";
23
+ import { join, resolve } from "node:path";
24
+
25
+ const HELP = `rngpui — react-native-gpui developer CLI
26
+
27
+ usage:
28
+ rngpui bundle <entry.tsx> <out.js> [--dev] bundle an app for rngpui-service (production unless --dev)
29
+ rngpui run <entry.tsx | --bundle app.js> [--dev] [--size WxH] bundle the app and open its window
30
+ rngpui shot <--bundle app.js | --launch entry.tsx | --session dir> [--size WxH] [--appearance light|dark] [--select id ...] [--out png] [--fixture] [--keep]
31
+ rngpui reshot --session <dir> [--window <sel>] [--select id ...] [--out png] sub-second re-capture of a kept session
32
+ rngpui dev --launch <entry.tsx> [--root <dir> ...] [--size WxH] launch once and Fast Refresh on edit
33
+ rngpui dev --socket <control.sock> --build <cmd> --bundle <bundle.js> --root <dir> Fast Refresh an owned external instance
34
+ rngpui diff <before.png> <after.png> [--out highlight.png] [--threshold n]
35
+ rngpui <get|do> <subcommand> [selector] [--launch <entry.tsx> | --bundle <app.js> | --session <dir> | --attach] [--json]
36
+ rngpui trace <selector ...|--all> [--keys k1,k2] [--ms n] [--action "tap <sel>"] [target] [--json]
37
+ rngpui flow [--profile] tap <selector> [tap <selector> ...] [--out <dir>] [target]
38
+ rngpui close --session <dir>
39
+
40
+ the fast loop (offscreen, no screenshots, one command):
41
+ shot launch → wait for a stable frame → write png + tree → print
42
+ the png path + bounds/color of every --select'd node. ONE call.
43
+ dev launch one development instance and keep its PID and native state
44
+ while JavaScriptCore React Refresh applies source edits.
45
+ reshot re-capture a kept --session instantly (state/data changed → look again).
46
+ diff compare two pngs: changed-pixel ratio + changed-region box (+ --out highlight).
47
+
48
+ shot/dev flags:
49
+ --appearance light|dark force the app theme (RNGPUI_FORCE_APPEARANCE) — no system toggle
50
+ --select <selector> measure this node (repeatable): prints bounds + sampled color
51
+ --crop <x,y,w,h> (reserved) region of interest
52
+ --fixture load deterministic demo data (TM_FIXTURE_ONLY=1) — the
53
+ team-machine app paints an empty shell without a daemon or fixture
54
+ --out <png> where to write the capture (default /tmp/rngpui-shot.png)
55
+
56
+ target (pick one; defaults to --attach):
57
+ --launch <entry.tsx> bundle the entry to JavaScript source, then spawn rngpui-service
58
+ --bundle <app.js> spawn rngpui-service against an existing JavaScript source bundle
59
+ --keep keep the launched service alive and print its session dir
60
+ --session <dir> reuse a kept driveable session for do-then-get workflows
61
+ --attach attach to a running rngpui window; driveable only when
62
+ owner metadata advertises RNGPUI_CONTROL_SOCKET
63
+ --size <WxH> launched window size (default 1280x860)
64
+ --window <sel> aim get/do/trace at one window: an id from \`get windows\`,
65
+ main, a <Window testID>, a kind (dialog/popup/normal) or a
66
+ title substring
67
+
68
+ get (introspect — read-only):
69
+ get windows every window the service shows: id, kind, title,
70
+ testID, bounds, node count, frames painted
71
+ get screen actionable visible-screen summary
72
+ get tree full annotated node tree (bounds + ids)
73
+ get stats [selector] aggregate node counts, visibility, duplicate IDs,
74
+ type/list-group counts, and webview totals
75
+ get webviews webview inventory: source, bounds, visibility
76
+ get describe [selector] path, ids, computed bounds, resolved style, AND
77
+ sampled dominant/average color within the bounds
78
+ get layout [selector] computed window-coordinate bounds per node
79
+ get style <selector> resolved style facts (incl. background/border/color)
80
+ get color <selector|x,y> sampled dominant/average color in a node or at a point
81
+ get point <x,y> topmost node + pixel color at a window point
82
+ get frames painted-frame counter + live fps + frame-gap stats
83
+ get provenance native renderer/service/GPUI/JavaScriptCore/bundle identity
84
+
85
+ trace (animation forensics — no screenshots):
86
+ trace <selector ...> record every animated style write (off-thread
87
+ reanimated / tamagui driver) and NativeLayout tween
88
+ tick under the matched subtrees, then report per-key
89
+ curves: sparkline, endpoints, min/max, sample cadence,
90
+ dropped-frame gaps, spring overshoot count.
91
+ --all trace every node instead of a subtree
92
+ --keys opacity,transform only record these style keys
93
+ --ms <n> observation window after actions fire (default 1200)
94
+ --action "tap <sel>" fire input after arming (repeatable; tap/key/type)
95
+ example: rngpui trace dialog --action "tap open-settings" --ms 1500 --session <dir>
96
+
97
+ do (drive):
98
+ do tap <selector|x,y> synthesize a press at the node center / point
99
+ do type <text> type into the focused input
100
+ do hud <on|off> toggle the native in-window performance HUD
101
+ do key <key> send one key (enter, backspace, space, a, …)
102
+ do scroll <selector|x,y> <dx,dy> scroll the container at the point by a delta
103
+ do drag <from> <to> [steps] synthesize an owned offscreen press-drag
104
+
105
+ flow:
106
+ flow --profile tap "Session A" tap "Session B"
107
+ run semantic taps and write trees/screenshots
108
+ plus profile.json (default /tmp/rngpui-flow-*)
109
+ --no-screenshots keep profile timing clean for rapid stress flows
110
+ --settle-ms <n> wait up to n ms for first tree change per step
111
+ (default 700; use 0 for rapid fire)
112
+ --cadence-ms <n> fixed delay after each tap before the next step
113
+
114
+ selectors:
115
+ #42 globalId 42
116
+ composer substring match on testID/identifier/nativeID/label/text/type
117
+ 200,300 literal window-coordinate point
118
+
119
+ examples:
120
+ rngpui shot --bundle native-shell/.gpui/app.js --size 1360x880 --fixture --appearance dark --select stage --select trees-rail
121
+ rngpui dev --launch app/index.tsx --fixture # Fast Refresh until Ctrl-C
122
+ rngpui reshot --session /var/.../rngpui-cli-XXXX --select composer
123
+ rngpui diff /tmp/before.png /tmp/rngpui-shot.png --out /tmp/diff.png
124
+ rngpui get describe stage --launch examples/superconductor.tsx
125
+ rngpui get tree --launch examples/kitchen-sink.tsx --keep
126
+ rngpui do tap count-button --session /tmp/rngpui-cli-abc123
127
+ rngpui get describe --attach
128
+ rngpui close --session /tmp/rngpui-cli-abc123
129
+ `;
130
+
131
+ function parseArgs(argv: string[]) {
132
+ const args = argv.slice(2);
133
+ const flags: Record<string, string | boolean> = {};
134
+ const select: string[] = [];
135
+ const actions: string[] = [];
136
+ const roots: string[] = [];
137
+ const ignores: string[] = [];
138
+ const extensions: string[] = [];
139
+ const positional: string[] = [];
140
+ for (let i = 0; i < args.length; i++) {
141
+ const a = args[i];
142
+ if (a === "--json") flags.json = true;
143
+ else if (a === "--attach") flags.attach = true;
144
+ else if (a === "--launch") flags.launch = args[++i] ?? "";
145
+ else if (a === "--bundle") flags.bundle = args[++i] ?? "";
146
+ else if (a === "--session") flags.session = args[++i] ?? "";
147
+ else if (a === "--window") flags.window = args[++i] ?? "";
148
+ else if (a === "--keep") flags.keep = true;
149
+ else if (a === "--size") flags.size = args[++i] ?? "";
150
+ else if (a === "--appearance") flags.appearance = args[++i] ?? "";
151
+ else if (a === "--select") select.push(args[++i] ?? "");
152
+ else if (a === "--crop") flags.crop = args[++i] ?? "";
153
+ else if (a === "--fixture") flags.fixture = true;
154
+ else if (a === "--threshold") flags.threshold = args[++i] ?? "";
155
+ else if (a === "--profile") flags.profile = true;
156
+ else if (a === "--screenshots") flags.screenshots = true;
157
+ else if (a === "--no-screenshots") flags.screenshots = false;
158
+ else if (a === "--settle-ms") flags.settleMs = args[++i] ?? "";
159
+ else if (a === "--cadence-ms") flags.cadenceMs = args[++i] ?? "";
160
+ else if (a === "--keys") flags.keys = args[++i] ?? "";
161
+ else if (a === "--ms") flags.ms = args[++i] ?? "";
162
+ else if (a === "--action") actions.push(args[++i] ?? "");
163
+ else if (a === "--all") flags.all = true;
164
+ else if (a === "--out") flags.out = args[++i] ?? "";
165
+ else if (a === "--socket") flags.socket = args[++i] ?? "";
166
+ else if (a === "--build") flags.build = args[++i] ?? "";
167
+ else if (a === "--root") roots.push(args[++i] ?? "");
168
+ else if (a === "--ignore") ignores.push(args[++i] ?? "");
169
+ else if (a === "--ext") extensions.push(args[++i] ?? "");
170
+ else if (a === "--debounce-ms") flags.debounceMs = args[++i] ?? "";
171
+ else if (a === "--label") flags.label = args[++i] ?? "";
172
+ else if (a === "--once") flags.once = true;
173
+ else if (a === "--dev") flags.dev = true;
174
+ else if (a === "-h" || a === "--help") flags.help = true;
175
+ else positional.push(a);
176
+ }
177
+ return { flags, positional, select, actions, roots, ignores, extensions };
178
+ }
179
+
180
+ function shotFlags(flags: Record<string, string | boolean>, select: string[]): ShotFlags {
181
+ return {
182
+ json: flags.json === true,
183
+ size: flags.size ? String(flags.size) : undefined,
184
+ appearance: flags.appearance ? String(flags.appearance) : undefined,
185
+ out: flags.out ? String(flags.out) : undefined,
186
+ select: select.filter(Boolean),
187
+ crop: flags.crop ? String(flags.crop) : undefined,
188
+ fixture: flags.fixture === true,
189
+ session: flags.session ? String(flags.session) : process.env.RNGPUI_SESSION || undefined,
190
+ bundle: flags.bundle !== undefined ? String(flags.bundle) : undefined,
191
+ launch: flags.launch !== undefined ? String(flags.launch) : undefined,
192
+ keep: flags.keep === true,
193
+ window: typeof flags.window === "string" && flags.window ? flags.window : undefined,
194
+ };
195
+ }
196
+
197
+ function bundleCommand(args: string[], flags: Record<string, string | boolean>): number {
198
+ const [entry, positionalOut] = args;
199
+ const out = flags.out ? String(flags.out) : positionalOut;
200
+ if (!entry || !out) {
201
+ console.error(" bundle needs an entry and an output path: rngpui bundle app.tsx dist/app.js");
202
+ return 1;
203
+ }
204
+ bundleApp(entry, out, { development: flags.dev === true, inheritOutput: true });
205
+ return 0;
206
+ }
207
+
208
+ function runCommand(entry: string | undefined, flags: Record<string, string | boolean>): Promise<number> {
209
+ return runApp({
210
+ entry,
211
+ bundle: flags.bundle ? String(flags.bundle) : undefined,
212
+ development: flags.dev === true,
213
+ size: flags.size ? String(flags.size) : undefined,
214
+ });
215
+ }
216
+
217
+ async function main(): Promise<number> {
218
+ const { flags, positional, select, actions, roots, ignores, extensions } = parseArgs(process.argv);
219
+ if (flags.help || positional.length === 0) {
220
+ console.log(HELP);
221
+ return positional.length === 0 ? 1 : 0;
222
+ }
223
+
224
+ const [group, sub, ...rest] = positional;
225
+ const json = flags.json === true;
226
+
227
+ if (group === "bundle" || group === "run") {
228
+ try {
229
+ return group === "bundle" ? bundleCommand(positional.slice(1), flags) : await runCommand(positional[1], flags);
230
+ } catch (err) {
231
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
232
+ return 1;
233
+ }
234
+ }
235
+
236
+ // The fast iteration commands own their whole host lifecycle (launch/attach +
237
+ // capture + close), so they short-circuit the generic get/do/flow plumbing.
238
+ if (group === "shot") {
239
+ return runShot(shotFlags(flags, select));
240
+ }
241
+ if (group === "reshot") {
242
+ const sf = shotFlags(flags, select);
243
+ if (!sf.session) {
244
+ console.error(" reshot needs --session <dir> (the dir `rngpui dev` printed) or RNGPUI_SESSION");
245
+ return 1;
246
+ }
247
+ return runShot(sf);
248
+ }
249
+ if (group === "dev") {
250
+ const entry = flags.launch ? resolve(String(flags.launch)) : undefined;
251
+ if (entry) {
252
+ const host = await launchHost(entry, {
253
+ size: flags.size ? String(flags.size) : undefined,
254
+ development: true,
255
+ appearance: flags.appearance ? String(flags.appearance) : undefined,
256
+ fixture: flags.fixture === true,
257
+ });
258
+ console.log(`[rngpui-dev] pid=${host.servicePid} session=${host.sessionDir}`);
259
+ try {
260
+ return await runHotReload({
261
+ socketPath: host.controlSocketPath,
262
+ roots: roots.filter(Boolean).length ? roots.filter(Boolean) : [process.cwd()],
263
+ entryPath: entry,
264
+ bundlePath: join(host.sessionDir, "hot.js"),
265
+ ignores: ignores.filter(Boolean),
266
+ extensions: extensions.filter(Boolean),
267
+ debounceMs: flags.debounceMs ? Number(flags.debounceMs) : undefined,
268
+ label: flags.label ? String(flags.label) : "rngpui-dev",
269
+ once: flags.once === true,
270
+ });
271
+ } finally {
272
+ host.close();
273
+ }
274
+ }
275
+ return runHotReload({
276
+ socketPath: flags.socket ? String(flags.socket) : undefined,
277
+ roots: roots.filter(Boolean),
278
+ buildCommand: flags.build ? String(flags.build) : undefined,
279
+ bundlePath: flags.bundle ? String(flags.bundle) : undefined,
280
+ ignores: ignores.filter(Boolean),
281
+ extensions: extensions.filter(Boolean),
282
+ debounceMs: flags.debounceMs ? Number(flags.debounceMs) : undefined,
283
+ label: flags.label ? String(flags.label) : undefined,
284
+ once: flags.once === true,
285
+ });
286
+ }
287
+ if (group === "diff") {
288
+ return runDiff(positional.slice(1), {
289
+ json,
290
+ out: flags.out ? String(flags.out) : undefined,
291
+ threshold: flags.threshold ? Number(flags.threshold) : undefined,
292
+ });
293
+ }
294
+
295
+ if (group === "close") {
296
+ const session = flags.session || process.env.RNGPUI_SESSION;
297
+ if (!session) {
298
+ console.error(" close needs --session <dir> or RNGPUI_SESSION");
299
+ return 1;
300
+ }
301
+ closeSession(String(session));
302
+ if (!json) console.log(` closed ${session}`);
303
+ else console.log(JSON.stringify({ closed: String(session) }));
304
+ return 0;
305
+ }
306
+
307
+ if (group !== "get" && group !== "do" && group !== "flow" && group !== "trace") {
308
+ console.error(` unknown command group: ${group}`);
309
+ console.error(" run `rngpui --help` for the surface.");
310
+ return 1;
311
+ }
312
+ if (!sub && group !== "flow" && !(group === "trace" && flags.all === true)) {
313
+ console.error(` ${group} needs a ${group === "trace" ? "selector (or --all)" : "subcommand"} — run \`rngpui --help\``);
314
+ return 1;
315
+ }
316
+
317
+ let host: LaunchedHost | AttachedHost | null = null;
318
+ try {
319
+ const session = flags.session || process.env.RNGPUI_SESSION;
320
+ if (session) {
321
+ host = await attachSession(String(session));
322
+ } else if (flags.bundle !== undefined) {
323
+ host = await launchHost("", {
324
+ bundle: String(flags.bundle),
325
+ keep: flags.keep === true,
326
+ size: flags.size ? String(flags.size) : undefined,
327
+ });
328
+ } else if (flags.launch !== undefined) {
329
+ host = await launchHost(String(flags.launch), {
330
+ keep: flags.keep === true,
331
+ size: flags.size ? String(flags.size) : undefined,
332
+ });
333
+ } else {
334
+ // default + explicit --attach both land here
335
+ host = await attachHost();
336
+ }
337
+ if (typeof flags.window === "string" && flags.window) {
338
+ host = await scopeToWindow(host, flags.window);
339
+ }
340
+
341
+ const code =
342
+ group === "get"
343
+ ? await runGet(host, sub, rest, json)
344
+ : group === "do"
345
+ ? await runDo(host, sub, rest, json)
346
+ : group === "trace"
347
+ ? await runTrace(host, sub ? [sub, ...rest] : [], {
348
+ json,
349
+ keys: flags.keys ? String(flags.keys).split(",").map((k) => k.trim()).filter(Boolean) : undefined,
350
+ ms: flags.ms ? Number(flags.ms) : 1_200,
351
+ actions: actions.filter(Boolean),
352
+ all: flags.all === true,
353
+ })
354
+ : await runFlow(host, sub ? [sub, ...rest] : rest, {
355
+ json,
356
+ profile: flags.profile === true,
357
+ screenshots: flags.screenshots === undefined ? undefined : flags.screenshots === true,
358
+ settleMs: flags.settleMs === undefined ? undefined : Number(flags.settleMs),
359
+ cadenceMs: flags.cadenceMs === undefined ? undefined : Number(flags.cadenceMs),
360
+ out: flags.out ? String(flags.out) : undefined,
361
+ });
362
+ if (flags.keep === true && host.mode === "launch") {
363
+ console.error(`[rngpui] session ${host.sessionDir}`);
364
+ }
365
+ return code;
366
+ } catch (err) {
367
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
368
+ return 1;
369
+ } finally {
370
+ host?.close();
371
+ }
372
+ }
373
+
374
+ main()
375
+ .then((code) => process.exit(code))
376
+ .catch((err) => {
377
+ console.error(err instanceof Error ? err.stack : String(err));
378
+ process.exit(1);
379
+ });
package/cli/bundle.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { packageRoot } from "../scripts/service-binary.mjs";
5
+
6
+ export type BundleOptions = {
7
+ development?: boolean;
8
+ inheritOutput?: boolean;
9
+ };
10
+
11
+ const bunExecutable = process.versions.bun ? process.execPath : "bun";
12
+
13
+ export function bundleApp(entry: string, out: string, options: BundleOptions = {}): string {
14
+ const entryPath = resolve(entry);
15
+ if (!existsSync(entryPath)) throw new Error(`entry not found: ${entryPath}`);
16
+ const outPath = resolve(out);
17
+ mkdirSync(dirname(outPath), { recursive: true });
18
+ const nodeEnv = options.development ? "development" : "production";
19
+ const stdio = options.inheritOutput ? "inherit" : "pipe";
20
+ const result = spawnSync(bunExecutable, [join(packageRoot, "scripts", "bundle-app.mjs"), entryPath, outPath], {
21
+ cwd: packageRoot,
22
+ encoding: "utf8",
23
+ stdio,
24
+ env: { ...process.env, NODE_ENV: nodeEnv },
25
+ });
26
+ if (result.status !== 0) {
27
+ const output = options.inheritOutput ? "" : `\n${result.stdout}\n${result.stderr}`;
28
+ throw new Error(`bundle-app failed${output}`);
29
+ }
30
+ if (!existsSync(outPath)) throw new Error(`bundle-app did not write ${outPath}`);
31
+ return outPath;
32
+ }
@@ -0,0 +1,213 @@
1
+ // `rngpui do ...` - drive the live instance with synthetic input. Launched
2
+ // sessions are driveable, and attached apps become driveable when their owner
3
+ // metadata advertises a debug control socket.
4
+
5
+ import { isDriveableHost, type AttachedHost, type DriveableHost, type DumpNode, type LaunchedHost } from "../host";
6
+ import { sleep } from "../../scripts/conformance-utils.mjs";
7
+ import { centerOf, parsePoint, resolve } from "../selectors";
8
+
9
+ type Host = LaunchedHost | AttachedHost;
10
+ type ControlResponse = { ok: boolean; error?: string; targetId?: number; focusedId?: number; activated?: boolean };
11
+
12
+ function shortId(node: DumpNode): string {
13
+ const a = node.accessibility ?? {};
14
+ return a.testID ?? a.identifier ?? a.nativeID ?? a.label ?? (node.text ? `"${node.text.slice(0, 24)}"` : `#${node.globalId}`);
15
+ }
16
+
17
+ export async function runDo(host: Host, sub: string, args: string[], json: boolean): Promise<number> {
18
+ if (!isDriveableHost(host)) {
19
+ console.error(" do commands need a driveable target: use --launch, --bundle, --session, or attach to an app with RNGPUI_CONTROL_SOCKET metadata");
20
+ return 1;
21
+ }
22
+ const driveable = host;
23
+
24
+ switch (sub) {
25
+ case "hud": {
26
+ const enabled = args[0] === "on" ? true : args[0] === "off" ? false : null;
27
+ if (enabled == null) {
28
+ console.error(" usage: rngpui do hud <on|off>");
29
+ return 1;
30
+ }
31
+ const response = await driveable.request<ControlResponse>({ $cmd: "performanceHud", enabled });
32
+ if (!response.ok) {
33
+ console.error(` performance HUD failed: ${response.error || "native command failed"}`);
34
+ return 1;
35
+ }
36
+ if (json) console.log(JSON.stringify({ performanceHud: enabled }));
37
+ else console.log(` performance HUD: ${enabled ? "on" : "off"}`);
38
+ return 0;
39
+ }
40
+
41
+ case "tap": {
42
+ const selector = args[0];
43
+ if (!selector) {
44
+ console.error(" usage: rngpui do tap <selector|x,y>");
45
+ return 1;
46
+ }
47
+ let x: number;
48
+ let y: number;
49
+ let label = selector;
50
+ const pt = parsePoint(selector);
51
+ if (pt) {
52
+ ({ x, y } = pt);
53
+ } else {
54
+ const dump = await driveable.dump();
55
+ const { best } = resolve(dump, selector);
56
+ if (!best) {
57
+ console.error(` no node matched "${selector}"`);
58
+ return 1;
59
+ }
60
+ const center = centerOf(best.node);
61
+ if (!center) {
62
+ console.error(` node ${shortId(best.node)} #${best.node.globalId} has no bounds — not tappable`);
63
+ return 1;
64
+ }
65
+ ({ x, y } = center);
66
+ label = `${shortId(best.node)} #${best.node.globalId}`;
67
+ }
68
+ const response = await driveable.request<ControlResponse>({ $cmd: "tap", x, y });
69
+ if (!response.ok) {
70
+ console.error(` tap failed: ${response.error || "no native target"}`);
71
+ return 1;
72
+ }
73
+ await sleep(150);
74
+ if (json) console.log(JSON.stringify({ tapped: label, x, y, targetId: response.targetId }));
75
+ else console.log(` tapped ${label} at ${x.toFixed(0)},${y.toFixed(0)}`);
76
+ return 0;
77
+ }
78
+
79
+ case "type": {
80
+ const text = args.join(" ");
81
+ if (!text) {
82
+ console.error(" usage: rngpui do type <text>");
83
+ return 1;
84
+ }
85
+ const response = await driveable.request<ControlResponse>({ $cmd: "type", text });
86
+ if (!response.ok) {
87
+ console.error(` type failed: ${response.error || "no focused input"}`);
88
+ return 1;
89
+ }
90
+ await sleep(120);
91
+ if (json) console.log(JSON.stringify({ typed: text, focusedId: response.focusedId }));
92
+ else console.log(` typed: ${JSON.stringify(text)}`);
93
+ return 0;
94
+ }
95
+
96
+ case "key": {
97
+ const key = args[0];
98
+ if (!key) {
99
+ console.error(" usage: rngpui do key <key> (e.g. enter, backspace, space, a)");
100
+ return 1;
101
+ }
102
+ const response = await driveable.request<ControlResponse>({ $cmd: "key", key });
103
+ if (!response.ok) {
104
+ console.error(` key failed: ${response.error || "no focused input"}`);
105
+ return 1;
106
+ }
107
+ await sleep(100);
108
+ if (json) console.log(JSON.stringify({ key, focusedId: response.focusedId }));
109
+ else console.log(` key: ${key}`);
110
+ return 0;
111
+ }
112
+
113
+ case "scroll": {
114
+ const target = args[0];
115
+ const delta = args[1];
116
+ if (!target || !delta) {
117
+ console.error(" usage: rngpui do scroll <selector|x,y> <dx,dy>");
118
+ return 1;
119
+ }
120
+ const d = parsePoint(delta);
121
+ if (!d) {
122
+ console.error(` invalid delta "${delta}" — expected dx,dy`);
123
+ return 1;
124
+ }
125
+ let x: number;
126
+ let y: number;
127
+ const pt = parsePoint(target);
128
+ if (pt) {
129
+ ({ x, y } = pt);
130
+ } else {
131
+ const dump = await driveable.dump();
132
+ const { best } = resolve(dump, target);
133
+ const center = best ? centerOf(best.node) : null;
134
+ if (!center) {
135
+ console.error(` no scrollable node matched "${target}"`);
136
+ return 1;
137
+ }
138
+ ({ x, y } = center);
139
+ }
140
+ const response = await driveable.request<ControlResponse>({ $cmd: "scrollAt", x, y, dx: d.x, dy: d.y });
141
+ if (!response.ok) {
142
+ console.error(` scroll failed: ${response.error || "no scroll container"}`);
143
+ return 1;
144
+ }
145
+ await sleep(120);
146
+ if (json) console.log(JSON.stringify({ scrolled: { x, y, dx: d.x, dy: d.y, targetId: response.targetId } }));
147
+ else console.log(` scrolled at ${x.toFixed(0)},${y.toFixed(0)} by ${d.x},${d.y}`);
148
+ return 0;
149
+ }
150
+
151
+ case "drag": {
152
+ const from = args[0];
153
+ const to = args[1];
154
+ const steps = Math.max(2, Math.min(120, Number(args[2] ?? 24) || 24));
155
+ if (!from || !to) {
156
+ console.error(" usage: rngpui do drag <selector|x,y> <selector|x,y> [steps]");
157
+ return 1;
158
+ }
159
+ const start = await resolveDrivePoint(driveable, from);
160
+ const end = await resolveDrivePoint(driveable, to);
161
+ if (!start || !end) return 1;
162
+ const startResponse = await driveable.request<ControlResponse>({
163
+ $cmd: "dragAt",
164
+ phase: "start",
165
+ x: start.x,
166
+ y: start.y,
167
+ });
168
+ if (!startResponse.ok) {
169
+ console.error(` drag start failed: ${startResponse.error || "no native target"}`);
170
+ return 1;
171
+ }
172
+ let lastTargetId = startResponse.targetId;
173
+ let activations = 0;
174
+ for (let i = 1; i <= steps; i++) {
175
+ const t = i / steps;
176
+ const x = start.x + (end.x - start.x) * t;
177
+ const y = start.y + (end.y - start.y) * t;
178
+ const response = await driveable.request<ControlResponse>({ $cmd: "dragAt", phase: "move", x, y });
179
+ if (!response.ok) {
180
+ await driveable.request<ControlResponse>({ $cmd: "dragAt", phase: "end", x, y });
181
+ console.error(` drag move failed: ${response.error || "no native target"}`);
182
+ return 1;
183
+ }
184
+ if (response.targetId !== lastTargetId && response.activated) activations += 1;
185
+ lastTargetId = response.targetId;
186
+ await sleep(16);
187
+ }
188
+ await driveable.request<ControlResponse>({ $cmd: "dragAt", phase: "end", x: end.x, y: end.y });
189
+ await sleep(150);
190
+ if (json) console.log(JSON.stringify({ dragged: { from: start, to: end, steps, activations } }));
191
+ else console.log(` dragged ${steps} steps from ${start.x.toFixed(0)},${start.y.toFixed(0)} to ${end.x.toFixed(0)},${end.y.toFixed(0)}`);
192
+ return 0;
193
+ }
194
+
195
+ default:
196
+ console.error(` unknown do subcommand: ${sub}`);
197
+ console.error(" available: hud, tap, type, key, scroll, drag");
198
+ return 1;
199
+ }
200
+ }
201
+
202
+ async function resolveDrivePoint(driveable: DriveableHost, selector: string): Promise<{ x: number; y: number } | null> {
203
+ const pt = parsePoint(selector);
204
+ if (pt) return pt;
205
+ const dump = await driveable.dump();
206
+ const { best } = resolve(dump, selector);
207
+ const center = best ? centerOf(best.node) : null;
208
+ if (!center) {
209
+ console.error(` no node matched "${selector}"`);
210
+ return null;
211
+ }
212
+ return center;
213
+ }