authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-15 00:47:06-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-15 00:47:06-07:00
log50ff4c371eab927a9650c114975241999f4cd7b1
tree79a1bff24196663f4de41a1a05026c0c44f63f80
parent6ecc15fa25c808922a58f07377add25b55d9ac90
signature Signed by PGP key B5690EEEBB952194

fix(mobile): ensure a compatible native client before verification (#11862)


6 files changed, 469 insertions(+), 18 deletions(-)

.agents/skills/ios-debugger-agent/SKILL.md+2
......@@ -31,6 +31,8 @@ Avoid generic Mac window automation for switching among Simulator windows. Expli
3131
3232## Choose build or launch
3333
34For T3 Code Mobile, run `node scripts/mobile-native-client.ts ensure ios <simulator-udid>` from the checkout on the simulator host first. It checks the local Expo native fingerprint against the installed client and builds/installs when stale, missing, or unknown. Then launch with the intended Metro bundle. Authorized verification includes native builds and installs; do not stop because the existing client is old. Use `check` instead of `ensure` only when the user explicitly prohibits rebuilding or requests a read-only check.
35
3436- Use `build_run_sim` when native source, native dependencies, entitlements, or project configuration changed.
3537- Use `test_sim` for the smallest relevant native test target or test cases; do not run an entire workspace test matrix routinely.
3638- Use `launch_app_sim` when a compatible app is already installed and no native rebuild is needed.
.agents/skills/test-t3-mobile/SKILL.md+17-15
......@@ -15,24 +15,28 @@ Inspect the host and the affected code before launching processes:
1515
1616- On macOS with Xcode, prefer one representative iOS Simulator when the change is cross-platform so the user can watch through serve-sim. Load and follow [`ios-debugger-agent`](../ios-debugger-agent/SKILL.md), and load [`ios-simulator-browser`](../ios-simulator-browser/SKILL.md) when live streaming is available.
1717- On macOS, Linux, or Windows with the Android SDK, use one Android Emulator when Android is the affected surface or iOS tooling is unavailable.
18- When the change is platform-specific, test that platform. When neither platform is viable, report the missing SDK, emulator, or dev-client prerequisite rather than claiming verification.
18- When the change is platform-specific, test that platform. When neither platform is viable, report the missing SDK or emulator prerequisite rather than claiming verification. A missing development client is a build step, not a blocker.
1919
2020Do not treat unavailable iOS tooling as a blocker when Android is a valid representative target.
2121
22## Choose the lightest valid launch path
22## Ensure a compatible native client
2323
24- For JavaScript, TypeScript, or asset-only changes, reuse a compatible installed development client and start Metro. Do not rebuild native code merely to load a new bundle.
25- For native source, native dependencies, entitlements, config plugins, or generated project changes, rebuild the affected platform.
26- Use `vp run ios:dev` or `vp run android:dev` only when an Expo clean prebuild is actually required; both commands regenerate the native project.
27- If the user requested no native rebuild and no compatible app is installed, reuse an existing compatible `.app` or `.apk` artifact when available. Otherwise report the missing dev client instead of silently rebuilding.
24Authorized mobile verification includes building and installing a development client. A missing, stale, or unknown native client is not a reason to skip verification or leave a PR in draft. Build and install it, then continue. Respect an explicit user instruction not to rebuild; otherwise do not ask for separate permission.
2825
29The development identity on both platforms is:
26Run this from the checkout being tested, on the machine that hosts the selected simulator or emulator. Select and boot one explicit iOS UDID or Android emulator serial first:
3027
31- App: `T3 Code Dev`
32- Bundle/package identifier: `com.t3tools.t3code.dev`
33- URL scheme: `t3code-dev`
28```bash
29node scripts/mobile-native-client.ts ensure ios <simulator-udid>
30node scripts/mobile-native-client.ts ensure android <emulator-serial>
31```
32
33`ensure` compares the checkout's local Expo development fingerprint and the installed app's binary contents against the last successful build record. It reuses a matching client; otherwise it runs a clean prebuild, builds and installs the development app, and records the successful result. It does not start Metro. Start Metro below after it succeeds. On hosts with an `agent-job` requirement, run the entire `ensure` command through that queue.
34
35For a read-only decision, use `check` in place of `ensure`. Exit 0 means compatible, 2 means build required, and 1 means an operational error. An app installed outside this helper is initially unknown and gets rebuilt once. Records are local to the simulator host under `~/.cache/t3code/native-clients` and work across checkouts. Do not copy records between machines or write them manually.
36
37A JavaScript-only diff, bundle identifier, app version, or recent install date does not prove native compatibility. Always check the whole checkout. Expo fingerprints are computed locally with `APP_VARIANT=development`; no EAS credentials or cloud build are required. Generated `ios/` and `android/` directories are excluded by `.fingerprintignore`, so edit native source modules or config plugins rather than generated output.
3438
35Bundle or package presence proves the correct variant, not native compatibility. Reuse it only when the current changes did not alter its Expo SDK, native dependencies, config plugins, entitlements, generated project, or native source.
39The development identity is `T3 Code Dev`, bundle/package `com.t3tools.t3code.dev`, scheme `t3code-dev`. If a build fails, investigate the build error and fix the local prerequisites. Report the concrete failure if it cannot be resolved, not “no compatible client.”
3640
3741## Start one disposable T3 environment
3842
......@@ -98,10 +102,9 @@ Use `ios-debugger-agent` to select one UDID and set these XcodeBuildMCP session
98102- Simulator ID: the selected UDID
99103- Bundle ID: `com.t3tools.t3code.dev`
100104
101Check the installed client with:
105After `ensure` succeeds, open the Metro URL:
102106
103107```bash
104xcrun simctl get_app_container <simulator-udid> com.t3tools.t3code.dev app
105108xcrun simctl openurl <simulator-udid> <printed-dev-client-url>
106109```
107110
......@@ -109,10 +112,9 @@ Accept the iOS confirmation prompt and dismiss the developer menu when it obscur
109112
110113### Android launch
111114
112Select one running emulator serial from `adb devices` and check the installed client:
115Use the emulator serial already checked by `ensure`:
113116
114117```bash
115adb -s <emulator-serial> shell pm path com.t3tools.t3code.dev
116118adb -s <emulator-serial> reverse tcp:<metro-port> tcp:<metro-port>
117119adb -s <emulator-serial> shell am start -W \
118120 -a android.intent.action.VIEW \
AGENTS.md+2
......@@ -110,6 +110,8 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
110110- The server is event-sourced and its async flows emit typed receipts. Wait on receipts and worker drains, never on sleeps or polling. A test that needs a timeout to pass is wrong.
111111- Upon request, user-visible frontend changes should get one integrated pass in a real client: `test-t3-app` for web, `test-t3-mobile` for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers.
112112
113For authorized mobile verification, a missing or outdated native client is a build step, not a blocker. Run `node scripts/mobile-native-client.ts ensure <ios|android> <device-id>` on the simulator host before starting Metro. It checks the local Expo fingerprint and builds/installs when needed. See `test-t3-mobile` for the full workflow.
114
113115## Pull requests
114116
115117- Never make a PR unless the developer explicitly asks you to do so.
apps/mobile/README.md+16-1
......@@ -22,7 +22,22 @@ repository-root `.env` or `.env.local`, not an `apps/mobile/.env` file. See
2222
2323## Development
2424
25Start Metro for the dev client:
25For simulator/emulator development, select and boot a device, then ensure its native client matches
26this checkout before starting Metro:
27
28```bash
29node ../../scripts/mobile-native-client.ts ensure ios <simulator-udid>
30# Or: node ../../scripts/mobile-native-client.ts ensure android <emulator-serial>
31vp run dev:client
32```
33
34The helper compares a local Expo fingerprint and the installed binary with its last successful
35build record. It builds and installs missing, stale, or unverified clients and reuses matching ones.
36Use `check` instead of `ensure` for a read-only decision: exit 0 means compatible, 2 means a build is
37needed, and 1 means an operational error. Run it on the simulator host; no EAS login is required.
38An externally installed client is unverified until the helper builds it once.
39
40Start Metro for an already verified dev client:
2641
2742```bash
2843vp run dev:client
knip.jsonc+2-2
......@@ -13,8 +13,8 @@
1313 "vitest": { "entry": [".github/**/*.test.cjs"] },
1414 },
1515 "scripts": {
16 // Knip loads its preprocessor through a CLI option, not a source import.
17 "entry": ["knip-schemas.ts"],
16 // Knip loads its preprocessor through a CLI option; native verification runs directly.
17 "entry": ["knip-schemas.ts", "mobile-native-client.ts"],
1818 },
1919 "apps/server": {
2020 // Vite+ pack entries and the launcher used by installed background services.
scripts/mobile-native-client.ts created+430
......@@ -0,0 +1,430 @@
1import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import {
4 HostProcessEnvironment,
5 HostProcessExecutablePath,
6 HostProcessPlatform,
7} from "@t3tools/shared/hostProcess";
8import { isCommandAvailable, resolveSpawnCommand } from "@t3tools/shared/shell";
9import * as Console from "effect/Console";
10import * as Crypto from "effect/Crypto";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Path from "effect/Path";
14import * as Schema from "effect/Schema";
15import * as Stream from "effect/Stream";
16import { Argument, Command } from "effect/unstable/cli";
17import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
18
19export type NativePlatform = "ios" | "android";
20const NativeClientRecord = Schema.Struct({ fingerprint: Schema.String, binary: Schema.String });
21const encodeRecord = Schema.encodeEffect(Schema.fromJsonString(NativeClientRecord));
22const decodeRecord = Schema.decodeUnknownEffect(Schema.fromJsonString(NativeClientRecord));
23const encodeOutput = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
24const isNotLink = Schema.is(Schema.Struct({ code: Schema.Literal("EINVAL") }));
25const decodeSimulators = Schema.decodeUnknownEffect(
26 Schema.fromJsonString(
27 Schema.Struct({
28 devices: Schema.Record(
29 Schema.String,
30 Schema.Array(Schema.Struct({ udid: Schema.String, state: Schema.String })),
31 ),
32 }),
33 ),
34);
35export type NativeClientRecord = typeof NativeClientRecord.Type;
36export type NativeClientStatus = "compatible" | "missing" | "unknown" | "stale";
37export class NativeClientError extends Schema.TaggedError<NativeClientError>()(
38 "NativeClientError",
39 { message: Schema.String },
40) {}
41
42export function clientStatus(
43 fingerprint: string,
44 binary: string | null,
45 record: NativeClientRecord | null,
46): NativeClientStatus {
47 if (binary === null) return "missing";
48 if (!record || record.binary !== binary) return "unknown";
49 return record.fingerprint === fingerprint ? "compatible" : "stale";
50}
51
52/** Keep native sources stable during ensure, as with a normal build; endpoint checks reject detected edits. */
53export const ensureClient = Effect.fn("ensureClient")(function* <E, R, E2, R2>(operations: {
54 fingerprint: Effect.Effect<string, E, R>;
55 installedBinary: Effect.Effect<string | null, E, R>;
56 readRecord: Effect.Effect<NativeClientRecord | null, E, R>;
57 build: Effect.Effect<void, E, R>;
58 saveRecord: (record: NativeClientRecord) => Effect.Effect<void, E2, R2>;
59}) {
60 const fingerprint = yield* operations.fingerprint;
61 const status = clientStatus(
62 fingerprint,
63 yield* operations.installedBinary,
64 yield* operations.readRecord,
65 );
66 const verifyInputs = Effect.gen(function* () {
67 if ((yield* operations.fingerprint) !== fingerprint) {
68 return yield* new NativeClientError({
69 message:
70 "Native inputs changed during verification. Run ensure again; this build was not recorded.",
71 });
72 }
73 });
74 yield* verifyInputs;
75 if (status === "compatible") {
76 return { status, rebuilt: false, fingerprint };
77 }
78 yield* operations.build;
79 const binary = yield* operations.installedBinary;
80 if (binary === null)
81 return yield* new NativeClientError({
82 message: "Build finished but the development client is not installed.",
83 });
84 yield* verifyInputs;
85 yield* operations.saveRecord({ fingerprint, binary });
86 return { status: "compatible" as const, rebuilt: true, fingerprint };
87});
88
89const digest = Effect.fn("nativeClient.digest")(function* (value: string | Uint8Array) {
90 const crypto = yield* Crypto.Crypto;
91 const bytes = yield* crypto.digest(
92 "SHA-256",
93 typeof value === "string" ? new TextEncoder().encode(value) : value,
94 );
95 return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
96});
97
98/** Hash fixed-size chunks to bound memory, including resources and symlink targets. */
99export const hashBundle = Effect.fn("hashBundle")(function* (root: string) {
100 const fs = yield* FileSystem.FileSystem;
101 const path = yield* Path.Path;
102 const entries: string[] = [];
103 const visit = (
104 relative: string,
105 ): Effect.Effect<void, FileSystemError, FileSystem.FileSystem | Crypto.Crypto> =>
106 Effect.gen(function* () {
107 const names = (yield* fs.readDirectory(path.join(root, relative))).sort((a, b) =>
108 a.localeCompare(b, "en"),
109 );
110 for (const name of names) {
111 const key = path.join(relative, name);
112 const absolute = path.join(root, key);
113 // FileSystem.stat follows links; readLink distinguishes them without following a cycle.
114 const link = yield* fs.readLink(absolute).pipe(
115 Effect.catchIf(
116 (error) => isNotLink(error.reason.cause),
117 () => Effect.succeed(null),
118 ),
119 );
120 if (link !== null) {
121 entries.push(`link:${key}:${link}`);
122 continue;
123 }
124 const info = yield* fs.stat(absolute);
125 if (info.type === "Directory") {
126 entries.push(`directory:${key}`);
127 yield* visit(key);
128 } else {
129 const chunks = yield* fs.stream(absolute, { chunkSize: FileSystem.Size(65536) }).pipe(
130 Stream.mapEffect((chunk) => digest(chunk)),
131 Stream.runCollect,
132 );
133 entries.push(`file:${key}:${yield* digest(chunks.join("\n"))}`);
134 }
135 }
136 });
137 yield* visit("");
138 return yield* digest(entries.map((entry) => `${entry.length}:${entry}`).join(""));
139});
140type FileSystemError = import("effect/PlatformError").PlatformError;
141
142const bundleId = "com.t3tools.t3code.dev";
143const roots = Effect.gen(function* () {
144 const path = yield* Path.Path;
145 const repo = yield* path.fromFileUrl(new URL("../", import.meta.url));
146 return { repo, mobile: path.join(repo, "apps/mobile") };
147});
148const collect = <E>(stream: Stream.Stream<Uint8Array, E>) =>
149 stream.pipe(
150 Stream.decodeText(),
151 Stream.runFold(
152 () => "",
153 (a, b) => a + b,
154 ),
155 );
156export const resolveAdb = Effect.gen(function* () {
157 if (yield* isCommandAvailable("adb")) return "adb";
158 const environment = yield* HostProcessEnvironment;
159 const path = yield* Path.Path;
160 const executable = (yield* HostProcessPlatform) === "win32" ? "adb.exe" : "adb";
161 for (const sdk of [environment.ANDROID_SDK_ROOT, environment.ANDROID_HOME]) {
162 if (!sdk) continue;
163 const candidate = path.join(sdk, "platform-tools", executable);
164 if (yield* isCommandAvailable(candidate)) return candidate;
165 }
166 return yield* new NativeClientError({
167 message:
168 "adb was not found on PATH or in ANDROID_SDK_ROOT/ANDROID_HOME. Install Android SDK platform-tools.",
169 });
170});
171
172const command = Effect.fn("nativeClient.command")(function* (
173 program: string,
174 args: string[],
175 inherit = false,
176 cwd?: string,
177) {
178 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
179 const environment = yield* HostProcessEnvironment;
180 const spawn = yield* resolveSpawnCommand(program === "adb" ? yield* resolveAdb : program, args);
181 const child = yield* spawner.spawn(
182 ChildProcess.make(spawn.command, spawn.args, {
183 shell: spawn.shell,
184 cwd: cwd ?? (yield* roots).mobile,
185 env: {
186 ...environment,
187 APP_VARIANT: "development",
188 MOBILE_VERSION_POLICY: "appVersion",
189 T3CODE_IOS_PERSONAL_TEAM: "0",
190 CI: "1",
191 EXPO_NO_GIT_STATUS: "1",
192 },
193 stdin: "ignore",
194 stdout: inherit ? "inherit" : "pipe",
195 stderr: inherit ? "inherit" : "pipe",
196 }),
197 );
198 const [stdout, stderr, code] = yield* Effect.all(
199 [collect(child.stdout), collect(child.stderr), child.exitCode],
200 { concurrency: "unbounded" },
201 );
202 if (code !== 0)
203 return yield* new NativeClientError({
204 message: `${program} ${args[0]} failed (${code}): ${stderr || "see build output"}`,
205 });
206 return stdout.trim();
207}, Effect.scoped);
208
209const fingerprint = Effect.fn("nativeClient.fingerprint")(function* (platform: NativePlatform) {
210 const output = yield* command(yield* HostProcessExecutablePath, [
211 "--eval",
212 `require('expo/fingerprint').createFingerprintAsync(process.cwd(), { platforms: [process.argv[1]], silent: true }).then(fp => console.log('T3_NATIVE_FINGERPRINT=' + fp.hash)).catch(e => { console.error(e); process.exitCode = 1; });`,
213 platform,
214 ]);
215 const hash = output
216 .split("\n")
217 .find((line) => line.startsWith("T3_NATIVE_FINGERPRINT="))
218 ?.split("=")[1];
219 if (!hash || !/^[a-f0-9]{40,64}$/.test(hash))
220 return yield* new NativeClientError({ message: "Expo did not return a native fingerprint." });
221 return hash;
222});
223
224const validateDevice = Effect.fn("nativeClient.validateDevice")(function* (
225 platform: NativePlatform,
226 device: string,
227) {
228 if (platform === "ios") {
229 if ((yield* HostProcessPlatform) !== "darwin")
230 return yield* new NativeClientError({
231 message: "Run iOS check/ensure on the Mac that hosts the simulator.",
232 });
233 const listing = yield* decodeSimulators(
234 yield* command("xcrun", ["simctl", "list", "devices", "available", "--json"]),
235 );
236 const simulator = Object.values(listing.devices)
237 .flat()
238 .find((entry) => entry.udid === device);
239 if (!simulator)
240 return yield* new NativeClientError({
241 message: `No available iOS simulator with UDID ${device}.`,
242 });
243 if (simulator.state !== "Booted")
244 return yield* new NativeClientError({
245 message: `Boot the selected simulator first: xcrun simctl boot ${device}`,
246 });
247 } else {
248 if ((yield* command("adb", ["-s", device, "get-state"])) !== "device")
249 return yield* new NativeClientError({ message: "Android device is not connected." });
250 if ((yield* command("adb", ["-s", device, "shell", "getprop", "ro.kernel.qemu"])) !== "1")
251 return yield* new NativeClientError({
252 message: "Select an Android emulator, not a physical device.",
253 });
254 }
255});
256
257export const installedBinary = Effect.fn("installedBinary")(function* (
258 platform: NativePlatform,
259 device: string,
260 run: typeof command = command,
261) {
262 if (platform === "ios") {
263 const apps = yield* run("xcrun", ["simctl", "listapps", device]);
264 if (!apps.includes(`"${bundleId}"`)) return null;
265 return yield* hashBundle(
266 yield* run("xcrun", ["simctl", "get_app_container", device, bundleId, "app"]),
267 );
268 }
269 const installed = yield* run("adb", ["-s", device, "shell", "pm", "list", "packages", bundleId]);
270 if (!installed.split("\n").some((line) => line.trim() === `package:${bundleId}`)) return null;
271 const packages = yield* run("adb", ["-s", device, "shell", "pm", "path", bundleId]);
272 const apks = packages
273 .split("\n")
274 .filter((line) => line.startsWith("package:"))
275 .map((line) => line.slice(8).trim())
276 .sort();
277 if (apks.length === 0) return null;
278 const hashes = yield* Effect.forEach(apks, (apk) =>
279 Effect.gen(function* () {
280 if (!/^\/[\w/+=.~-]+\.apk$/.test(apk))
281 return yield* new NativeClientError({ message: "Unexpected installed APK path." });
282 const hash = (yield* run("adb", ["-s", device, "shell", "sha256sum", apk])).split(/\s/)[0];
283 if (!hash || !/^[a-f0-9]{64}$/.test(hash))
284 return yield* new NativeClientError({ message: "Could not hash installed APK." });
285 return hash;
286 }),
287 );
288 return yield* digest(hashes.sort().join("\n"));
289});
290
291const main = Command.make(
292 "mobile-native-client",
293 {
294 mode: Argument.choice("mode", ["check", "ensure"]),
295 platform: Argument.choice("platform", ["ios", "android"]),
296 device: Argument.string("device"),
297 },
298 Effect.fn("nativeClient.main")(function* ({ mode, platform, device }) {
299 yield* validateDevice(platform, device);
300 const fs = yield* FileSystem.FileSystem;
301 const path = yield* Path.Path;
302 const environment = yield* HostProcessEnvironment;
303 const home = environment.HOME ?? environment.USERPROFILE;
304 if (!home)
305 return yield* new NativeClientError({
306 message: "HOME or USERPROFILE must be set to store native client records.",
307 });
308 const recordPath = path.join(
309 home,
310 ".cache/t3code/native-clients",
311 platform,
312 `${yield* digest(device)}.json`,
313 );
314 const operations = {
315 fingerprint: fingerprint(platform),
316 installedBinary: installedBinary(platform, device),
317 readRecord: fs.readFileString(recordPath).pipe(
318 Effect.flatMap(decodeRecord),
319 Effect.catchTag("SchemaError", () => Effect.succeed(null)),
320 Effect.catchIf(
321 (error) => error.reason._tag === "NotFound",
322 () => Effect.succeed(null),
323 ),
324 ),
325 build: Effect.gen(function* () {
326 yield* Console.error(
327 "Native client is missing, stale, or unverified. Building and installing a development client...",
328 );
329 const tracked = yield* command(
330 "git",
331 ["ls-files", `apps/mobile/${platform}`],
332 false,
333 (yield* roots).repo,
334 );
335 if (tracked)
336 return yield* new NativeClientError({
337 message:
338 "Native directory contains tracked files; clean prebuild would overwrite them.",
339 });
340 yield* command(
341 "vp",
342 ["exec", "expo", "prebuild", "--clean", "--platform", platform, "--no-install"],
343 true,
344 );
345 if (platform === "ios") {
346 const output = yield* fs.makeTempDirectoryScoped({ prefix: "t3-native-client-" });
347 const { mobile } = yield* roots;
348 yield* command("pod", ["install"], true, path.join(mobile, "ios"));
349 // Target this simulator only, without Expo's desktop activation or log streaming.
350 yield* command(
351 "xcrun",
352 [
353 "xcodebuild",
354 "-workspace",
355 path.join(mobile, "ios/T3CodeDev.xcworkspace"),
356 "-scheme",
357 "T3CodeDev",
358 "-configuration",
359 "Debug",
360 "-destination",
361 `id=${device}`,
362 "-derivedDataPath",
363 output,
364 "build",
365 ],
366 true,
367 );
368 yield* command(
369 "xcrun",
370 [
371 "simctl",
372 "install",
373 device,
374 path.join(output, "Build/Products/Debug-iphonesimulator/T3CodeDev.app"),
375 ],
376 true,
377 );
378 } else {
379 yield* command(
380 "vp",
381 [
382 "exec",
383 "expo",
384 "run:android",
385 "--device",
386 device,
387 "--no-bundler",
388 "--variant",
389 "debug",
390 ],
391 true,
392 );
393 }
394 }).pipe(Effect.scoped),
395 saveRecord: Effect.fn(function* (record: NativeClientRecord) {
396 yield* fs.makeDirectory(path.dirname(recordPath), { recursive: true });
397 yield* fs.writeFileString(recordPath, yield* encodeRecord(record));
398 }),
399 };
400 if (mode === "ensure") {
401 yield* Console.log(yield* encodeOutput(yield* ensureClient(operations)));
402 } else {
403 const current = yield* operations.fingerprint;
404 const status = clientStatus(
405 current,
406 yield* operations.installedBinary,
407 yield* operations.readRecord,
408 );
409 yield* Console.log(
410 yield* encodeOutput({
411 status,
412 fingerprint: current,
413 next:
414 status === "compatible"
415 ? "Start Metro with vp run dev:client"
416 : `node scripts/mobile-native-client.ts ensure ${platform} ${device}`,
417 }),
418 );
419 process.exitCode = status === "compatible" ? 0 : 2;
420 }
421 }),
422);
423
424if (import.meta.main) {
425 Command.run(main, { version: "0.0.0" }).pipe(
426 Effect.scoped,
427 Effect.provide(NodeServices.layer),
428 NodeRuntime.runMain,
429 );
430}