mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
feat: export batch orchestration helpers (#472)
This commit is contained in:
committed by
GitHub
parent
a9e8d902dd
commit
6a2bf9f626
@@ -33,6 +33,10 @@
|
||||
"import": "./dist/src/metro.js",
|
||||
"types": "./dist/src/metro.d.ts"
|
||||
},
|
||||
"./batch": {
|
||||
"import": "./dist/src/batch.js",
|
||||
"types": "./dist/src/batch.d.ts"
|
||||
},
|
||||
"./remote-config": {
|
||||
"import": "./dist/src/remote-config.js",
|
||||
"types": "./dist/src/remote-config.d.ts"
|
||||
|
||||
@@ -19,6 +19,7 @@ export default defineConfig({
|
||||
index: 'src/index.ts',
|
||||
io: 'src/io.ts',
|
||||
artifacts: 'src/artifacts.ts',
|
||||
batch: 'src/batch.ts',
|
||||
metro: 'src/metro.ts',
|
||||
'remote-config': 'src/remote-config.ts',
|
||||
'install-source': 'src/install-source.ts',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { test } from 'vitest';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
BATCH_BLOCKED_COMMANDS,
|
||||
DEFAULT_BATCH_MAX_STEPS,
|
||||
INHERITED_PARENT_FLAG_KEYS,
|
||||
buildBatchStepFlags,
|
||||
runBatch,
|
||||
validateAndNormalizeBatchSteps,
|
||||
type BatchStepResult,
|
||||
} from '../batch.ts';
|
||||
import type { DaemonRequest } from '../contracts.ts';
|
||||
|
||||
test('public batch entrypoint exports daemon-compatible orchestration helpers', async () => {
|
||||
const seenCommands: string[] = [];
|
||||
const req: Omit<DaemonRequest, 'token'> = {
|
||||
command: 'batch',
|
||||
positionals: [],
|
||||
flags: {
|
||||
platform: 'ios',
|
||||
udid: 'sim-1',
|
||||
batchSteps: [
|
||||
{ command: 'open', positionals: ['settings'] },
|
||||
{ command: 'wait', positionals: ['100'], flags: { platform: 'android' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const response = await runBatch(req, 'resolved-session', async (stepReq) => {
|
||||
seenCommands.push(stepReq.command);
|
||||
assert.equal(stepReq.session, 'resolved-session');
|
||||
assert.equal(stepReq.flags?.session, 'resolved-session');
|
||||
if (stepReq.command === 'open') {
|
||||
assert.equal(stepReq.flags?.platform, 'ios');
|
||||
assert.equal(stepReq.flags?.udid, 'sim-1');
|
||||
}
|
||||
if (stepReq.command === 'wait') {
|
||||
assert.equal(stepReq.flags?.platform, 'android');
|
||||
}
|
||||
return { ok: true, data: { command: stepReq.command } };
|
||||
});
|
||||
|
||||
assert.equal(response.ok, true);
|
||||
assert.deepEqual(seenCommands, ['open', 'wait']);
|
||||
if (response.ok) {
|
||||
assert.equal(response.data?.total, 2);
|
||||
const results = response.data?.results as BatchStepResult[];
|
||||
assert.equal(results[0]?.command, 'open');
|
||||
}
|
||||
});
|
||||
|
||||
test('public batch helpers expose validation and flag policy', () => {
|
||||
assert.equal(DEFAULT_BATCH_MAX_STEPS, 100);
|
||||
assert.equal(BATCH_BLOCKED_COMMANDS.has('replay'), true);
|
||||
assert.equal(INHERITED_PARENT_FLAG_KEYS.includes('udid'), true);
|
||||
assert.deepEqual(validateAndNormalizeBatchSteps([{ command: 'WAIT', positionals: ['100'] }], 1), [
|
||||
{ command: 'wait', positionals: ['100'], flags: {}, runtime: undefined },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
buildBatchStepFlags(
|
||||
{ platform: 'ios', udid: 'sim-1', batchSteps: [{ command: 'open' }] },
|
||||
{ batchMaxSteps: 10, platform: 'android' },
|
||||
),
|
||||
{ platform: 'android', udid: 'sim-1' },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
BATCH_BLOCKED_COMMANDS,
|
||||
DEFAULT_BATCH_MAX_STEPS,
|
||||
INHERITED_PARENT_FLAG_KEYS,
|
||||
buildBatchStepFlags,
|
||||
runBatch,
|
||||
validateAndNormalizeBatchSteps,
|
||||
} from './core/batch.ts';
|
||||
|
||||
export type {
|
||||
BatchFlags,
|
||||
BatchInvoke,
|
||||
BatchRequest,
|
||||
BatchStep,
|
||||
BatchStepResult,
|
||||
NormalizedBatchStep,
|
||||
} from './core/batch.ts';
|
||||
+1
-2
@@ -5,8 +5,7 @@ import { readVersion } from './utils/version.ts';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { sendToDaemon } from './daemon-client.ts';
|
||||
import fs from 'node:fs';
|
||||
import type { BatchStep } from './core/dispatch.ts';
|
||||
import { parseBatchStepsJson } from './core/batch.ts';
|
||||
import { parseBatchStepsJson, type BatchStep } from './core/batch.ts';
|
||||
import {
|
||||
createAgentDeviceClient,
|
||||
type AgentDeviceClientConfig,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type RemoteConnectionState,
|
||||
} from '../../remote-connection-state.ts';
|
||||
import { REMOTE_CONFIG_FIELD_SPECS, type RemoteConfigProfile } from '../../remote-config-schema.ts';
|
||||
import type { BatchStep } from '../../core/dispatch.ts';
|
||||
import type { BatchStep } from '../../core/batch.ts';
|
||||
import { AppError } from '../../utils/errors.ts';
|
||||
import type { LeaseBackend, SessionRuntimeHints } from '../../contracts.ts';
|
||||
import type { CliFlags } from '../../utils/command-schema.ts';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { test } from 'vitest';
|
||||
import assert from 'node:assert/strict';
|
||||
import { validateAndNormalizeBatchSteps } from '../batch.ts';
|
||||
import type { BatchStep } from '../dispatch.ts';
|
||||
import { validateAndNormalizeBatchSteps, type BatchStep } from '../batch.ts';
|
||||
|
||||
test('validateAndNormalizeBatchSteps rejects unknown top-level step fields', () => {
|
||||
assert.throws(
|
||||
|
||||
+193
-7
@@ -1,14 +1,42 @@
|
||||
import { AppError } from '../utils/errors.ts';
|
||||
import type { BatchStep, CommandFlags } from './dispatch.ts';
|
||||
import type { DaemonRequest, DaemonResponse } from '../contracts.ts';
|
||||
import { AppError, asAppError } from '../utils/errors.ts';
|
||||
|
||||
export const DEFAULT_BATCH_MAX_STEPS = 100;
|
||||
const BATCH_BLOCKED_COMMANDS = new Set(['batch', 'replay']);
|
||||
export const BATCH_BLOCKED_COMMANDS: ReadonlySet<string> = new Set(['batch', 'replay']);
|
||||
const BATCH_ALLOWED_STEP_KEYS = new Set(['command', 'positionals', 'flags', 'runtime']);
|
||||
export const INHERITED_PARENT_FLAG_KEYS = [
|
||||
'platform',
|
||||
'target',
|
||||
'device',
|
||||
'udid',
|
||||
'serial',
|
||||
'verbose',
|
||||
'out',
|
||||
] as const;
|
||||
|
||||
export type BatchStep = {
|
||||
command: string;
|
||||
positionals?: string[];
|
||||
flags?: Record<string, unknown>;
|
||||
runtime?: unknown;
|
||||
};
|
||||
|
||||
export type BatchFlags = Record<string, unknown> & {
|
||||
batchOnError?: 'stop';
|
||||
batchMaxSteps?: number;
|
||||
batchSteps?: BatchStep[];
|
||||
};
|
||||
|
||||
export type BatchRequest = Omit<DaemonRequest, 'flags'> & {
|
||||
flags?: BatchFlags | Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type BatchInvoke = (req: BatchRequest) => Promise<DaemonResponse>;
|
||||
|
||||
export type NormalizedBatchStep = {
|
||||
command: string;
|
||||
positionals: string[];
|
||||
flags: Partial<CommandFlags>;
|
||||
flags: Record<string, unknown>;
|
||||
runtime?: unknown;
|
||||
};
|
||||
|
||||
@@ -20,6 +48,68 @@ export type BatchStepResult = {
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
export async function runBatch(
|
||||
req: BatchRequest,
|
||||
sessionName: string,
|
||||
invoke: BatchInvoke,
|
||||
): Promise<DaemonResponse> {
|
||||
const flags = readBatchFlags(req.flags);
|
||||
const batchOnError = flags?.batchOnError ?? 'stop';
|
||||
if (batchOnError !== 'stop') {
|
||||
return batchErrorResponse('INVALID_ARGS', `Unsupported batch on-error mode: ${batchOnError}.`);
|
||||
}
|
||||
const batchMaxSteps = flags?.batchMaxSteps ?? DEFAULT_BATCH_MAX_STEPS;
|
||||
if (!Number.isInteger(batchMaxSteps) || batchMaxSteps < 1 || batchMaxSteps > 1000) {
|
||||
return batchErrorResponse(
|
||||
'INVALID_ARGS',
|
||||
`Invalid batch max-steps: ${String(flags?.batchMaxSteps)}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
const steps = validateAndNormalizeBatchSteps(flags?.batchSteps, batchMaxSteps);
|
||||
const startedAt = Date.now();
|
||||
const partialResults: BatchStepResult[] = [];
|
||||
for (let index = 0; index < steps.length; index += 1) {
|
||||
const step = steps[index];
|
||||
const stepResponse = await runBatchStep(req, sessionName, step, invoke, index + 1);
|
||||
if (!stepResponse.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: stepResponse.error.code,
|
||||
message: `Batch failed at step ${stepResponse.step} (${step.command}): ${stepResponse.error.message}`,
|
||||
hint: stepResponse.error.hint,
|
||||
diagnosticId: stepResponse.error.diagnosticId,
|
||||
logPath: stepResponse.error.logPath,
|
||||
details: {
|
||||
...(stepResponse.error.details ?? {}),
|
||||
step: stepResponse.step,
|
||||
command: step.command,
|
||||
positionals: step.positionals,
|
||||
executed: index,
|
||||
total: steps.length,
|
||||
partialResults,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
partialResults.push(stepResponse.result);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
total: steps.length,
|
||||
executed: steps.length,
|
||||
totalDurationMs: Date.now() - startedAt,
|
||||
results: partialResults,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const appErr = asAppError(error);
|
||||
return batchErrorResponse(appErr.code, appErr.message, appErr.details);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBatchStepsJson(raw: string): BatchStep[] {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
@@ -34,7 +124,7 @@ export function parseBatchStepsJson(raw: string): BatchStep[] {
|
||||
}
|
||||
|
||||
export function validateAndNormalizeBatchSteps(
|
||||
steps: CommandFlags['batchSteps'],
|
||||
steps: unknown,
|
||||
maxSteps: number,
|
||||
): NormalizedBatchStep[] {
|
||||
if (!Array.isArray(steps) || steps.length === 0) {
|
||||
@@ -49,7 +139,7 @@ export function validateAndNormalizeBatchSteps(
|
||||
|
||||
const normalized: NormalizedBatchStep[] = [];
|
||||
for (let index = 0; index < steps.length; index += 1) {
|
||||
const step = steps[index];
|
||||
const step = steps[index] as Partial<BatchStep>;
|
||||
if (!step || typeof step !== 'object') {
|
||||
throw new AppError('INVALID_ARGS', `Invalid batch step at index ${index}.`);
|
||||
}
|
||||
@@ -93,9 +183,105 @@ export function validateAndNormalizeBatchSteps(
|
||||
normalized.push({
|
||||
command,
|
||||
positionals: positionals as string[],
|
||||
flags: (step.flags ?? {}) as Partial<CommandFlags>,
|
||||
flags: (step.flags ?? {}) as Record<string, unknown>,
|
||||
runtime: step.runtime,
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function buildBatchStepFlags(
|
||||
parentFlags: BatchFlags | Record<string, unknown> | undefined,
|
||||
stepFlags: BatchStep['flags'] | Record<string, unknown> | undefined,
|
||||
): BatchFlags {
|
||||
const {
|
||||
batchSteps: _batchSteps,
|
||||
batchOnError: _batchOnError,
|
||||
batchMaxSteps: _batchMaxSteps,
|
||||
...merged
|
||||
} = stepFlags ?? {};
|
||||
return mergeParentFlags(readBatchFlags(parentFlags), merged as BatchFlags);
|
||||
}
|
||||
|
||||
export function mergeParentFlags<TFlags extends Record<string, unknown>>(
|
||||
parentFlags: BatchFlags | Record<string, unknown> | undefined,
|
||||
childFlags: TFlags,
|
||||
): TFlags {
|
||||
const parentRecord = readBatchFlags(parentFlags) ?? {};
|
||||
const childRecord = childFlags as Record<string, unknown>;
|
||||
for (const key of INHERITED_PARENT_FLAG_KEYS) {
|
||||
if (childRecord[key] === undefined && parentRecord[key] !== undefined) {
|
||||
childRecord[key] = parentRecord[key];
|
||||
}
|
||||
}
|
||||
return childFlags;
|
||||
}
|
||||
|
||||
async function runBatchStep(
|
||||
req: BatchRequest,
|
||||
sessionName: string,
|
||||
step: NormalizedBatchStep,
|
||||
invoke: BatchInvoke,
|
||||
stepNumber: number,
|
||||
): Promise<
|
||||
| { ok: true; step: number; result: BatchStepResult }
|
||||
| {
|
||||
ok: false;
|
||||
step: number;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
hint?: string;
|
||||
diagnosticId?: string;
|
||||
logPath?: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
> {
|
||||
const stepStartedAt = Date.now();
|
||||
const stepFlags = buildBatchStepFlags(req.flags, step.flags);
|
||||
if (stepFlags.session === undefined) {
|
||||
stepFlags.session = sessionName;
|
||||
}
|
||||
const response = await invoke({
|
||||
token: req.token,
|
||||
session: sessionName,
|
||||
command: step.command,
|
||||
positionals: step.positionals,
|
||||
flags: stepFlags,
|
||||
runtime: (step.runtime === undefined ? req.runtime : step.runtime) as DaemonRequest['runtime'],
|
||||
meta: req.meta,
|
||||
});
|
||||
const durationMs = Date.now() - stepStartedAt;
|
||||
if (!response.ok) {
|
||||
return { ok: false, step: stepNumber, error: response.error };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
step: stepNumber,
|
||||
result: {
|
||||
step: stepNumber,
|
||||
command: step.command,
|
||||
ok: true,
|
||||
data: response.data ?? {},
|
||||
durationMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readBatchFlags(
|
||||
flags: BatchFlags | Record<string, unknown> | undefined,
|
||||
): BatchFlags | undefined {
|
||||
return flags as BatchFlags | undefined;
|
||||
}
|
||||
|
||||
function batchErrorResponse(
|
||||
code: string,
|
||||
message: string,
|
||||
details?: Record<string, unknown>,
|
||||
): Extract<DaemonResponse, { ok: false }> {
|
||||
return {
|
||||
ok: false,
|
||||
error: { code, message, ...(details ? { details } : {}) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { CommandFlags } from '../../core/dispatch.ts';
|
||||
import { SessionStore } from '../session-store.ts';
|
||||
import type { DaemonRequest, SessionState } from '../types.ts';
|
||||
|
||||
export { mergeParentFlags } from '../../core/batch.ts';
|
||||
|
||||
/**
|
||||
* Record a session action if a session is active. No-op when session is undefined.
|
||||
*/
|
||||
@@ -20,34 +22,3 @@ export function recordSessionAction(
|
||||
result: result ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag keys inherited from a parent request (batch/replay) into child step flags.
|
||||
* Shared between batch and replay so the inheritance rules stay in sync.
|
||||
*/
|
||||
const INHERITED_PARENT_FLAG_KEYS: ReadonlyArray<keyof CommandFlags> = [
|
||||
'platform',
|
||||
'target',
|
||||
'device',
|
||||
'udid',
|
||||
'serial',
|
||||
'verbose',
|
||||
'out',
|
||||
];
|
||||
|
||||
/**
|
||||
* Merge parent flag values into child flags for keys that are undefined in the child.
|
||||
*/
|
||||
export function mergeParentFlags(
|
||||
parentFlags: CommandFlags | undefined,
|
||||
childFlags: CommandFlags,
|
||||
): CommandFlags {
|
||||
const parentRecord = (parentFlags ?? {}) as Record<string, unknown>;
|
||||
const childRecord = childFlags as Record<string, unknown>;
|
||||
for (const key of INHERITED_PARENT_FLAG_KEYS) {
|
||||
if (childRecord[key] === undefined && parentRecord[key] !== undefined) {
|
||||
childRecord[key] = parentRecord[key];
|
||||
}
|
||||
}
|
||||
return childFlags;
|
||||
}
|
||||
|
||||
@@ -1,137 +1,10 @@
|
||||
import {
|
||||
DEFAULT_BATCH_MAX_STEPS,
|
||||
type BatchStepResult,
|
||||
type NormalizedBatchStep,
|
||||
validateAndNormalizeBatchSteps,
|
||||
} from '../../core/batch.ts';
|
||||
import type { BatchStep, CommandFlags } from '../../core/dispatch.ts';
|
||||
import { asAppError } from '../../utils/errors.ts';
|
||||
import { type BatchInvoke, runBatch } from '../../core/batch.ts';
|
||||
import type { DaemonRequest, DaemonResponse } from '../types.ts';
|
||||
import { mergeParentFlags } from './handler-utils.ts';
|
||||
import { errorResponse } from './response.ts';
|
||||
|
||||
export async function runBatchCommands(
|
||||
req: DaemonRequest,
|
||||
sessionName: string,
|
||||
invoke: (req: DaemonRequest) => Promise<DaemonResponse>,
|
||||
): Promise<DaemonResponse> {
|
||||
const batchOnError = req.flags?.batchOnError ?? 'stop';
|
||||
if (batchOnError !== 'stop') {
|
||||
return errorResponse('INVALID_ARGS', `Unsupported batch on-error mode: ${batchOnError}.`);
|
||||
}
|
||||
const batchMaxSteps = req.flags?.batchMaxSteps ?? DEFAULT_BATCH_MAX_STEPS;
|
||||
if (!Number.isInteger(batchMaxSteps) || batchMaxSteps < 1 || batchMaxSteps > 1000) {
|
||||
return errorResponse(
|
||||
'INVALID_ARGS',
|
||||
`Invalid batch max-steps: ${String(req.flags?.batchMaxSteps)}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
const steps = validateAndNormalizeBatchSteps(req.flags?.batchSteps, batchMaxSteps);
|
||||
const startedAt = Date.now();
|
||||
const partialResults: BatchStepResult[] = [];
|
||||
for (let index = 0; index < steps.length; index += 1) {
|
||||
const step = steps[index];
|
||||
const stepResponse = await runBatchStep(req, sessionName, step, invoke, index + 1);
|
||||
if (!stepResponse.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: stepResponse.error.code,
|
||||
message: `Batch failed at step ${stepResponse.step} (${step.command}): ${stepResponse.error.message}`,
|
||||
hint: stepResponse.error.hint,
|
||||
diagnosticId: stepResponse.error.diagnosticId,
|
||||
logPath: stepResponse.error.logPath,
|
||||
details: {
|
||||
...(stepResponse.error.details ?? {}),
|
||||
step: stepResponse.step,
|
||||
command: step.command,
|
||||
positionals: step.positionals,
|
||||
executed: index,
|
||||
total: steps.length,
|
||||
partialResults,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
partialResults.push(stepResponse.result);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
total: steps.length,
|
||||
executed: steps.length,
|
||||
totalDurationMs: Date.now() - startedAt,
|
||||
results: partialResults,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const appErr = asAppError(error);
|
||||
return errorResponse(appErr.code, appErr.message, appErr.details);
|
||||
}
|
||||
}
|
||||
|
||||
async function runBatchStep(
|
||||
req: DaemonRequest,
|
||||
sessionName: string,
|
||||
step: NormalizedBatchStep,
|
||||
invoke: (req: DaemonRequest) => Promise<DaemonResponse>,
|
||||
stepNumber: number,
|
||||
): Promise<
|
||||
| { ok: true; step: number; result: BatchStepResult }
|
||||
| {
|
||||
ok: false;
|
||||
step: number;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
hint?: string;
|
||||
diagnosticId?: string;
|
||||
logPath?: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
> {
|
||||
const stepStartedAt = Date.now();
|
||||
const stepFlags = buildBatchStepFlags(req.flags, step.flags);
|
||||
if (stepFlags.session === undefined) {
|
||||
stepFlags.session = sessionName;
|
||||
}
|
||||
const response = await invoke({
|
||||
token: req.token,
|
||||
session: sessionName,
|
||||
command: step.command,
|
||||
positionals: step.positionals,
|
||||
flags: stepFlags,
|
||||
runtime: (step.runtime === undefined ? req.runtime : step.runtime) as DaemonRequest['runtime'],
|
||||
meta: req.meta,
|
||||
});
|
||||
const durationMs = Date.now() - stepStartedAt;
|
||||
if (!response.ok) {
|
||||
return { ok: false, step: stepNumber, error: response.error };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
step: stepNumber,
|
||||
result: {
|
||||
step: stepNumber,
|
||||
command: step.command,
|
||||
ok: true,
|
||||
data: response.data ?? {},
|
||||
durationMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildBatchStepFlags(
|
||||
parentFlags: CommandFlags | undefined,
|
||||
stepFlags: BatchStep['flags'] | undefined,
|
||||
): CommandFlags {
|
||||
const {
|
||||
batchSteps: _batchSteps,
|
||||
batchOnError: _batchOnError,
|
||||
batchMaxSteps: _batchMaxSteps,
|
||||
...merged
|
||||
} = stepFlags ?? {};
|
||||
return mergeParentFlags(parentFlags, merged as CommandFlags);
|
||||
return await runBatch(req, sessionName, invoke as BatchInvoke);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ Public subpath API exposed for Node consumers:
|
||||
- `buildIosRuntimeHints(baseUrl)`
|
||||
- `buildAndroidRuntimeHints(baseUrl)`
|
||||
- types: `PrepareRemoteMetroOptions`, `PrepareRemoteMetroResult`, `EnsureMetroTunnelOptions`, `EnsureMetroTunnelResult`, `ReloadRemoteMetroOptions`, `ReloadRemoteMetroResult`, `StopMetroTunnelOptions`, `MetroRuntimeHints`, `MetroBridgeResult`
|
||||
- `agent-device/batch`
|
||||
- `runBatch(req, sessionName, invoke)`
|
||||
- `validateAndNormalizeBatchSteps(steps, maxSteps)`
|
||||
- `buildBatchStepFlags(parentFlags, stepFlags)`
|
||||
- `DEFAULT_BATCH_MAX_STEPS`
|
||||
- `BATCH_BLOCKED_COMMANDS`
|
||||
- `INHERITED_PARENT_FLAG_KEYS`
|
||||
- types: `BatchInvoke`, `BatchRequest`, `BatchStep`, `BatchStepResult`, `NormalizedBatchStep`
|
||||
- `agent-device/remote-config`
|
||||
- `resolveRemoteConfigPath(options)`
|
||||
- `resolveRemoteConfigProfile(options)`
|
||||
@@ -61,7 +69,7 @@ Public subpath API exposed for Node consumers:
|
||||
- `verifyAndroidSnapshotHelperArtifact(artifact)`
|
||||
- types: `AndroidAdbExecutor`, `AndroidSnapshotHelperArtifact`, `AndroidSnapshotHelperManifest`, `AndroidSnapshotHelperOutput`, `AndroidSnapshotHelperParsedSnapshot`
|
||||
|
||||
The `contracts`, `selectors`, `finders`, `install-source`, `android-apps`, `artifacts`, `metro`, `remote-config`, and `io` subpaths remain available for compatibility. The former hosted-runtime subpaths `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability` are no longer published.
|
||||
The `contracts`, `selectors`, `finders`, `install-source`, `android-apps`, `artifacts`, `batch`, `metro`, `remote-config`, and `io` subpaths remain available for compatibility. The former hosted-runtime subpaths `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability` are no longer published.
|
||||
|
||||
## Basic usage
|
||||
|
||||
@@ -188,6 +196,26 @@ Additional CLI-backed methods are exposed on their domain groups with typed opti
|
||||
|
||||
`client.recording.record({ action: 'start', path, quality: 5 })` starts a smaller 50% resolution video; omit `quality` to keep native/current resolution.
|
||||
|
||||
## Batch orchestration for custom transports
|
||||
|
||||
Use `agent-device/batch` when a bridge or in-process runner receives daemon-shaped requests but owns command dispatch itself. The helper keeps validation, inherited flags, serial execution, partial results, and error envelopes aligned with the daemon batch command.
|
||||
|
||||
```ts
|
||||
import { runBatch } from 'agent-device/batch';
|
||||
import type { BatchRequest } from 'agent-device/batch';
|
||||
import type { DaemonResponse } from 'agent-device/contracts';
|
||||
|
||||
async function handleBatch(req: BatchRequest): Promise<DaemonResponse> {
|
||||
return await runBatch(req, req.session ?? 'default', async (stepReq) => {
|
||||
try {
|
||||
return { ok: true, data: await dispatch(stepReq) };
|
||||
} catch (error) {
|
||||
return bridgeErrorToDaemonResponse(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Android `installFromSource()`
|
||||
|
||||
```ts
|
||||
|
||||
Reference in New Issue
Block a user