@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.
- package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
- package/LICENSE +21 -0
- package/README.md +657 -2
- package/cli/bin.ts +379 -0
- package/cli/bundle.ts +32 -0
- package/cli/commands/do.ts +213 -0
- package/cli/commands/flow.ts +283 -0
- package/cli/commands/get.ts +584 -0
- package/cli/commands/shot.ts +312 -0
- package/cli/commands/trace.ts +293 -0
- package/cli/host.ts +714 -0
- package/cli/hotReload.ts +210 -0
- package/cli/run.ts +66 -0
- package/cli/selectors.ts +205 -0
- package/dist/Animated.d.ts +120 -0
- package/dist/ChatList.d.ts +28 -0
- package/dist/Dimensions.d.ts +31 -0
- package/dist/StyleSheet.d.ts +18 -0
- package/dist/Window.d.ts +33 -0
- package/dist/apis.d.ts +165 -0
- package/dist/app-state.d.ts +27 -0
- package/dist/chat-list-types.d.ts +136 -0
- package/dist/colors.d.ts +32 -0
- package/dist/commands.d.ts +230 -0
- package/dist/components.d.ts +419 -0
- package/dist/hotModules.d.ts +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.development.js +14876 -0
- package/dist/index.js +12 -0
- package/dist/keyboard.d.ts +140 -0
- package/dist/native-menu.d.ts +7 -0
- package/dist/platform-driver.d.ts +32 -0
- package/dist/platform-services.d.ts +35 -0
- package/dist/portal.d.ts +43 -0
- package/dist/raf.d.ts +4 -0
- package/dist/reanimated/colors-processor-shim.d.ts +10 -0
- package/dist/reanimated/reanimated-host.d.ts +10 -0
- package/dist/reanimated/seam.d.ts +34 -0
- package/dist/reanimated/ui-entry.d.ts +2 -0
- package/dist/reanimated/worklet-channel.d.ts +10 -0
- package/dist/reanimated/worklet-runtime.d.ts +466 -0
- package/dist/reanimated/worklets.d.ts +199 -0
- package/dist/reconciler.d.ts +102 -0
- package/dist/refresh.d.ts +1 -0
- package/dist/render.d.ts +37 -0
- package/dist/runtime.d.ts +235 -0
- package/dist/surfaces.d.ts +51 -0
- package/dist/types.d.ts +276 -0
- package/dist/ui-runtime.js +42276 -0
- package/dist/window-context.d.ts +7 -0
- package/dist/window-host.d.ts +41 -0
- package/dist/wire-delta.d.ts +20 -0
- package/package.json +176 -5
- package/scripts/bundle-app.mjs +88 -0
- package/scripts/conformance-utils.mjs +293 -0
- package/scripts/hot-update-alias-plugin.mjs +88 -0
- package/scripts/native-package-exports-plugin.mjs +235 -0
- package/scripts/pixel.mjs +152 -0
- package/scripts/png.mjs +198 -0
- package/scripts/react-refresh-swc.mjs +35 -0
- package/scripts/reanimated-bun-plugin.mjs +188 -0
- package/scripts/service-binary.d.mts +27 -0
- package/scripts/service-binary.mjs +100 -0
- package/src/reanimated/colors-processor-shim.ts +119 -0
- package/src/reanimated/reanimated-host.ts +69 -0
- package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
- package/src/reanimated/seam.ts +566 -0
- package/src/reanimated/ui-entry.ts +80 -0
- package/src/reanimated/worklet-channel.ts +64 -0
- package/src/reanimated/worklet-runtime.ts +2569 -0
- package/src/reanimated/worklets.ts +1334 -0
- 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
|
+
}
|