Files
Michał Pierzchała 630dc7c99b feat(examples): add runnable Node.js SDK examples under examples/sdk/ (#1469)
* feat(examples): add runnable Node.js SDK examples under examples/sdk/

examples/test-app is a fixture and the repo's only prior examples/
content; the real SDK usage patterns lived only in
website/docs/docs/client-api.md with no runnable script anywhere.

Adds four standalone, typechecked examples covering the minimum surface
from #1463: root client session (create -> open -> snapshot/tap ->
close with typed error handling), agent-device/metro
(normalizeBaseUrl/resolveRuntimeTransport), agent-device/contracts
(centerOfRect on a snapshot node), and agent-device/batch (runBatch for
a custom transport). Each imports the published `agent-device/...`
subpaths rather than relative src/ paths.

examples/sdk/tsconfig.json path-maps those subpaths to src/sdk/ so
`pnpm typecheck` (now also run against this tsconfig) checks the
examples in CI without a prior build, workspace link, or publish step.
Running an example for real still resolves `agent-device` as a
self-referencing package after `pnpm build`.

src/__tests__/client-api-examples-drift.test.ts guards the examples
against drifting from client-api.md's subpath API manifest in both
directions, picked up automatically by the existing unit-core vitest
project (no new script or workflow needed).

examples/README.md indexes the new examples and notes that test-app/
remains a fixture, not an example; it is not renamed or moved.

Refs #1463

* fix: address Fallow findings on the new SDK examples

Fallow flagged the four examples/sdk/*.ts files as unused files (not
reachable from any entry point) and three functions as high complexity.

- Register the examples as manual entry points in .fallowrc.json,
  matching how other standalone scripts (scripts/patch-xcuitest-runner-icon.ts,
  scripts/runner-request-count/run.ts) are already declared.
- Reduce complexity in client-session.ts and contracts-result.ts by
  extracting device-resolution/error-reporting and rect-assertion
  helpers out of main().
- Reduce complexity in the drift guard's parseSubpathManifest by
  splitting bullet-matching and backtick-name extraction into their
  own functions.

Verified: pnpm check:fallow --base <PR base sha> now reports no issues,
and pnpm check:tooling / pnpm test:unit stay green.

Refs #1463

* fix: compile client-api.md's actual code snippets, not just its symbol manifest

Addresses review feedback on #1463's drift guard: the existing guard only
parsed the doc's "Public subpath API" bullet manifest and compared imported
symbol names, so a fenced ```ts snippet could drift or stop compiling
without the guard noticing.

Added test/integration/client-api-doc-snippets.test.ts, which extracts every
fenced ```ts block from client-api.md and typechecks it against the real
agent-device/* sources (reusing examples/sdk/tsconfig.json's existing paths
mapping, read via `tsc --showConfig` so there's one source of truth). Free
identifiers that continue a `client`/`snapshot` from an earlier snippet are
stubbed — typed against the real SDK return type, not `any`, so continuation
snippets still get real checking. Lives in the Node integration lane
(test/integration/*.test.ts), not vitest's unit-core: it spawns a real tsc
Program, well past the unit suite's 2.5s budget.

Running this check against the existing doc surfaced real, pre-existing
snippet bugs (unrelated to the new examples), fixed here:
- "sessions.artifacts": `result.cloudArtifacts` accessed without narrowing
  the `CloudArtifactsResult | DaemonArtifactsResult` union first.
- "Device cloud sessions": `platform`/`device` were passed into the client
  constructor config, which doesn't accept them; moved to the `apps.open()`
  call where those fields actually belong.
- "Android ADB providers": the inline `exec` handler had no parameter types,
  so it failed under strict/noImplicitAny; annotated with the real
  `AndroidAdbExecutorOptions` type.

Two further gaps the check surfaced are pre-existing product/API-surface
questions out of scope for this PR (not the new examples), so they're
allowlisted in KNOWN_DOC_GAPS with comments rather than silently patched:
- "Remote Metro helpers" documents prepareRemoteMetro/reloadRemoteMetro/
  stopMetroTunnel/resolveRemoteConfigProfile as public, but none of them are
  exported from agent-device/metro or agent-device/remote-config today.
- "Web sessions"/audio probe pass `platform` to `observability.network()`/
  `.audio()`, but NetworkOptions/AudioOptions have no `platform` field even
  though the CLI's network/audio commands accept `--platform`.

Refs #1463

* fix: close the doc-snippet compiler's stubbing hole and the two suppressed gaps

Addresses the second round of review feedback on #1463's drift guard:

1. stubFreeNamesAndRecompile auto-stubbed every "Cannot find name" as `any`,
   so a typo like `cliet.apps.open()` would silently pass on the second
   compile. It now only stubs identifiers in an explicit allowlist
   (KNOWN_FREE_NAME_STUB_TYPES) — the real SDK-derived continuations
   (`client`, `androidClient`, `snapshot`) plus the doc's own invented
   host-glue names, each typed precisely rather than loosely. Anything else
   is left as a real compile failure. Added a regression test that feeds a
   `cliet` typo through the guard and asserts it fails.

2. KNOWN_DOC_GAPS filtered six real compiler errors out of the final
   assertion while the test claimed every snippet compiles. Investigated
   both and fixed the actual contracts instead of suppressing them:
   - `prepareMetroRuntime`/`reloadMetro` (src/metro/client-metro.ts) and
     `stopMetroTunnel` (src/metro/metro.ts) already existed and matched the
     doc's described workflow almost exactly (same result shape) but were
     never re-exported from `agent-device/metro`; same for
     `resolveRemoteConfigProfile` and `agent-device/remote-config`. Added
     the four exports and fixed the doc's stale function names
     (`prepareRemoteMetro`/`reloadRemoteMetro`) and one stale field name
     (`profileKey` -> `companionProfileKey` on the prepare call) to match.
   - `NetworkOptions`/`AudioOptions` (src/contracts/client-observability.ts)
     had no `platform` field even though the CLI's `network`/`audio`
     commands accept `--platform` for the same use case, and the client
     methods already forward the options object to the daemon generically
     (`executeCommand('network'|'audio', options)`) — so this was a type
     gap, not a runtime one. Switched both from AgentDeviceRequestOverrides
     to DeviceCommandBaseOptions (matching PerfOptions' existing pattern),
     closing the gap for real instead of stripping `platform` from the doc.
   - The "Android installFromSource()" snippet was missing its
     `createAgentDeviceClient` import outright; added it.
   KNOWN_DOC_GAPS is gone — every fenced snippet now compiles for real, and
   the test's assertion matches what it claims.

3. Switched the raw `execFileSync` calls to `runCmdSync` from
   src/utils/exec.ts, per AGENTS.md's process-execution invariant (this is
   a .ts integration test, not a packaging fixture that needs to stay
   dependency-free).

Refs #1463

* docs: fix stale reloadRemoteMetro() prose reference to reloadMetro()

The prose right after the Remote Metro helpers snippet still named the old
function; the compile guard only checks the fenced snippet, not surrounding
prose, so it didn't catch this leftover from the prior rename.

Refs #1463

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-28 21:36:07 +02:00

76 lines
2.3 KiB
TypeScript

/**
* Root client session: create a client, open an app, capture a snapshot, tap
* a node, then close the session — with typed error handling via the
* exported error helpers.
*
* Demonstrates: `createAgentDeviceClient`, `AppError`, `isAgentDeviceError`,
* and `normalizeAgentDeviceError` from the `agent-device` root export.
*
* Prerequisites: an `agent-device` daemon target (a booted iOS simulator).
* This file typechecks without one; running it for real also requires
* `pnpm build` first, so the package resolves at runtime.
*
* Run: node --experimental-strip-types examples/sdk/client-session.ts
*/
import {
AppError,
createAgentDeviceClient,
isAgentDeviceError,
normalizeAgentDeviceError,
} from 'agent-device';
async function resolveSnapshotCapableIosDevice(client: ReturnType<typeof createAgentDeviceClient>) {
const devices = await client.devices.list({ platform: 'ios' });
const device = devices[0];
if (!device) {
throw new AppError('DEVICE_NOT_FOUND', 'No iOS device available');
}
const capabilities = await client.devices.capabilities({ platform: 'ios' });
if (!capabilities.availableCommands.includes('snapshot')) {
throw new AppError('UNSUPPORTED_OPERATION', 'Selected target does not support snapshots');
}
return device;
}
function reportAgentDeviceError(error: unknown): void {
const normalized = normalizeAgentDeviceError(error);
console.error(`agent-device error [${normalized.code}]: ${normalized.message}`);
if (normalized.hint) {
console.error(`hint: ${normalized.hint}`);
}
process.exitCode = 1;
}
async function main(): Promise<void> {
const client = createAgentDeviceClient({
session: 'sdk-example',
lockPolicy: 'reject',
lockPlatform: 'ios',
});
try {
const device = await resolveSnapshotCapableIosDevice(client);
await client.apps.open({
app: 'com.apple.Preferences',
platform: 'ios',
udid: device.id,
});
const snapshot = await client.capture.snapshot({ interactiveOnly: true });
const target = snapshot.nodes.find((node) => node.role === 'button');
if (target) {
await client.interactions.press({ ref: target.ref });
}
} catch (error) {
if (!isAgentDeviceError(error)) throw error;
reportAgentDeviceError(error);
} finally {
await client.sessions.close();
}
}
await main();