Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"packages/proxy/src/index.ts",
"packages/proxy/tsdown.config.ts",
"src/bin.ts",
"src/client/companion-tunnel.ts",
"packages/react-native/src/companion/companion-tunnel.ts",
"src/daemon.ts",
"packages/capture-kit/src/png-worker.ts",
"scripts/patch-xcuitest-runner-icon.ts",
Expand Down
5 changes: 4 additions & 1 deletion docs/adr/0019-request-bound-platform-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -1000,9 +1000,12 @@ anchor, not as a proposed commitment.
`packages/provider-limrun/src/`; these root files are the composition-time wiring, comparable in
role to `src/platform-runtime.ts` itself.
- `src/core` (R13-governed consumer seams), `src/commands`, `src/cli`, `src/cli-schema`,
`src/mcp`, `src/client`, `src/sdk`, `src/ai-sdk`, `src/remote`, `src/request`, `src/metro`.
`src/mcp`, `src/client`, `src/sdk`, `src/ai-sdk`, `src/remote`, `src/request`.
Not migration targets: these are command-surface, protocol-projection, and
daemon-owned zones this ADR keeps in `src/` by design, not residue awaiting a package.
- React Native development tooling lives in `@agent-device/react-native`: Metro preparation and
session bindings, companion tunnels, component inspection, and render profiling share one
package. CLI output and SDK session normalization remain application adapters in `src/`.

Neither number has an owning issue, gate, or accepted budget yet. Treat both as inputs to a
maintainer decision, not as a ratchet.
1 change: 1 addition & 0 deletions examples/sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
{ "path": "../../packages/provider-webdriver" },
{ "path": "../../packages/provision-kit" },
{ "path": "../../packages/proxy" },
{ "path": "../../packages/react-native" },
{ "path": "../../packages/replay-port" },
{ "path": "../../packages/replay-test" },
{ "path": "../../packages/selectors" },
Expand Down
4 changes: 2 additions & 2 deletions fallow-baselines/health.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"count": 1
}
},
"src/client/client-companion-tunnel-worker.ts": {
"packages/react-native/src/companion/client-companion-tunnel-worker.ts": {
"complexity_high": {
"count": 1
}
Expand Down Expand Up @@ -118,6 +118,6 @@
"packages/platform-android/src/ui-hierarchy.ts:high impact",
"packages/platform-android/src/sdk.ts:high impact",
"src/daemon-client/daemon-client-lifecycle.ts:complexity",
"src/client/client-companion-tunnel-worker.ts:complexity"
"packages/react-native/src/companion/client-companion-tunnel-worker.ts:complexity"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@
"@agent-device/provider-webdriver": "workspace:*",
"@agent-device/provision-kit": "workspace:*",
"@agent-device/proxy": "workspace:*",
"@agent-device/react-native": "workspace:*",
"@agent-device/replay-port": "workspace:*",
"@agent-device/replay-test": "workspace:*",
"@agent-device/selectors": "workspace:*",
Expand Down
10 changes: 2 additions & 8 deletions packages/contracts/src/companion-tunnel-scope.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
/**
* The identity a companion tunnel is scoped to.
*
* This shape is shared vocabulary rather than a client concern: the public API surface declares
* command options in terms of it, and `client/`, `metro/` and `cli/` all pass it around. It lived
* inside `client/client-companion-tunnel-contract.ts` next to the env-var names and worker
* options that really are client-local, which made every zone that merely needed the shape
* declare itself in terms of the client. Declaring it below them is what lets the contract be
* shared without the dependency.
* The tenant, run, and lease identity shared by public command options and the
* companion tunnel in `@agent-device/react-native`.
*/
export type CompanionTunnelScope = {
tenantId: string;
Expand Down
22 changes: 22 additions & 0 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@agent-device/react-native",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "React Native development runtime: Metro preparation, companion tunnelling, component inspection, and render profiling. Internal workspace package bundled into agent-device.",
"dependencies": {
"@agent-device/contracts": "workspace:*",
"@agent-device/daemon-contracts": "workspace:*",
"@agent-device/host-kit": "workspace:*",
"@agent-device/kernel": "workspace:*"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"devDependencies": {
"@agent-device/test-kit": "workspace:*"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Duplex } from 'node:stream';
import { listenOnLoopback } from '@agent-device/test-kit';

/**
* Wire-level doubles for the companion tunnel worker (`src/client/client-companion-tunnel-worker.ts`).
* Wire-level doubles for the companion tunnel worker (`packages/react-native/src/companion/client-companion-tunnel-worker.ts`).
*
* The worker speaks raw WebSocket frames over an `http.Server` upgrade, so these helpers encode and
* decode frames by hand instead of pulling in a WebSocket dependency, and a hand-written parser is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function spawnMetroCompanionWorker(options: {
process.execPath,
[
'--experimental-strip-types',
'src/client/companion-tunnel.ts',
'packages/react-native/src/companion/companion-tunnel.ts',
'--agent-device-run-metro-companion',
],
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
import type {
MetroTunnelRequestMessage as MetroCompanionRequest,
MetroTunnelResponseMessage,
} from '../metro/metro.ts';
import { normalizeBaseUrl } from './base-url.ts';
} from './metro-tunnel-messages.ts';
import { normalizeBaseUrl } from '../metro/base-url.ts';

const COMPANION_REGISTER_TIMEOUT_MS = 5_000;
const COMPANION_REGISTER_MAX_RETRY_DELAY_MS = 60_000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ENV_COMPANION_TUNNEL_STATE_PATH,
ENV_COMPANION_TUNNEL_UNREGISTER_PATH,
} from './client-companion-tunnel-contract.ts';
import { normalizeBaseUrl } from './base-url.ts';
import { normalizeBaseUrl } from '../metro/base-url.ts';
import { runCmdDetached } from '@agent-device/host-kit/command';
import {
confirmedProcessStopDetails,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '@agent-device/host-kit/process';

import { ensureMetroCompanion, stopMetroCompanion } from './client-metro-companion.ts';
import { METRO_COMPANION_RUN_ARG } from '../client/client-companion-tunnel-contract.ts';
import { METRO_COMPANION_RUN_ARG } from './client-companion-tunnel-contract.ts';

const TEST_BRIDGE_SCOPE = {
tenantId: 'tenant-1',
Expand All @@ -47,15 +47,17 @@ function assertCompanionSpawnTarget(): void {
assert.ok(firstCall, 'expected runCmdDetached to be called');
assert.equal(firstCall[0], process.execPath);
assert.ok(
firstCall[1].some((arg) => arg.includes('src/client/companion-tunnel.ts')),
firstCall[1].some((arg) =>
arg.includes('packages/react-native/src/companion/companion-tunnel.ts'),
),
`expected companion entry path in ${JSON.stringify(firstCall[1])}`,
);
assert.equal(firstCall[1].at(-1), '--agent-device-run-metro-companion');
}

/** The stops the code asked host-kit for: the identity recorded beside the pid, and the budget. */
const COMPANION_STOP_BUDGET = { termTimeoutMs: 1_000, killTimeoutMs: 1_000 };
const COMPANION_TUNNEL_COMMAND = `${process.execPath} src/client/companion-tunnel.ts ${METRO_COMPANION_RUN_ARG}`;
const COMPANION_TUNNEL_COMMAND = `${process.execPath} packages/react-native/src/companion/companion-tunnel.ts ${METRO_COMPANION_RUN_ARG}`;
const UNRELATED_COMMAND = '/usr/bin/nginx -g daemon off;';
// A predicate compares by identity, so deepEqual cannot see whether it would accept the tunnel or
// refuse a stranger. Projecting its verdicts is what makes a dropped or inverted matcher fail here.
Expand Down Expand Up @@ -88,7 +90,8 @@ test('companion ownership is profile-scoped and consumer-counted', async () => {
pid === 111 ? 'start-111' : 'start-222',
);
vi.mocked(readProcessCommand).mockImplementation(
() => `${process.execPath} src/client/companion-tunnel.ts --agent-device-run-metro-companion`,
() =>
`${process.execPath} packages/react-native/src/companion/companion-tunnel.ts --agent-device-run-metro-companion`,
);
vi.mocked(stopProcessAndConfirmExit).mockResolvedValue(STOPPED);

Expand Down Expand Up @@ -185,7 +188,8 @@ test('launchUrl changes force a companion respawn for the same profile', async (
pid === 333 ? 'start-333' : 'start-444',
);
vi.mocked(readProcessCommand).mockImplementation(
() => `${process.execPath} src/client/companion-tunnel.ts --agent-device-run-metro-companion`,
() =>
`${process.execPath} packages/react-native/src/companion/companion-tunnel.ts --agent-device-run-metro-companion`,
);
vi.mocked(stopProcessAndConfirmExit).mockResolvedValue(STOPPED);

Expand Down Expand Up @@ -231,7 +235,7 @@ test('spawned companion uses neutral env names', async () => {
vi.mocked(runCmdDetached).mockReturnValueOnce(999);
vi.mocked(readProcessStartTime).mockReturnValue('start-999');
vi.mocked(readProcessCommand).mockReturnValue(
`${process.execPath} src/client/companion-tunnel.ts --agent-device-run-metro-companion`,
`${process.execPath} packages/react-native/src/companion/companion-tunnel.ts --agent-device-run-metro-companion`,
);

await ensureMetroCompanion({
Expand Down Expand Up @@ -272,7 +276,7 @@ test('state sentinel exists before spawning companion worker', async () => {
});
vi.mocked(readProcessStartTime).mockReturnValue('start-1001');
vi.mocked(readProcessCommand).mockReturnValue(
`${process.execPath} src/client/companion-tunnel.ts --agent-device-run-metro-companion`,
`${process.execPath} packages/react-native/src/companion/companion-tunnel.ts --agent-device-run-metro-companion`,
);

const spawned = await ensureMetroCompanion({
Expand Down Expand Up @@ -518,7 +522,7 @@ test('legacy state without bridge scope is stopped before respawn', async () =>
`${JSON.stringify({
pid: 555,
startTime: 'start-555',
command: `${process.execPath} src/client/companion-tunnel.ts --agent-device-run-metro-companion`,
command: `${process.execPath} packages/react-native/src/companion/companion-tunnel.ts --agent-device-run-metro-companion`,
serverBaseUrl: 'https://bridge.example.test',
localBaseUrl: 'http://127.0.0.1:8081',
tokenHash: 'legacy-token-hash',
Expand All @@ -530,7 +534,7 @@ test('legacy state without bridge scope is stopped before respawn', async () =>
vi.mocked(isProcessAlive).mockReturnValue(true);
vi.mocked(readProcessStartTime).mockReturnValue('start-555');
vi.mocked(readProcessCommand).mockReturnValue(
`${process.execPath} src/client/companion-tunnel.ts --agent-device-run-metro-companion`,
`${process.execPath} packages/react-native/src/companion/companion-tunnel.ts --agent-device-run-metro-companion`,
);
vi.mocked(stopProcessAndConfirmExit).mockResolvedValue(STOPPED);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
type CompanionTunnelDefinition,
type EnsureCompanionTunnelOptions,
type StopCompanionTunnelOptions,
} from '../client/client-companion-tunnel.ts';
import { METRO_COMPANION_RUN_ARG } from '../client/client-companion-tunnel-contract.ts';
} from './client-companion-tunnel.ts';
import { METRO_COMPANION_RUN_ARG } from './client-companion-tunnel-contract.ts';

const METRO_COMPANION_REGISTER_PATH = '/api/metro/companion/register';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
import type { SessionRuntimeHints } from '@agent-device/kernel/contracts';
import { stopMetroCompanion } from './client-metro-companion.ts';
import { resolveRuntimeTransportHints } from '@agent-device/host-kit/runtime-transport-hints';

export type { MetroBridgeDescriptor } from './metro-types.ts';

export function resolveRuntimeTransport(
runtime: SessionRuntimeHints | undefined,
): { host: string; port: number; scheme: 'http' | 'https' } | undefined {
return resolveRuntimeTransportHints(runtime);
}

export type MetroTunnelPingMessage = {
type: 'ping';
timestamp: number;
Expand Down Expand Up @@ -86,13 +74,3 @@ export type MetroTunnelResponseMessage =
| MetroTunnelWebSocketOpenResultMessage
| MetroTunnelWebSocketFrameMessage
| MetroTunnelWebSocketCloseMessage;

export type StopMetroTunnelOptions = {
projectRoot: string;
profileKey?: string;
consumerKey?: string;
};

export async function stopMetroTunnel(options: StopMetroTunnelOptions): Promise<void> {
await stopMetroCompanion(options);
}
Loading
Loading