@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2
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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- package/skills/ai-app-bridge-use/SKILL.md +23 -360
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileBounded } = require('./shared-kernel/execution-io');
|
|
4
|
+
const { checkExecution } = require('./shared-kernel/execution-scope');
|
|
5
|
+
const { CommandError, executionFields } = require('./command-errors');
|
|
6
|
+
const { executeAndroidShell } = require('./shared-kernel/android-shell-execution');
|
|
7
|
+
const execute = (file, args, { timeout, mutation, actionId, ...options }) => mutation
|
|
8
|
+
? executeAndroidShell({ adb: file, serial: args[1], argv: args.slice(3), timeoutMs: timeout, actionId })
|
|
9
|
+
: execFileBounded(file, args, { ...options, timeoutMs: timeout, encoding: 'utf8' });
|
|
10
|
+
const REQUEST_PERMISSIONS = 'android.content.pm.action.REQUEST_PERMISSIONS';
|
|
11
|
+
|
|
12
|
+
function requireTarget(args) {
|
|
13
|
+
for (const field of ['packageName', 'permission']) {
|
|
14
|
+
if (typeof args[field] !== 'string' || !/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+$/.test(args[field])) {
|
|
15
|
+
throw new CommandError('invalid_argument', `${field} must be a qualified Android identifier.`, { field });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (typeof args.serial !== 'string' || !args.serial.trim()) throw new CommandError('missing_argument', 'serial is required.', { field: 'serial' });
|
|
19
|
+
if (args.userId !== undefined && (!Number.isSafeInteger(args.userId) || args.userId < 0 || args.userId > 2147483647)) {
|
|
20
|
+
throw new CommandError('invalid_argument', 'userId must be a non-negative Android user ID.', { field: 'userId' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function androidCommand(args, values, run = execute, { mutation = false } = {}) {
|
|
25
|
+
checkExecution();
|
|
26
|
+
return run(args.adb || process.env.ADB || 'adb', ['-s', args.serial, ...values], {
|
|
27
|
+
timeout: args.adbTimeoutMs || 15000, maxBuffer: 16 * 1024 * 1024, mutation,
|
|
28
|
+
actionId: args.runtimeActionId ?? args.requestId,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function block(lines, start) {
|
|
33
|
+
const indent = lines[start].search(/\S/);
|
|
34
|
+
let end = start + 1;
|
|
35
|
+
while (end < lines.length && (!lines[end].trim() || lines[end].search(/\S/) > indent)) end++;
|
|
36
|
+
return lines.slice(start + 1, end);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function uniqueBlock(lines, predicate, error) {
|
|
40
|
+
const indexes = lines.flatMap((line, i) => predicate(line.trim()) ? [i] : []);
|
|
41
|
+
if (indexes.length !== 1) throw new CommandError(error, 'Android did not provide one unambiguous package/user permission record.');
|
|
42
|
+
return { header: lines[indexes[0]].trim(), lines: block(lines, indexes[0]) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parsePermissionState(text, { packageName, permission, userId }) {
|
|
46
|
+
const pkg = uniqueBlock(text.split(/\r?\n/), line => line.startsWith(`Package [${packageName}] (`), 'permission_package_not_found');
|
|
47
|
+
const appId = pkg.lines.map(line => /^\s+(?:appId|userId)=(\d+)\s*$/.exec(line)).filter(Boolean);
|
|
48
|
+
if (appId.length !== 1 || Number(appId[0][1]) >= 100000) throw new CommandError('permission_state_unsupported', 'Android package appId is unavailable or ambiguous.');
|
|
49
|
+
const user = uniqueBlock(pkg.lines, line => line.startsWith(`User ${userId}:`), 'permission_user_not_found');
|
|
50
|
+
if (!/\binstalled=true\b/.test(user.header)) throw new CommandError('permission_package_not_installed', 'The package is not installed for the requested Android user.');
|
|
51
|
+
const runtime = uniqueBlock(user.lines, line => line === 'runtime permissions:', 'runtime_permission_not_found');
|
|
52
|
+
const matches = runtime.lines.filter(line => line.trim().startsWith(`${permission}:`));
|
|
53
|
+
const match = matches.length === 1 && /^\s*[^:]+:\s+granted=(true|false),\s*flags=\[([^\]]*)\]\s*$/.exec(matches[0]);
|
|
54
|
+
if (!matches.length) throw new CommandError('runtime_permission_not_found', 'The requested permission has no runtime permission record for this package and user.');
|
|
55
|
+
if (!match) throw new CommandError('permission_state_unsupported', 'Android returned an unrecognized runtime permission record.');
|
|
56
|
+
const flags = match[2].trim() ? match[2].split('|').map(value => value.trim()) : [];
|
|
57
|
+
if (flags.some(value => !/^[A-Z][A-Z0-9_]*$/.test(value)) || new Set(flags).size !== flags.length) {
|
|
58
|
+
throw new CommandError('permission_state_unsupported', 'Android returned unrecognized permission flags.');
|
|
59
|
+
}
|
|
60
|
+
return { packageName, permission, userId, uid: userId * 100000 + Number(appId[0][1]), granted: match[1] === 'true', flags: flags.sort() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readPermissionState(args, run = execute) {
|
|
64
|
+
requireTarget(args);
|
|
65
|
+
try {
|
|
66
|
+
let userId = args.userId;
|
|
67
|
+
if (userId === undefined) {
|
|
68
|
+
const current = (await androidCommand(args, ['shell', 'am', 'get-current-user'], run)).stdout.trim();
|
|
69
|
+
if (!/^\d+$/.test(current) || Number(current) > 2147483647) throw new CommandError('permission_user_unknown', 'Android current user could not be determined.');
|
|
70
|
+
userId = Number(current);
|
|
71
|
+
}
|
|
72
|
+
const dump = await androidCommand(args, ['shell', 'dumpsys', 'package', args.packageName], run);
|
|
73
|
+
return { ok: true, source: 'android-package-manager', serial: args.serial,
|
|
74
|
+
...parsePermissionState(dump.stdout, { ...args, userId }), capturedAtMs: Date.now() };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof CommandError) throw error;
|
|
77
|
+
throw new CommandError('permission_query_failed', 'Android permission state could not be read.', { details: { cause: error.code || null } });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function changePermission(args, action, run = execute) {
|
|
82
|
+
if (!['grant', 'revoke'].includes(action)) throw new TypeError('Unknown permission mutation');
|
|
83
|
+
const before = await readPermissionState(args, run);
|
|
84
|
+
const target = { ...args, userId: before.userId };
|
|
85
|
+
let failure = null;
|
|
86
|
+
let execution;
|
|
87
|
+
try { execution = await androidCommand(target, ['shell', 'pm', action, '--user', String(before.userId), args.packageName, args.permission], run, { mutation: true }); }
|
|
88
|
+
catch (error) { failure = error; }
|
|
89
|
+
if (failure?.settled === false) throw failure;
|
|
90
|
+
try { checkExecution(); }
|
|
91
|
+
catch (error) { throw Object.assign(error, executionFields(failure || execution)); }
|
|
92
|
+
let after; let queryError;
|
|
93
|
+
try { after = await readPermissionState(target, run); }
|
|
94
|
+
catch (error) { queryError = error.code || 'permission_query_failed'; }
|
|
95
|
+
const details = { action, before, after: after || null, queryError: queryError || null };
|
|
96
|
+
if (failure) {
|
|
97
|
+
const response = [failure.stderr, failure.stdout].filter(Boolean).join('\n');
|
|
98
|
+
const denied = /\bjava\.lang\.SecurityException:/.test(response);
|
|
99
|
+
const unchanged = after && after.uid === before.uid && after.granted === before.granted && JSON.stringify(after.flags) === JSON.stringify(before.flags);
|
|
100
|
+
const reason = response.split(/\r?\n/).find(line => line.includes('Exception:')) || response.trim().slice(0, 1024);
|
|
101
|
+
throw new CommandError(denied ? 'permission_change_denied' : 'permission_change_failed',
|
|
102
|
+
denied ? 'Android does not authorize this ADB identity to change runtime permissions.' : 'Android rejected or did not acknowledge the permission change.', {
|
|
103
|
+
details: { ...details, cause: failure.code || null, reason: reason.slice(0, 1024) || null }, dispatched: failure.code !== 'ENOENT',
|
|
104
|
+
ambiguous: failure.code !== 'ENOENT' && !(denied && unchanged),
|
|
105
|
+
...executionFields(failure),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!after || after.uid !== before.uid || after.granted !== (action === 'grant')) {
|
|
109
|
+
throw new CommandError('permission_verification_failed', 'The permission change was submitted but its resulting state was not verified.', { details, ...executionFields(execution), dispatched: true, ambiguous: !after || after.uid !== before.uid });
|
|
110
|
+
}
|
|
111
|
+
return { ...after, action, before, verified: true, dispatched: true, ambiguous: false, ...executionFields(execution) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function activityIdentity(value) {
|
|
115
|
+
const match = /ActivityRecord\{([^\s{}]+) u(\d+) ([^\s{}]+\/[^\s{}]+)\s/.exec(value);
|
|
116
|
+
if (!match) return null;
|
|
117
|
+
const [packageName, activity] = match[3].split('/');
|
|
118
|
+
// Android's flattened ComponentName may abbreviate the class with a leading dot.
|
|
119
|
+
const component = `${packageName}/${activity.startsWith('.') ? packageName + activity : activity}`;
|
|
120
|
+
return { token: match[1], userId: Number(match[2]), component, packageName };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ActivityManager supplies requester identity even when Intent extras are redacted.
|
|
124
|
+
// It does not expose the requested permission list here; the Agent must inspect
|
|
125
|
+
// the actual dialog, and PackageManager independently verifies the named permission.
|
|
126
|
+
function parsePermissionRequest(text) {
|
|
127
|
+
const lines = text.split(/\r?\n/);
|
|
128
|
+
const tops = lines.filter(line => /^\s*topResumedActivity=/.test(line)).map(activityIdentity);
|
|
129
|
+
if (tops.length !== 1 || !tops[0]) throw new CommandError('permission_request_unsupported', 'One top-resumed Android Activity could not be identified.');
|
|
130
|
+
const records = lines.flatMap((line, i) => /^\s*\* Hist\s+#\d+: ActivityRecord\{/.test(line) ? [{ ...activityIdentity(line), lines: block(lines, i) }] : []);
|
|
131
|
+
const requests = records.filter(record => record.lines.some(line => line.trim().startsWith('Intent {') && line.includes(`act=${REQUEST_PERMISSIONS} `)));
|
|
132
|
+
const current = records.filter(record => record.token === tops[0].token && record.component === tops[0].component && record.userId === tops[0].userId);
|
|
133
|
+
if (current.length !== 1) throw new CommandError('permission_request_unsupported', 'The top-resumed Activity has no unique detailed record.');
|
|
134
|
+
const result = { source: 'android-activity-manager', foreground: tops[0], activeRequestTokens: requests.map(record => record.token), request: null };
|
|
135
|
+
if (!requests.includes(current[0])) return result;
|
|
136
|
+
const origin = current[0].lines.map(line => /\blaunchedFromUid=(\d+) launchedFromPackage=([^\s]+).*\buserId=(\d+)\s*$/.exec(line)).filter(Boolean);
|
|
137
|
+
if (origin.length !== 1 || Number(origin[0][3]) !== tops[0].userId) throw new CommandError('permission_request_unsupported', 'The runtime permission requester is unavailable or ambiguous.');
|
|
138
|
+
result.request = { ...tops[0], action: REQUEST_PERMISSIONS, requesterPackage: origin[0][2], requesterUid: Number(origin[0][1]) };
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function readPermissionRequest(args, run = execute) {
|
|
143
|
+
try {
|
|
144
|
+
const dump = await androidCommand(args, ['shell', 'dumpsys', 'activity', 'activities'], run);
|
|
145
|
+
return { ...parsePermissionRequest(dump.stdout), capturedAtMs: Date.now() };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof CommandError) throw error;
|
|
148
|
+
throw new CommandError('permission_request_query_failed', 'Android runtime permission request could not be read.', { details: { cause: error.code || null } });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { readPermissionState, changePermission, readPermissionRequest, parsePermissionState, parsePermissionRequest, androidCommand };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Legacy `uiautomator dump` serializes every accessibility node as `<node>`.
|
|
4
|
+
// Appium's persistent UiAutomator2 server instead serializes the Android class
|
|
5
|
+
// name as the XML element name (for example `<android.view.View>`). Keep one
|
|
6
|
+
// lightweight scanner for both wire formats so summary, actions, compaction,
|
|
7
|
+
// and persistence redaction cannot drift apart.
|
|
8
|
+
const XML_TAG_PATTERN = /<\s*(\/?)\s*([A-Za-z_][A-Za-z0-9_.:$-]*)(?:\s+(?:[^"'<>]|"[^"]*"|'[^']*')*)?\s*\/?>/g;
|
|
9
|
+
const XML_OPENING_TAG_PATTERN = /^<\s*([A-Za-z_][A-Za-z0-9_.:$-]*)(?:\s|\/?>)/;
|
|
10
|
+
|
|
11
|
+
function visitAndroidUiHierarchyTags(value, visitor) {
|
|
12
|
+
const text = String(value || '');
|
|
13
|
+
const pattern = new RegExp(XML_TAG_PATTERN.source, XML_TAG_PATTERN.flags);
|
|
14
|
+
let insideHierarchy = false;
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
17
|
+
const tag = match[0];
|
|
18
|
+
const closing = match[1] === '/';
|
|
19
|
+
const tagName = match[2];
|
|
20
|
+
const selfClosing = /\/\s*>$/.test(tag);
|
|
21
|
+
if (tagName.toLowerCase() === 'hierarchy') {
|
|
22
|
+
insideHierarchy = !closing && !selfClosing;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!insideHierarchy) continue;
|
|
26
|
+
if (visitor({
|
|
27
|
+
tag,
|
|
28
|
+
tagName,
|
|
29
|
+
closing,
|
|
30
|
+
selfClosing,
|
|
31
|
+
start: match.index,
|
|
32
|
+
end: pattern.lastIndex,
|
|
33
|
+
}) === false) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function looksLikeAndroidUiHierarchyXml(value) {
|
|
39
|
+
const text = String(value || '').trimStart();
|
|
40
|
+
const withoutDeclaration = text.replace(/^<\?xml\s[^?]*\?>\s*/i, '');
|
|
41
|
+
if (!/^<hierarchy(?:\s|>)/i.test(withoutDeclaration)) return false;
|
|
42
|
+
let foundElement = false;
|
|
43
|
+
visitAndroidUiHierarchyTags(withoutDeclaration, ({ closing }) => {
|
|
44
|
+
if (closing) return;
|
|
45
|
+
foundElement = true;
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
return foundElement;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function replaceAndroidUiOpeningTags(value, transform) {
|
|
52
|
+
const text = String(value || '');
|
|
53
|
+
let output = '';
|
|
54
|
+
let cursor = 0;
|
|
55
|
+
visitAndroidUiHierarchyTags(text, ({ tag, tagName, closing, start, end }) => {
|
|
56
|
+
if (closing) return;
|
|
57
|
+
output += text.slice(cursor, start);
|
|
58
|
+
output += transform(tag, tagName);
|
|
59
|
+
cursor = end;
|
|
60
|
+
});
|
|
61
|
+
return cursor === 0 ? text : output + text.slice(cursor);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isAndroidUiElementOpeningTag(value) {
|
|
65
|
+
const match = XML_OPENING_TAG_PATTERN.exec(String(value || ''));
|
|
66
|
+
return Boolean(match && match[1].toLowerCase() !== 'hierarchy');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
isAndroidUiElementOpeningTag,
|
|
71
|
+
looksLikeAndroidUiHierarchyXml,
|
|
72
|
+
replaceAndroidUiOpeningTags,
|
|
73
|
+
visitAndroidUiHierarchyTags,
|
|
74
|
+
};
|
package/bin/artifact-paths.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
const { execFileSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { requestDirectory } = require('./shared-kernel/request-context');
|
|
4
5
|
|
|
5
6
|
const artifactDirectoryName = 'ai_app_bridge_artifacts';
|
|
7
|
+
const generatedArtifactRetention = 20;
|
|
8
|
+
const generatedArtifactMaxAgeMs = 24 * 60 * 60 * 1000;
|
|
9
|
+
const generatedArtifactMaxBytes = 64 * 1024 * 1024;
|
|
6
10
|
|
|
7
11
|
function defaultArtifactDirectory(options = {}) {
|
|
8
|
-
const cwd = path.resolve(options.cwd ||
|
|
12
|
+
const cwd = path.resolve(options.cwd || requestDirectory());
|
|
9
13
|
const gitRoot = gitOutput(cwd, ['rev-parse', '--show-toplevel']);
|
|
10
14
|
if (!gitRoot) {
|
|
11
15
|
return path.join(cwd, 'build', artifactDirectoryName);
|
|
@@ -34,6 +38,104 @@ function defaultArtifactPath(prefix, extension, options = {}) {
|
|
|
34
38
|
return path.join(directory, name);
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
async function pruneGeneratedArtifacts(options = {}) {
|
|
42
|
+
const keep = positiveIntegerOption(options.keep, generatedArtifactRetention);
|
|
43
|
+
const maxAgeMs = nonNegativeNumberOption(options.maxAgeMs, generatedArtifactMaxAgeMs);
|
|
44
|
+
const maxBytes = nonNegativeNumberOption(options.maxBytes, generatedArtifactMaxBytes);
|
|
45
|
+
const nowMs = finiteNumberOption(options.nowMs, Date.now());
|
|
46
|
+
const result = {
|
|
47
|
+
keep,
|
|
48
|
+
maxAgeMs,
|
|
49
|
+
maxBytes,
|
|
50
|
+
matched: 0,
|
|
51
|
+
deleted: 0,
|
|
52
|
+
deletedExpired: 0,
|
|
53
|
+
deletedForCount: 0,
|
|
54
|
+
deletedForBytes: 0,
|
|
55
|
+
bytesBefore: 0,
|
|
56
|
+
bytesAfter: 0,
|
|
57
|
+
};
|
|
58
|
+
const directory = path.resolve(options.directory || requestDirectory());
|
|
59
|
+
const prefix = sanitizeArtifactName(options.prefix || 'artifact');
|
|
60
|
+
const extension = sanitizeArtifactExtension(options.extension || 'bin');
|
|
61
|
+
const currentPath = options.currentPath ? path.resolve(options.currentPath) : '';
|
|
62
|
+
const pattern = new RegExp(`^${escapeRegExp(prefix)}-\\d{8}-\\d{6}-\\d{3}-\\d+-[a-z0-9]+\\.${escapeRegExp(extension)}$`);
|
|
63
|
+
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { ...result, error: firstErrorLine(error) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isFile() || !pattern.test(entry.name)) continue;
|
|
74
|
+
const filePath = path.join(directory, entry.name);
|
|
75
|
+
try {
|
|
76
|
+
const stat = await fs.promises.stat(filePath);
|
|
77
|
+
files.push({
|
|
78
|
+
name: entry.name,
|
|
79
|
+
path: path.resolve(filePath),
|
|
80
|
+
mtimeMs: stat.mtimeMs,
|
|
81
|
+
size: stat.size,
|
|
82
|
+
});
|
|
83
|
+
} catch (_) {
|
|
84
|
+
// Ignore files that disappear while pruning.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
result.matched = files.length;
|
|
89
|
+
result.bytesBefore = files.reduce((total, file) => total + file.size, 0);
|
|
90
|
+
result.bytesAfter = result.bytesBefore;
|
|
91
|
+
files.sort((left, right) => (right.mtimeMs - left.mtimeMs) || right.name.localeCompare(left.name));
|
|
92
|
+
|
|
93
|
+
const keepSet = new Set();
|
|
94
|
+
let retainedBytes = 0;
|
|
95
|
+
const current = files.find((file) => file.path === currentPath);
|
|
96
|
+
if (current) {
|
|
97
|
+
keepSet.add(current.path);
|
|
98
|
+
retainedBytes += current.size;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const deletionReasons = new Map();
|
|
102
|
+
const expiresBeforeMs = nowMs - maxAgeMs;
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
if (keepSet.has(file.path)) continue;
|
|
105
|
+
if (file.mtimeMs < expiresBeforeMs) {
|
|
106
|
+
deletionReasons.set(file.path, 'expired');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (keepSet.size >= keep) {
|
|
110
|
+
deletionReasons.set(file.path, 'count');
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (retainedBytes + file.size > maxBytes) {
|
|
114
|
+
deletionReasons.set(file.path, 'bytes');
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
keepSet.add(file.path);
|
|
118
|
+
retainedBytes += file.size;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const reason = deletionReasons.get(file.path);
|
|
123
|
+
if (!reason) continue;
|
|
124
|
+
try {
|
|
125
|
+
await fs.promises.rm(file.path, { force: true });
|
|
126
|
+
result.deleted += 1;
|
|
127
|
+
result.bytesAfter -= file.size;
|
|
128
|
+
if (reason === 'expired') result.deletedExpired += 1;
|
|
129
|
+
if (reason === 'count') result.deletedForCount += 1;
|
|
130
|
+
if (reason === 'bytes') result.deletedForBytes += 1;
|
|
131
|
+
} catch (_) {
|
|
132
|
+
// Pruning is best-effort and should not make screenshot capture fail.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
result.overBudgetBytes = Math.max(0, result.bytesAfter - maxBytes);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
37
139
|
function projectRoots(cwd, gitRoot) {
|
|
38
140
|
const roots = [];
|
|
39
141
|
const resolvedGitRoot = path.resolve(gitRoot);
|
|
@@ -156,11 +258,35 @@ function sanitizeArtifactExtension(value) {
|
|
|
156
258
|
return sanitizeArtifactName(String(value || 'bin').replace(/^\.+/, '')) || 'bin';
|
|
157
259
|
}
|
|
158
260
|
|
|
261
|
+
function escapeRegExp(value) {
|
|
262
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function firstErrorLine(error) {
|
|
266
|
+
return String(error?.message || error || 'unknown error').split(/\r?\n/, 1)[0];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function positiveIntegerOption(value, fallback) {
|
|
270
|
+
const number = Number(value);
|
|
271
|
+
return Number.isInteger(number) && number > 0 ? number : fallback;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function nonNegativeNumberOption(value, fallback) {
|
|
275
|
+
const number = Number(value);
|
|
276
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function finiteNumberOption(value, fallback) {
|
|
280
|
+
const number = Number(value);
|
|
281
|
+
return Number.isFinite(number) ? number : fallback;
|
|
282
|
+
}
|
|
283
|
+
|
|
159
284
|
module.exports = {
|
|
160
285
|
artifactDirectoryCandidates,
|
|
161
286
|
artifactTimestamp,
|
|
162
287
|
defaultArtifactDirectory,
|
|
163
288
|
defaultArtifactPath,
|
|
289
|
+
pruneGeneratedArtifacts,
|
|
164
290
|
sanitizeArtifactExtension,
|
|
165
291
|
sanitizeArtifactName,
|
|
166
292
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function listForwards(ctx, adb) {
|
|
4
|
+
const { stdout } = await adb(ctx, ['forward', '--list']);
|
|
5
|
+
return stdout.trim().split(/\r?\n/).filter(Boolean).map((line) => {
|
|
6
|
+
const [serial, local, remote] = line.trim().split(/\s+/);
|
|
7
|
+
return { serial, local, remote };
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function forwardMismatch(expected, actual) {
|
|
12
|
+
const error = new Error(`bridge forward mismatch: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
13
|
+
error.aiAppBridgeForwardMismatch = true;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function createBridgeForward(ctx, endpoint, adb) {
|
|
18
|
+
const serial = ctx.serial || (await adb(ctx, ['get-serialno'])).stdout.trim();
|
|
19
|
+
const forwards = await listForwards(ctx, adb);
|
|
20
|
+
const remote = `localabstract:${endpoint.socketName}`;
|
|
21
|
+
const existing = ctx.explicitPort
|
|
22
|
+
? forwards.find((entry) => entry.local === `tcp:${ctx.port}`)
|
|
23
|
+
: forwards.find((entry) => entry.serial === serial && entry.remote === remote && /^tcp:\d+$/.test(entry.local));
|
|
24
|
+
if (existing) {
|
|
25
|
+
if (existing.serial !== serial || existing.remote !== remote) {
|
|
26
|
+
throw forwardMismatch({ serial, local: `tcp:${ctx.port}`, remote }, existing);
|
|
27
|
+
}
|
|
28
|
+
return { serial, hostPort: Number(existing.local.slice(4)), reused: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ADB allocates the host port; the remote address is the discovered App runtime.
|
|
32
|
+
const { stdout } = await adb(ctx, ['forward', '--no-rebind', `tcp:${ctx.explicitPort ? ctx.port : 0}`, remote]);
|
|
33
|
+
const hostPort = ctx.explicitPort ? ctx.port : Number(stdout.trim());
|
|
34
|
+
if (!Number.isInteger(hostPort) || hostPort < 1 || hostPort > 65535) {
|
|
35
|
+
throw new Error(`ADB returned an invalid allocated host port: ${stdout.trim()}`);
|
|
36
|
+
}
|
|
37
|
+
return { serial, hostPort, reused: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function verifyBridgeForward(ctx, connection, adb) {
|
|
41
|
+
const expected = { serial: connection.serial, local: `tcp:${connection.hostPort}`, remote: `localabstract:${connection.endpoint.socketName}` };
|
|
42
|
+
const actual = (await listForwards(ctx, adb)).find((entry) => entry.local === expected.local);
|
|
43
|
+
if (!actual || actual.serial !== expected.serial || actual.remote !== expected.remote) {
|
|
44
|
+
throw forwardMismatch(expected, actual || null);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function removeBridgeForward(ctx, adb) {
|
|
49
|
+
const local = `tcp:${ctx.port}`;
|
|
50
|
+
const actual = (await listForwards(ctx, adb)).find(entry => entry.local === local);
|
|
51
|
+
if (!actual || actual.serial !== ctx.serial) throw forwardMismatch({ serial: ctx.serial, local }, actual || null);
|
|
52
|
+
await adb(ctx, ['forward', '--remove', local]);
|
|
53
|
+
return { ok: true, serial: ctx.serial, removed: local, remote: actual.remote };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { createBridgeForward, verifyBridgeForward, removeBridgeForward };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { commandDefinitions, isolatedCommandDefinitions, commandByName, isolatedByName, commandSchema, commandContract } = require('./command-registry');
|
|
4
|
+
const supportedTargets = [
|
|
5
|
+
'Android native apps',
|
|
6
|
+
'Android WebView/H5/CDP',
|
|
7
|
+
'Flutter apps on Android and iOS',
|
|
8
|
+
'iOS native apps via AiAppBridgeIOS + WebDriverAgent/XCUITest',
|
|
9
|
+
'WKWebView',
|
|
10
|
+
'desktop Web Bridge sessions',
|
|
11
|
+
];
|
|
12
|
+
const commandDomains = {
|
|
13
|
+
execution: 'intent for observed decisions; script for repeatable local code with progress, persisted results and assertions; runtime and device-ownership controls',
|
|
14
|
+
evidence: 'export and verify retained execution evidence',
|
|
15
|
+
core: 'status, tree, uia-tree, screenshot, logs, network, state, events',
|
|
16
|
+
app: 'install-apk, clear-app-data, launch-*, freeze-app/thaw-app, permission-*, appops-set',
|
|
17
|
+
action: 'tap, tap-text, tap-uia, tap-uia-text, input-text, swipe, keyevent, wait-text, keyboard-state, hide-keyboard',
|
|
18
|
+
flutter: 'flutter-tree, flutter-nodes, flutter-action, tap/input/scroll Flutter controls',
|
|
19
|
+
webview: 'h5-*, flutter-h5-*, webview-pages, webview-network, webview-console',
|
|
20
|
+
ios: 'ios-devices, ios-doctor, ios-setup, ios runtime evidence, ios-uia-tree, ios-tap/ios-input/ios-swipe, ios-h5-*, ios-flutter-*',
|
|
21
|
+
web: 'web-session-start, web-sessions, web-status, web-dom, web-logs, web-network, web-state, web-events, web-command, web-click, web-input, web-wait, web-scroll',
|
|
22
|
+
diagnostics: 'logcat',
|
|
23
|
+
advanced: 'uia-runtime maintenance, forward, remove-forward',
|
|
24
|
+
};
|
|
25
|
+
const supportedTargetsText = `AI App Bridge supports ${supportedTargets.join('; ')}.`;
|
|
26
|
+
const commandDomainsText = `Command domains: ${Object.entries(commandDomains).map(([domain, summary]) => `${domain}(${summary})`).join('; ')}.`;
|
|
27
|
+
const discoveryText = 'Discover domains, commands and argument schemas with capabilities, then run the selected command with its arguments.';
|
|
28
|
+
function capabilityPayload(args = {}) {
|
|
29
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) return { ok: false, error: 'invalid_argument', field: 'arguments', dispatched: false, ambiguous: false };
|
|
30
|
+
const invalidKey = Object.keys(args).find(key => !['command', 'domain', 'includeOptions'].includes(key));
|
|
31
|
+
if (invalidKey) return { ok: false, error: 'unsupported_argument', field: invalidKey, dispatched: false, ambiguous: false };
|
|
32
|
+
for (const [key, type] of [['command', 'string'], ['domain', 'string'], ['includeOptions', 'boolean']]) {
|
|
33
|
+
if (Object.hasOwn(args, key) && typeof args[key] !== type) return { ok: false, error: 'invalid_argument', field: key, dispatched: false, ambiguous: false };
|
|
34
|
+
}
|
|
35
|
+
if (args.command && args.domain) return { ok: false, error: 'invalid_argument', field: 'domain', message: 'Use command or domain, not both.', dispatched: false, ambiguous: false };
|
|
36
|
+
const includeOptions = args.includeOptions === true;
|
|
37
|
+
const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
|
|
38
|
+
if (requestedCommand) {
|
|
39
|
+
const definition = commandByName.get(requestedCommand) || isolatedByName.get(requestedCommand);
|
|
40
|
+
return {
|
|
41
|
+
ok: Boolean(definition),
|
|
42
|
+
command: requestedCommand,
|
|
43
|
+
...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const requestedDomain = args.domain ? String(args.domain) : '';
|
|
48
|
+
const domains = {};
|
|
49
|
+
if (requestedDomain && !Object.hasOwn(commandDomains, requestedDomain)) return { ok: false, error: 'unknown_domain', field: 'domain', dispatched: false, ambiguous: false };
|
|
50
|
+
for (const definition of [...isolatedCommandDefinitions, ...commandDefinitions]) {
|
|
51
|
+
if (requestedDomain && definition.domain !== requestedDomain) continue;
|
|
52
|
+
if (!domains[definition.domain]) domains[definition.domain] = [];
|
|
53
|
+
domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
surface: 'compact',
|
|
59
|
+
supportedTargets,
|
|
60
|
+
commandDomains,
|
|
61
|
+
usage: `${supportedTargetsText} ${discoveryText} Use run with one of these command names. Prefer packageName for Android app commands, bundleId/deviceId for iOS, and sessionId/targetId for Web Bridge sessions.`,
|
|
62
|
+
domains,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function shapeCommandDefinition(definition, includeOptions) {
|
|
67
|
+
const shaped = {
|
|
68
|
+
command: definition.command,
|
|
69
|
+
summary: definition.summary,
|
|
70
|
+
targetApp: Boolean(definition.targetApp),
|
|
71
|
+
...commandContract(definition.command),
|
|
72
|
+
...(includeOptions ? { options: Object.keys(commandSchema(definition.command).properties), inputSchema: commandSchema(definition.command) } : {}),
|
|
73
|
+
contractVersion: 'aab.command/v1',
|
|
74
|
+
};
|
|
75
|
+
if (definition.command === 'script') {
|
|
76
|
+
const { catalogPayload } = require('./script/script-catalog');
|
|
77
|
+
shaped.runtime = 'trusted-local-code';
|
|
78
|
+
shaped.warning = 'Script source is trusted-local-code, not an OS sandbox.';
|
|
79
|
+
shaped.catalog = catalogPayload();
|
|
80
|
+
}
|
|
81
|
+
return shaped;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
function normalizeCommandName(value) { return typeof value === 'string' ? value : ''; }
|
|
86
|
+
module.exports = { capabilities: capabilityPayload, commandDomains, supportedTargets, supportedTargetsText, commandDomainsText, discoveryText };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `error` is a stable machine-readable code; `message` is for the caller.
|
|
4
|
+
// Unknown dispatch state stays null. Only the rejecting/dispatching layer can
|
|
5
|
+
// claim that an action was not sent.
|
|
6
|
+
class CommandError extends Error {
|
|
7
|
+
constructor(code, message, { field, details, dispatched = false, ambiguous = false, settled, executionReceipt, executionReceipts, exitCode } = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.field = field;
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.dispatched = dispatched;
|
|
13
|
+
this.ambiguous = ambiguous;
|
|
14
|
+
for (const [name, value] of Object.entries({ settled, executionReceipt, executionReceipts, exitCode })) {
|
|
15
|
+
if (value !== undefined) this[name] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function commandFailure(error, command) {
|
|
21
|
+
const known = error instanceof CommandError;
|
|
22
|
+
const code = typeof error?.code === 'string' && /^[a-z][a-z0-9_]*$/.test(error.code)
|
|
23
|
+
? error.code : 'command_failed';
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: code,
|
|
27
|
+
message: error?.message || String(error),
|
|
28
|
+
...(command ? { command } : {}),
|
|
29
|
+
...(error?.field !== undefined ? { field: error.field } : {}),
|
|
30
|
+
...(error?.details !== undefined ? { details: error.details } : {}),
|
|
31
|
+
dispatched: typeof error?.dispatched === 'boolean' ? error.dispatched : null,
|
|
32
|
+
ambiguous: known ? error.ambiguous : error?.ambiguous !== false,
|
|
33
|
+
...executionFields(error),
|
|
34
|
+
...(error?._feedback ? { _feedback: error._feedback } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function executionFields(value) {
|
|
39
|
+
return Object.fromEntries(['settled', 'dispatched', 'ambiguous', 'executionReceipt', 'executionReceipts', 'exitCode']
|
|
40
|
+
.filter(name => value?.[name] !== undefined).map(name => [name, value[name]]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeCommandResult(result, command) {
|
|
44
|
+
if (!result || typeof result !== 'object' || Buffer.isBuffer(result) || Array.isArray(result)) return result;
|
|
45
|
+
if (result.ok !== false && result.error == null) return result;
|
|
46
|
+
const original = typeof result.error === 'object' ? result.error?.code : result.error;
|
|
47
|
+
const code = typeof original === 'string' && /^[a-z][a-z0-9_]*$/.test(original)
|
|
48
|
+
? original : 'command_failed';
|
|
49
|
+
return {
|
|
50
|
+
...result,
|
|
51
|
+
ok: false,
|
|
52
|
+
error: code,
|
|
53
|
+
message: result.message || result.error?.message || (typeof original === 'string' ? original : 'Command failed without an error reason.'),
|
|
54
|
+
...(command ? { command } : {}),
|
|
55
|
+
dispatched: typeof result.dispatched === 'boolean' ? result.dispatched : null,
|
|
56
|
+
ambiguous: typeof result.ambiguous === 'boolean' ? result.ambiguous : result.dispatched !== false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { CommandError, commandFailure, normalizeCommandResult, executionFields };
|