@deeeed/metamask-harness 0.17.5 → 0.18.0
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/CHANGELOG.md +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +90 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +2 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
package/dist/commands/call.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
6
5
|
import { importRecipeProtocol } from "../paths.js";
|
|
7
6
|
import { recipeRunning } from "../heal-bounds.js";
|
|
8
7
|
import { color } from "../cli-color.js";
|
|
9
|
-
import { EXIT } from "./shared.js";
|
|
8
|
+
import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
10
9
|
import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
|
|
11
10
|
import {
|
|
12
11
|
parseArgs,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
optionString,
|
|
16
15
|
resolveAdapter,
|
|
17
16
|
runtimeOptionsFromCli,
|
|
17
|
+
shellQuote,
|
|
18
18
|
usageError
|
|
19
19
|
} from "./parse-args.js";
|
|
20
20
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
@@ -27,11 +27,14 @@ import {
|
|
|
27
27
|
prepareHeal,
|
|
28
28
|
recoverRunInfra,
|
|
29
29
|
resolveMetaMaskLibrarySources,
|
|
30
|
+
preflightRecipe,
|
|
30
31
|
runRecipe,
|
|
31
32
|
synthesizeOneNodeRecipe,
|
|
32
33
|
validateRecipeAdapterAware
|
|
33
34
|
} from "./run-engine.js";
|
|
34
35
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
36
|
+
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
37
|
+
import { recipeTrustFailure } from "../recipe-security.js";
|
|
35
38
|
async function handleCall(argv) {
|
|
36
39
|
if (argv.includes("--list")) {
|
|
37
40
|
const { options: options2 } = parseArgs(argv, "call");
|
|
@@ -50,7 +53,8 @@ async function handleCall(argv) {
|
|
|
50
53
|
let discovery = "mm-harness actions";
|
|
51
54
|
try {
|
|
52
55
|
const { adapter: adapter2 } = resolveAdapter(options);
|
|
53
|
-
const
|
|
56
|
+
const librarySources2 = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
57
|
+
const { manifest: manifest2 } = await resolveActionManifest(adapter2, optionString(options, "actionManifest"), librarySources2);
|
|
54
58
|
const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
|
|
55
59
|
const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
|
|
56
60
|
example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
|
|
@@ -59,44 +63,65 @@ async function handleCall(argv) {
|
|
|
59
63
|
}
|
|
60
64
|
const message = `call requires <action>. Example: ${example}`;
|
|
61
65
|
const userAction = `${example} # see the vocabulary: ${discovery}`;
|
|
62
|
-
if (json) console.log(JSON.stringify({
|
|
66
|
+
if (json) console.log(JSON.stringify({
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
command: "call",
|
|
69
|
+
status: "fail",
|
|
70
|
+
error: { code: "CLI_MISSING_POSITIONAL", message, userAction },
|
|
71
|
+
exitCode: EXIT.usage
|
|
72
|
+
}, null, 2));
|
|
63
73
|
else console.error(`${message}
|
|
64
74
|
See the vocabulary: ${discovery}`);
|
|
65
75
|
return EXIT.usage;
|
|
66
76
|
}
|
|
67
77
|
const { adapter, target } = resolveAdapter(options);
|
|
78
|
+
if (!fs.existsSync(target)) {
|
|
79
|
+
return usageOut(
|
|
80
|
+
json,
|
|
81
|
+
"call",
|
|
82
|
+
`target does not exist: ${target}`,
|
|
83
|
+
"pass --target <metamask-checkout> pointing to an existing checkout"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
68
86
|
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
69
87
|
const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
|
|
70
88
|
if ("code" in dtResult) {
|
|
71
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
72
|
-
else console.error(`\u2717 call: ${dtResult.message}
|
|
89
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
|
|
90
|
+
else console.error(`\u2717 call: ${dtResult.message}
|
|
91
|
+
Next: ${dtResult.userAction}`);
|
|
73
92
|
return EXIT.usage;
|
|
74
93
|
}
|
|
75
94
|
if (recipeRunning(target)) {
|
|
76
95
|
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
96
|
+
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
|
|
77
97
|
if (json) {
|
|
78
|
-
console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
|
|
98
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
|
|
79
99
|
} else {
|
|
80
|
-
console.error(`\u2717 mm-harness call: ${msg}
|
|
100
|
+
console.error(`\u2717 mm-harness call: ${msg}
|
|
101
|
+
Next: ${userAction}`);
|
|
81
102
|
}
|
|
82
103
|
return EXIT.bounded;
|
|
83
104
|
}
|
|
84
105
|
const actionManifestOverride = optionString(options, "actionManifest");
|
|
85
|
-
const
|
|
106
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
107
|
+
const { manifest } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
|
|
86
108
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
87
109
|
const names = getRecipeActionManifestActionNames(manifest);
|
|
88
110
|
const resolution = resolveActionName(shortName, names);
|
|
89
111
|
if (resolution.status === "unknown") {
|
|
90
|
-
const message =
|
|
91
|
-
|
|
92
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message } }, null, 2));
|
|
93
|
-
else console.error(message
|
|
112
|
+
const message = `unknown action "${shortName}" for the ${adapter} adapter.`;
|
|
113
|
+
const userAction = `mm-harness actions --adapter ${adapter} --json`;
|
|
114
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
|
|
115
|
+
else console.error(`\u2717 call: ${message}
|
|
116
|
+
Next: ${userAction}`);
|
|
94
117
|
return EXIT.usage;
|
|
95
118
|
}
|
|
96
119
|
if (resolution.status === "ambiguous") {
|
|
97
|
-
const message =
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
const message = `"${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
|
|
121
|
+
const userAction = `mm-harness actions --action ${resolution.candidates[0]} --adapter ${adapter} --json`;
|
|
122
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates, userAction } }, null, 2));
|
|
123
|
+
else console.error(`\u2717 call: ${message}
|
|
124
|
+
Next: ${userAction}`);
|
|
100
125
|
return EXIT.usage;
|
|
101
126
|
}
|
|
102
127
|
const resolvedAction = resolution.resolved;
|
|
@@ -113,19 +138,74 @@ async function handleCall(argv) {
|
|
|
113
138
|
const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
|
|
114
139
|
const validation = await validateRecipeAdapterAware(recipe, manifest);
|
|
115
140
|
if (validation.status === "invalid") {
|
|
116
|
-
const message =
|
|
117
|
-
|
|
141
|
+
const message = `call ${resolvedAction}: recipe validation failed`;
|
|
142
|
+
const userAction = `mm-harness actions --action ${resolvedAction} --adapter ${adapter} --json`;
|
|
143
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
|
|
118
144
|
else {
|
|
119
|
-
console.error(message);
|
|
145
|
+
console.error(`\u2717 ${message}`);
|
|
120
146
|
for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
147
|
+
console.error(` Next: ${userAction}`);
|
|
148
|
+
}
|
|
149
|
+
return EXIT.validation;
|
|
150
|
+
}
|
|
151
|
+
const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction, args);
|
|
152
|
+
const requestedRuntimeOptions = runtimeOptionsFromCli(options);
|
|
153
|
+
const callRuntimeOptions = {
|
|
154
|
+
...requestedRuntimeOptions,
|
|
155
|
+
...librarySources ? { librarySources } : {},
|
|
156
|
+
autoHud: false,
|
|
157
|
+
stdoutIsMachineContract: json,
|
|
158
|
+
source: requestedRuntimeOptions.source ?? {
|
|
159
|
+
kind: "operator",
|
|
160
|
+
trust: "trusted",
|
|
161
|
+
name: "mm-harness call"
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let preflightedExecution;
|
|
165
|
+
try {
|
|
166
|
+
preflightedExecution = await preflightRecipe(
|
|
167
|
+
adapter,
|
|
168
|
+
recipe,
|
|
169
|
+
artifactsDir,
|
|
170
|
+
target,
|
|
171
|
+
actionManifestOverride,
|
|
172
|
+
callRuntimeOptions
|
|
173
|
+
);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const trustFailure = recipeTrustFailure(error);
|
|
176
|
+
if (!trustFailure) throw error;
|
|
177
|
+
const planDigest = trustFailure.details?.recipeDigest;
|
|
178
|
+
if (planDigest) {
|
|
179
|
+
trustFailure.userAction = `review the plan, then rerun this call with --artifacts-dir ${shellQuote(artifactsDir)} --approve-plan ${shellQuote(planDigest)}; managed callers must keep the same artifact directory and execution environment, then set FARMSLOT_RECIPE_APPROVE_PLAN=${shellQuote(planDigest)}`;
|
|
180
|
+
}
|
|
181
|
+
if (json) {
|
|
182
|
+
console.log(JSON.stringify({
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
command: "call",
|
|
185
|
+
status: "fail",
|
|
186
|
+
error: trustFailure,
|
|
187
|
+
exitCode: EXIT.validation
|
|
188
|
+
}, null, 2));
|
|
189
|
+
} else {
|
|
190
|
+
console.error(`\u2717 mm-harness call: ${trustFailure.message}`);
|
|
191
|
+
const blocked = trustFailure.details?.blocked;
|
|
192
|
+
if (blocked?.length) {
|
|
193
|
+
console.error(" Restricted plan nodes:");
|
|
194
|
+
for (const node of blocked.slice(0, 10)) {
|
|
195
|
+
const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
|
|
196
|
+
console.error(
|
|
197
|
+
` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
|
|
201
|
+
}
|
|
202
|
+
console.error(` Next: ${trustFailure.userAction}`);
|
|
121
203
|
}
|
|
122
204
|
return EXIT.validation;
|
|
123
205
|
}
|
|
124
206
|
const lock = acquireCheckoutLock(target, "call");
|
|
125
207
|
if ("message" in lock) {
|
|
126
|
-
|
|
127
|
-
else console.error(`\u2717 mm-harness call: ${lock.message}`);
|
|
128
|
-
return EXIT.bounded;
|
|
208
|
+
return checkoutBusyOut(json, "call", lock.message, lock.path);
|
|
129
209
|
}
|
|
130
210
|
try {
|
|
131
211
|
const prepared = await prepareHeal(adapter, target, options, json, {
|
|
@@ -133,19 +213,20 @@ async function handleCall(argv) {
|
|
|
133
213
|
});
|
|
134
214
|
if (typeof prepared === "number") return prepared;
|
|
135
215
|
const { state, heal } = prepared;
|
|
136
|
-
const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
|
|
137
|
-
const recipeFile = path.join(scratch, "call.recipe.json");
|
|
138
|
-
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
139
|
-
`);
|
|
140
|
-
const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
|
|
141
|
-
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
142
|
-
const callRuntimeOptions = {
|
|
143
|
-
...runtimeOptionsFromCli(options),
|
|
144
|
-
...librarySources ? { librarySources } : {},
|
|
145
|
-
stdoutIsMachineContract: json
|
|
146
|
-
};
|
|
147
216
|
const { result, violation } = await executeWithHealBounds(
|
|
148
|
-
() =>
|
|
217
|
+
() => {
|
|
218
|
+
const execution = preflightedExecution;
|
|
219
|
+
preflightedExecution = void 0;
|
|
220
|
+
return runRecipe(
|
|
221
|
+
adapter,
|
|
222
|
+
recipe,
|
|
223
|
+
artifactsDir,
|
|
224
|
+
target,
|
|
225
|
+
actionManifestOverride,
|
|
226
|
+
callRuntimeOptions,
|
|
227
|
+
execution
|
|
228
|
+
);
|
|
229
|
+
},
|
|
149
230
|
adapter,
|
|
150
231
|
target,
|
|
151
232
|
heal,
|
|
@@ -154,6 +235,9 @@ async function handleCall(argv) {
|
|
|
154
235
|
);
|
|
155
236
|
if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
|
|
156
237
|
const callOutput = readCallOutput(result.tracePath);
|
|
238
|
+
const safeArgs = redactCallValue(args);
|
|
239
|
+
const safeCallOutput = redactCallValue(callOutput);
|
|
240
|
+
const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
|
|
157
241
|
if (json) {
|
|
158
242
|
console.log(
|
|
159
243
|
JSON.stringify(
|
|
@@ -163,30 +247,41 @@ async function handleCall(argv) {
|
|
|
163
247
|
adapter,
|
|
164
248
|
action: shortName,
|
|
165
249
|
resolvedAction,
|
|
166
|
-
args,
|
|
250
|
+
args: safeArgs,
|
|
167
251
|
status: result.status,
|
|
168
252
|
summaryPath: result.summaryPath,
|
|
169
253
|
tracePath: result.tracePath,
|
|
170
254
|
artifactManifestPath: result.artifactManifestPath,
|
|
171
|
-
...
|
|
255
|
+
...result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {},
|
|
256
|
+
...result.sideFindings ? { sideFindings: result.sideFindings } : {},
|
|
257
|
+
...callOutput !== void 0 ? { output: safeCallOutput } : {},
|
|
172
258
|
recovered: state.recovered,
|
|
173
259
|
mutations: state.mutations,
|
|
174
|
-
exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
|
|
260
|
+
exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
261
|
+
...result.status === "fail" ? { error: { code: "ACTION_EXECUTION_FAILED", message: `${resolvedAction} failed; inspect the persisted result and evidence paths`, userAction: failureUserAction } } : {}
|
|
175
262
|
},
|
|
176
263
|
null,
|
|
177
264
|
2
|
|
178
265
|
)
|
|
179
266
|
);
|
|
180
267
|
} else {
|
|
181
|
-
const
|
|
182
|
-
Result:
|
|
183
|
-
${formatCallOutput(callOutput)}` : "";
|
|
268
|
+
const renderedInputs = Object.keys(args).length > 0 ? formatCallOutput(safeArgs) : "";
|
|
184
269
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
270
|
+
const diagnostics = formatRunDiagnosticsForHuman(
|
|
271
|
+
readRunDiagnosticsDocument(result.diagnosticsPath),
|
|
272
|
+
adapter
|
|
273
|
+
);
|
|
274
|
+
const renderedDiagnostics = `${out("label", "Diagnostics:")}
|
|
275
|
+
${diagnostics.map((line) => ` ${line}`).join("\n")}
|
|
276
|
+
`;
|
|
185
277
|
console.log(
|
|
186
|
-
`${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${
|
|
278
|
+
`${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${renderedInputs ? `
|
|
279
|
+
${out("label", "Inputs:")}
|
|
280
|
+
${renderedInputs}` : ""}${callOutput !== void 0 ? `
|
|
187
281
|
${out("label", "Result:")}
|
|
188
|
-
${formatCallOutput(
|
|
189
|
-
|
|
282
|
+
${formatCallOutput(safeCallOutput)}` : ""}
|
|
283
|
+
` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
|
|
284
|
+
Next: ${failureUserAction}` : "")
|
|
190
285
|
);
|
|
191
286
|
}
|
|
192
287
|
return result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
@@ -194,6 +289,11 @@ ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
|
|
|
194
289
|
lock.release();
|
|
195
290
|
}
|
|
196
291
|
}
|
|
292
|
+
function defaultCallArtifactsDir(target, action, args) {
|
|
293
|
+
const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
|
|
294
|
+
const digest = createHash("sha256").update(JSON.stringify({ action, args })).digest("hex").slice(0, 12);
|
|
295
|
+
return path.join(target, "temp", "recipe", "calls", `${actionStem}-${digest}`);
|
|
296
|
+
}
|
|
197
297
|
function readCallOutput(tracePath) {
|
|
198
298
|
try {
|
|
199
299
|
const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
|
|
@@ -209,20 +309,32 @@ function formatCallOutput(value) {
|
|
|
209
309
|
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
210
310
|
return JSON.stringify(value, null, 2);
|
|
211
311
|
}
|
|
312
|
+
function redactCallValue(value, key = "") {
|
|
313
|
+
if (/(?:password|secret|mnemonic|seed|srp|vault|private[_-]?key)/iu.test(key)) return "<redacted>";
|
|
314
|
+
if (Array.isArray(value)) return value.map((entry) => redactCallValue(entry));
|
|
315
|
+
if (isRecord(value)) {
|
|
316
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactCallValue(entry, entryKey)]));
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
212
320
|
async function handleCallHelp(argv, genericHelp) {
|
|
213
321
|
const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
|
|
214
322
|
const { options } = parseArgs(rest, "call");
|
|
215
323
|
let adapter;
|
|
216
324
|
let manifest;
|
|
325
|
+
let actionSources = /* @__PURE__ */ new Map();
|
|
217
326
|
try {
|
|
218
327
|
({ adapter } = resolveAdapter(options));
|
|
219
|
-
|
|
328
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
329
|
+
const resolution = await resolveActionManifest(adapter, optionString(options, "actionManifest"), librarySources);
|
|
330
|
+
manifest = resolution.manifest;
|
|
331
|
+
actionSources = resolution.actionSources;
|
|
220
332
|
} catch {
|
|
221
333
|
process.stdout.write(`${genericHelp}
|
|
222
334
|
`);
|
|
223
335
|
return EXIT.ok;
|
|
224
336
|
}
|
|
225
|
-
const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest), shortName) : [];
|
|
337
|
+
const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest, actionSources), shortName) : [];
|
|
226
338
|
if (matches.length === 0) {
|
|
227
339
|
process.stdout.write(`${genericHelp}
|
|
228
340
|
`);
|
|
@@ -250,7 +362,8 @@ function renderCallActionHelp(entry) {
|
|
|
250
362
|
);
|
|
251
363
|
const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
|
|
252
364
|
if (entry.description) lines.push(` ${entry.description}`, "");
|
|
253
|
-
|
|
365
|
+
lines.push(` Source: ${entry.source}${entry.sourceManifest ? ` (${entry.sourceManifest})` : ""}`, "");
|
|
366
|
+
const names = Object.keys(properties).filter((name) => name !== "action" && name !== "next").sort();
|
|
254
367
|
if (names.length === 0) {
|
|
255
368
|
lines.push(" Fields: (none)");
|
|
256
369
|
} else {
|
|
@@ -320,7 +433,7 @@ function parseCallValue(value) {
|
|
|
320
433
|
const number = Number(value);
|
|
321
434
|
if (Number.isFinite(number)) return number;
|
|
322
435
|
}
|
|
323
|
-
if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
|
|
436
|
+
if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}") || value.startsWith('"') && value.endsWith('"')) {
|
|
324
437
|
try {
|
|
325
438
|
return JSON.parse(value);
|
|
326
439
|
} catch {
|
|
@@ -340,6 +453,7 @@ function isForwardedOptionValue(argv, index) {
|
|
|
340
453
|
}
|
|
341
454
|
const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
|
|
342
455
|
"action-manifest",
|
|
456
|
+
"approve-plan",
|
|
343
457
|
"adapter",
|
|
344
458
|
"artifacts-dir",
|
|
345
459
|
"cdp-port",
|
|
@@ -350,6 +464,10 @@ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
350
464
|
"platform",
|
|
351
465
|
"project-root",
|
|
352
466
|
"slot",
|
|
467
|
+
"source-digest",
|
|
468
|
+
"source-kind",
|
|
469
|
+
"source-name",
|
|
470
|
+
"source-trust",
|
|
353
471
|
"target",
|
|
354
472
|
"validation-runtime-dir",
|
|
355
473
|
"watcher-port"
|
|
@@ -373,6 +491,8 @@ function pickCallExampleAction(names) {
|
|
|
373
491
|
return names[0] ?? "command";
|
|
374
492
|
}
|
|
375
493
|
export {
|
|
494
|
+
defaultCallArtifactsDir,
|
|
376
495
|
handleCall,
|
|
377
|
-
handleCallHelp
|
|
496
|
+
handleCallHelp,
|
|
497
|
+
redactCallValue
|
|
378
498
|
};
|
package/dist/commands/check.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { detectAdapter } from "../harness.js";
|
|
5
5
|
import { EXIT, usageOut } from "./shared.js";
|
|
6
|
-
import { optionFlag, optionString, parseArgs } from "./parse-args.js";
|
|
6
|
+
import { optionFlag, optionString, parseArgs, shellQuote } from "./parse-args.js";
|
|
7
7
|
const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
|
|
8
8
|
const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
|
|
9
9
|
const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
|
|
@@ -111,6 +111,7 @@ async function handleCheck(argv) {
|
|
|
111
111
|
const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
|
|
112
112
|
const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
|
|
113
113
|
const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === "eslint" && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
|
|
114
|
+
const failureUserAction = guidance[0] ?? `inspect ${path.join(artifactsDir, "validation-summary.json")}, fix the failing check, then retry: mm-harness check diff --target ${shellQuote(target)} --base ${shellQuote(baseRef)} --profile ${profile} --json`;
|
|
114
115
|
const summary = {
|
|
115
116
|
schemaVersion: 1,
|
|
116
117
|
command: "check",
|
|
@@ -129,6 +130,7 @@ async function handleCheck(argv) {
|
|
|
129
130
|
fixes,
|
|
130
131
|
checks,
|
|
131
132
|
guidance,
|
|
133
|
+
...status === "fail" ? { error: { code: "CHECK_DIFF_FAILED", message: "one or more bounded checks failed", userAction: failureUserAction } } : {},
|
|
132
134
|
artifacts: {
|
|
133
135
|
dir: artifactsDir,
|
|
134
136
|
summaryJson: path.join(artifactsDir, "validation-summary.json"),
|
|
@@ -518,10 +520,14 @@ function renderHuman(summary) {
|
|
|
518
520
|
if (check.status === "fail" && check.logPath) console.log(` log: ${check.logPath}`);
|
|
519
521
|
}
|
|
520
522
|
for (const item of summary.guidance) console.log(` Next: ${item}`);
|
|
523
|
+
if (summary.guidance.length === 0 && summary.status === "fail" && summary.error?.userAction) {
|
|
524
|
+
console.log(` Next: ${summary.error.userAction}`);
|
|
525
|
+
}
|
|
521
526
|
console.log(`Artifacts: ${summary.artifacts.summaryJson}`);
|
|
522
527
|
}
|
|
523
528
|
function failEnvelope(input) {
|
|
524
529
|
const exitCode = EXIT.usage;
|
|
530
|
+
const userAction = USAGE;
|
|
525
531
|
const body = {
|
|
526
532
|
schemaVersion: 1,
|
|
527
533
|
command: "check",
|
|
@@ -534,11 +540,11 @@ function failEnvelope(input) {
|
|
|
534
540
|
baseRef: input.baseRef,
|
|
535
541
|
baseSource: "unknown",
|
|
536
542
|
artifacts: { dir: input.artifactsDir },
|
|
537
|
-
error: { code: input.code, message: input.message }
|
|
543
|
+
error: { code: input.code, message: input.message, userAction }
|
|
538
544
|
};
|
|
539
545
|
if (input.json) console.log(JSON.stringify(body, null, 2));
|
|
540
546
|
else console.error(`\u2717 check diff: ${input.message}
|
|
541
|
-
Next: ${
|
|
547
|
+
Next: ${userAction}`);
|
|
542
548
|
return exitCode;
|
|
543
549
|
}
|
|
544
550
|
export {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
2
2
|
import {
|
|
3
3
|
invalidateCompletionCache,
|
|
4
4
|
readFreshCandidates,
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
resolveAdapter,
|
|
14
14
|
targetPath
|
|
15
15
|
} from "./parse-args.js";
|
|
16
|
-
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
16
|
+
import { listRunnableRecipes, resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
17
17
|
async function handleCompletionCandidates(argv) {
|
|
18
18
|
const kind = argv[0];
|
|
19
19
|
const { options } = parseArgs(argv.slice(1), "completion-candidates");
|
|
20
20
|
const json = optionFlag(options, "json");
|
|
21
|
-
if (kind !== "actions" && kind !== "flows") {
|
|
22
|
-
console.error("completion-candidates requires <actions|flows>.");
|
|
21
|
+
if (kind !== "actions" && kind !== "flows" && kind !== "recipes") {
|
|
22
|
+
console.error("completion-candidates requires <actions|flows|recipes>.");
|
|
23
23
|
return EXIT.usage;
|
|
24
24
|
}
|
|
25
25
|
const target = targetPath(options);
|
|
@@ -27,17 +27,15 @@ async function handleCompletionCandidates(argv) {
|
|
|
27
27
|
let candidates;
|
|
28
28
|
if (kind === "actions") {
|
|
29
29
|
adapter = resolveAdapter(options).adapter;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
30
|
+
const sources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
31
|
+
const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
|
|
32
|
+
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
33
|
+
candidates = getRecipeActionManifestActionNames(manifest);
|
|
34
|
+
writeCompletionCandidates(target, "actions", candidates);
|
|
35
|
+
} else if (kind === "flows") {
|
|
38
36
|
candidates = readFreshCandidates(target, "flows");
|
|
39
37
|
if (!candidates) {
|
|
40
|
-
const sources = await resolveMetaMaskLibrarySources(
|
|
38
|
+
const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
|
|
41
39
|
if (!sources) {
|
|
42
40
|
candidates = [];
|
|
43
41
|
} else {
|
|
@@ -47,6 +45,15 @@ async function handleCompletionCandidates(argv) {
|
|
|
47
45
|
writeCompletionCandidates(target, "flows", candidates);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
48
|
+
} else {
|
|
49
|
+
adapter = resolveAdapter(options).adapter;
|
|
50
|
+
const cacheKey = `recipes:${adapter}`;
|
|
51
|
+
candidates = readFreshCandidates(target, cacheKey);
|
|
52
|
+
if (!candidates) {
|
|
53
|
+
const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
|
|
54
|
+
candidates = sources ? listRunnableRecipes(adapter, sources).map((recipe) => recipe.name) : [];
|
|
55
|
+
writeCompletionCandidates(target, cacheKey, candidates);
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
if (json) console.log(JSON.stringify({ schemaVersion: 1, kind, adapter, candidates }, null, 2));
|
|
52
59
|
else for (const candidate of candidates) console.log(candidate);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { listConnectedDevices } from "../devices.js";
|
|
3
|
-
import { optionString } from "./parse-args.js";
|
|
3
|
+
import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
|
|
4
4
|
function normalizeDeviceName(value) {
|
|
5
5
|
return value.replace(/_/gu, " ").trim();
|
|
6
6
|
}
|
|
@@ -75,6 +75,12 @@ function configuredPinForPlatform(platform) {
|
|
|
75
75
|
function oppositePlatform(platform) {
|
|
76
76
|
return platform === "ios" ? "android" : "ios";
|
|
77
77
|
}
|
|
78
|
+
function deviceProbe(options) {
|
|
79
|
+
return `mm-harness status --target ${shellQuoteArg(targetPath(options))} --all-devices --json`;
|
|
80
|
+
}
|
|
81
|
+
function deviceRecovery(options, rerun) {
|
|
82
|
+
return rerun ? `${deviceProbe(options)}; then retry: ${rerun}` : deviceProbe(options);
|
|
83
|
+
}
|
|
78
84
|
function applyDeviceTargeting(command, adapter, options, opts) {
|
|
79
85
|
const device = optionString(options, "device");
|
|
80
86
|
const platformPreference = mobilePlatformPreference(options);
|
|
@@ -84,7 +90,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
84
90
|
ok: false,
|
|
85
91
|
code: "DEVICE_WRONG_ADAPTER",
|
|
86
92
|
message: `--device is only supported on the mobile adapter (the ${adapter} adapter has no device to target).
|
|
87
|
-
Drop --device, or run this ${command} inside a metamask-mobile checkout
|
|
93
|
+
Drop --device, or run this ${command} inside a metamask-mobile checkout.`,
|
|
94
|
+
userAction: `remove --device; ${adapter} has no device target`
|
|
88
95
|
};
|
|
89
96
|
}
|
|
90
97
|
return { ok: true };
|
|
@@ -97,7 +104,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
97
104
|
ok: false,
|
|
98
105
|
code: "DEVICE_PLATFORM_CONFLICT",
|
|
99
106
|
message: `--device ${device} resolves to ${matched.platform}, but --platform ${platformPreference} was requested.
|
|
100
|
-
To use this device intentionally, drop --platform ${platformPreference} or choose a ${platformPreference} device
|
|
107
|
+
To use this device intentionally, drop --platform ${platformPreference} or choose a ${platformPreference} device.`,
|
|
108
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
101
109
|
};
|
|
102
110
|
}
|
|
103
111
|
if (matched.platform === "android") setAndroidDeviceEnv(matched.id, matched.name);
|
|
@@ -126,7 +134,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
126
134
|
ok: false,
|
|
127
135
|
code: "DEVICE_NAME_AMBIGUOUS",
|
|
128
136
|
message: `device name '${device}' is ambiguous; use the id:
|
|
129
|
-
` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n")
|
|
137
|
+
` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n"),
|
|
138
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
130
139
|
};
|
|
131
140
|
}
|
|
132
141
|
return {
|
|
@@ -134,7 +143,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
134
143
|
code: "DEVICE_NOT_FOUND",
|
|
135
144
|
message: `--device ${device} did not match any connected device.
|
|
136
145
|
` + (connected2.length > 0 ? ` Connected devices:
|
|
137
|
-
${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
|
|
146
|
+
${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`),
|
|
147
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
138
148
|
};
|
|
139
149
|
}
|
|
140
150
|
const connected = listConnectedDevices();
|
|
@@ -158,7 +168,8 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
|
|
|
158
168
|
message: `--platform ${platformPreference} conflicts with the current slot-selected device.
|
|
159
169
|
Selected devices:
|
|
160
170
|
${formatConnectedDevices(selectedTargetable2)}
|
|
161
|
-
To use a different connected device intentionally, pass --device <id
|
|
171
|
+
To use a different connected device intentionally, pass --device <id>.`,
|
|
172
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
162
173
|
};
|
|
163
174
|
}
|
|
164
175
|
if (!configuredPin && oppositePin) {
|
|
@@ -167,7 +178,8 @@ ${formatConnectedDevices(selectedTargetable2)}
|
|
|
167
178
|
code: "DEVICE_PLATFORM_CONFLICT",
|
|
168
179
|
message: `--platform ${platformPreference} conflicts with the configured slot target.
|
|
169
180
|
Configured ${oppositePlatform(platformPreference)} target: ${oppositePin}
|
|
170
|
-
To use a different connected device intentionally, pass --device <id
|
|
181
|
+
To use a different connected device intentionally, pass --device <id>.`,
|
|
182
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
171
183
|
};
|
|
172
184
|
}
|
|
173
185
|
if (configuredPin) {
|
|
@@ -178,8 +190,8 @@ ${formatConnectedDevices(selectedTargetable2)}
|
|
|
178
190
|
message: `--platform ${platformPreference} matches the configured slot target, but that target is not ready.
|
|
179
191
|
Configured target: ${configuredPin}
|
|
180
192
|
Connected devices:
|
|
181
|
-
${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}
|
|
182
|
-
|
|
193
|
+
${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}`,
|
|
194
|
+
userAction: `mm-harness launch ${platformPreference} --target ${shellQuoteArg(targetPath(options))}`
|
|
183
195
|
};
|
|
184
196
|
}
|
|
185
197
|
if (platformTargetable.length === 1) {
|
|
@@ -195,7 +207,8 @@ ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}
|
|
|
195
207
|
code: "DEVICE_PLATFORM_NOT_FOUND",
|
|
196
208
|
message: `--platform ${platformPreference} did not match any ready connected device.
|
|
197
209
|
` + (connected.length > 0 ? ` Connected devices:
|
|
198
|
-
${formatConnectedDevices(connected)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
|
|
210
|
+
${formatConnectedDevices(connected)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`),
|
|
211
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
199
212
|
};
|
|
200
213
|
}
|
|
201
214
|
return {
|
|
@@ -205,7 +218,8 @@ ${formatConnectedDevices(connected)}` : ` No devices are currently connected (a
|
|
|
205
218
|
Connected devices:
|
|
206
219
|
${formatConnectedDevices(connected)}
|
|
207
220
|
Add --device <id> to disambiguate, e.g.:${platformTargetable.map((d) => `
|
|
208
|
-
--device ${d.id}`).join("")}
|
|
221
|
+
--device ${d.id}`).join("")}`,
|
|
222
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
209
223
|
};
|
|
210
224
|
}
|
|
211
225
|
const selectedTargetable = targetable.filter(deviceSelected);
|
|
@@ -224,7 +238,8 @@ ${formatConnectedDevices(connected)}
|
|
|
224
238
|
Connected devices:
|
|
225
239
|
${formatConnectedDevices(connected)}
|
|
226
240
|
` + (selectedTargetable.length > 1 ? ` The current slot context selects more than one target; fix the slot device pins or add --device <id>.` : ` Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
|
|
227
|
-
--device ${d.id}`).join("")}`)
|
|
241
|
+
--device ${d.id}`).join("")}`),
|
|
242
|
+
userAction: deviceRecovery(options, opts.rerun)
|
|
228
243
|
};
|
|
229
244
|
}
|
|
230
245
|
return { ok: true };
|