@deeeed/metamask-harness 0.42.0 → 0.44.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 +42 -0
- package/README.md +5 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +363 -55
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +23 -10
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +296 -25
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/adapters/mobile/start-console-forwarder.sh +5 -1
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +19 -110
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +39 -5
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/commands/call.js +63 -4
- package/dist/commands/run-engine.js +265 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +67 -3
- package/dist/execution-provenance.js +342 -0
- package/dist/network-observation.js +59 -47
- package/dist/performance-observation.js +465 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +7 -0
- package/library/actions/mobile/perps/performance-capture.mjs +570 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +43 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/extension.action-manifest.json +85 -0
- package/library/manifests/mobile.action-manifest.json +125 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +43 -27
package/dist/commands/call.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
emitHealViolation,
|
|
34
34
|
executeWithHealBounds,
|
|
35
35
|
prepareHeal,
|
|
36
|
+
persistRunEffects,
|
|
36
37
|
recoverRunInfra,
|
|
37
38
|
resolveMetaMaskLibrarySources,
|
|
38
39
|
preflightRecipe,
|
|
@@ -46,9 +47,13 @@ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run
|
|
|
46
47
|
import {
|
|
47
48
|
startRunNetworkObservation
|
|
48
49
|
} from "../network-observation.js";
|
|
50
|
+
import {
|
|
51
|
+
startRunPerformanceObservation
|
|
52
|
+
} from "../performance-observation.js";
|
|
49
53
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
50
54
|
import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
|
|
51
55
|
import { closest } from "../command-contract.js";
|
|
56
|
+
import { ProvenanceDriftError } from "../execution-provenance.js";
|
|
52
57
|
async function handleCall(argv) {
|
|
53
58
|
if (argv.includes("--list")) {
|
|
54
59
|
const { options: options2 } = parseArgs(argv, "call");
|
|
@@ -213,13 +218,17 @@ async function handleCall(argv) {
|
|
|
213
218
|
const requestedRuntimeOptions = runtimeOptionsFromCli(options);
|
|
214
219
|
const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
|
|
215
220
|
let networkObservation;
|
|
221
|
+
let performanceObservation;
|
|
216
222
|
const callRuntimeOptions = {
|
|
217
223
|
...requestedRuntimeOptions,
|
|
218
224
|
...librarySources ? { librarySources } : {},
|
|
219
225
|
autoHud: false,
|
|
220
226
|
suppressLibraryResolutionLogs: true,
|
|
221
227
|
stdoutIsMachineContract: json,
|
|
222
|
-
onActionEvent: ({ nodeId, action, status }) =>
|
|
228
|
+
onActionEvent: ({ nodeId, action, status }) => {
|
|
229
|
+
networkObservation?.onActionEvent({ nodeId, action, status });
|
|
230
|
+
performanceObservation?.onActionEvent({ nodeId, action, status });
|
|
231
|
+
},
|
|
223
232
|
...requestedRuntimeOptions.source ? { source: requestedRuntimeOptions.source } : inheritedSource ? {} : {
|
|
224
233
|
source: {
|
|
225
234
|
kind: "operator",
|
|
@@ -275,11 +284,20 @@ async function handleCall(argv) {
|
|
|
275
284
|
return checkoutBusyOut(json, "call", lock.message, lock.path);
|
|
276
285
|
}
|
|
277
286
|
try {
|
|
287
|
+
const authoredMobileRestart = adapter === "mobile" && resolvedAction === "app.lifecycle" && args.command === "restart";
|
|
278
288
|
const prepared = await prepareHeal(adapter, target, options, json, {
|
|
279
|
-
skipMobileSourceFreshness: adapter === "mobile" &&
|
|
289
|
+
skipMobileSourceFreshness: authoredMobileRestart || adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
|
|
280
290
|
});
|
|
281
291
|
if (typeof prepared === "number") return prepared;
|
|
282
292
|
const { state, heal } = prepared;
|
|
293
|
+
preflightedExecution = await preflightRecipe(
|
|
294
|
+
adapter,
|
|
295
|
+
recipe,
|
|
296
|
+
artifactsDir,
|
|
297
|
+
target,
|
|
298
|
+
actionManifestOverride,
|
|
299
|
+
callRuntimeOptions
|
|
300
|
+
);
|
|
283
301
|
networkObservation = await startRunNetworkObservation(
|
|
284
302
|
adapter,
|
|
285
303
|
target,
|
|
@@ -290,6 +308,16 @@ async function handleCall(argv) {
|
|
|
290
308
|
watcherPort: callRuntimeOptions.watcherPort
|
|
291
309
|
}
|
|
292
310
|
);
|
|
311
|
+
performanceObservation = startRunPerformanceObservation(
|
|
312
|
+
adapter,
|
|
313
|
+
target,
|
|
314
|
+
artifactsDir,
|
|
315
|
+
process.env,
|
|
316
|
+
{
|
|
317
|
+
cdpPort: callRuntimeOptions.cdpPort,
|
|
318
|
+
watcherPort: callRuntimeOptions.watcherPort
|
|
319
|
+
}
|
|
320
|
+
);
|
|
293
321
|
let executionResult;
|
|
294
322
|
try {
|
|
295
323
|
executionResult = await executeWithHealBounds(
|
|
@@ -303,7 +331,8 @@ async function handleCall(argv) {
|
|
|
303
331
|
target,
|
|
304
332
|
actionManifestOverride,
|
|
305
333
|
callRuntimeOptions,
|
|
306
|
-
execution
|
|
334
|
+
execution,
|
|
335
|
+
state
|
|
307
336
|
);
|
|
308
337
|
},
|
|
309
338
|
adapter,
|
|
@@ -316,13 +345,43 @@ async function handleCall(argv) {
|
|
|
316
345
|
})
|
|
317
346
|
);
|
|
318
347
|
} catch (error) {
|
|
319
|
-
await networkObservation?.finalize();
|
|
348
|
+
await networkObservation?.finalize().catch(() => void 0);
|
|
320
349
|
networkObservation = void 0;
|
|
350
|
+
await performanceObservation?.finalize().catch(() => void 0);
|
|
351
|
+
performanceObservation = void 0;
|
|
352
|
+
if (error instanceof ProvenanceDriftError) {
|
|
353
|
+
const failure = {
|
|
354
|
+
code: error.code,
|
|
355
|
+
message: error.message,
|
|
356
|
+
userAction: error.userAction,
|
|
357
|
+
provenancePath: error.provenancePath,
|
|
358
|
+
drift: error.drift
|
|
359
|
+
};
|
|
360
|
+
if (json) {
|
|
361
|
+
console.log(JSON.stringify({
|
|
362
|
+
schemaVersion: 1,
|
|
363
|
+
command: "call",
|
|
364
|
+
adapter,
|
|
365
|
+
action: shortName,
|
|
366
|
+
resolvedAction,
|
|
367
|
+
status: "fail",
|
|
368
|
+
error: failure,
|
|
369
|
+
exitCode: error.exitCode
|
|
370
|
+
}, null, 2));
|
|
371
|
+
} else {
|
|
372
|
+
console.error(`\u2717 mm-harness call: ${error.message}`);
|
|
373
|
+
console.error(` Next: ${error.userAction}`);
|
|
374
|
+
}
|
|
375
|
+
return error.exitCode;
|
|
376
|
+
}
|
|
321
377
|
throw error;
|
|
322
378
|
}
|
|
323
379
|
const { result, violation } = executionResult;
|
|
324
380
|
await networkObservation?.finalize(result.artifactManifestPath);
|
|
325
381
|
networkObservation = void 0;
|
|
382
|
+
await performanceObservation?.finalize(result.artifactManifestPath);
|
|
383
|
+
performanceObservation = void 0;
|
|
384
|
+
persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
|
|
326
385
|
if (violation !== null) {
|
|
327
386
|
const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
|
|
328
387
|
const example = describedAction ? actionExampleCommand(
|
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
validateMetaMaskActionInputs
|
|
29
29
|
} from "../metamask-action-validation.js";
|
|
30
30
|
import { gitLibraryProvenance } from "../library-provenance.js";
|
|
31
|
+
import {
|
|
32
|
+
captureExecutionProvenance,
|
|
33
|
+
executionProvenanceDrift,
|
|
34
|
+
ProvenanceDriftError,
|
|
35
|
+
writeExecutionProvenance
|
|
36
|
+
} from "../execution-provenance.js";
|
|
31
37
|
import {
|
|
32
38
|
authorizeTrustedMutationPlan,
|
|
33
39
|
loadTrustedMutationBase
|
|
@@ -36,7 +42,7 @@ import {
|
|
|
36
42
|
beginRunDiagnostics,
|
|
37
43
|
finishRunDiagnostics
|
|
38
44
|
} from "../run-diagnostics.js";
|
|
39
|
-
import { EXIT
|
|
45
|
+
import { EXIT } from "./shared.js";
|
|
40
46
|
import {
|
|
41
47
|
actionManifestPathOption,
|
|
42
48
|
optionString,
|
|
@@ -46,13 +52,14 @@ import {
|
|
|
46
52
|
targetPath,
|
|
47
53
|
usageError
|
|
48
54
|
} from "./parse-args.js";
|
|
49
|
-
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
|
|
55
|
+
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution, effectsState = newHealState()) {
|
|
50
56
|
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
51
57
|
adapter,
|
|
52
58
|
projectRoot,
|
|
53
59
|
runtimeOptions
|
|
54
60
|
);
|
|
55
61
|
try {
|
|
62
|
+
const wasPreflighted = preflightedExecution !== void 0;
|
|
56
63
|
const execution = preflightedExecution ?? await resolveRecipeExecution(
|
|
57
64
|
adapter,
|
|
58
65
|
recipe,
|
|
@@ -65,27 +72,48 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
65
72
|
runner,
|
|
66
73
|
runRequest,
|
|
67
74
|
absoluteArtifactsDir,
|
|
68
|
-
useFramedExtensionRecording
|
|
75
|
+
useFramedExtensionRecording,
|
|
76
|
+
provenanceInput,
|
|
77
|
+
provenanceSnapshots
|
|
69
78
|
} = execution;
|
|
70
|
-
await runner.preflight(runRequest);
|
|
79
|
+
if (!wasPreflighted) await runner.preflight(runRequest);
|
|
71
80
|
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
81
|
+
await runner.preflight(runRequest);
|
|
82
|
+
const preExecute = await captureExecutionProvenance(
|
|
83
|
+
provenanceInput,
|
|
84
|
+
"pre-execute"
|
|
85
|
+
);
|
|
86
|
+
provenanceSnapshots.push(preExecute);
|
|
87
|
+
const preExecuteDrift = executionProvenanceDrift(
|
|
88
|
+
provenanceSnapshots[0],
|
|
89
|
+
preExecute
|
|
90
|
+
);
|
|
91
|
+
if (preExecuteDrift.length > 0) {
|
|
92
|
+
const provenancePath2 = writeExecutionProvenance(
|
|
93
|
+
absoluteArtifactsDir,
|
|
94
|
+
provenanceSnapshots,
|
|
95
|
+
preExecuteDrift
|
|
96
|
+
);
|
|
97
|
+
throw new ProvenanceDriftError(provenancePath2, preExecuteDrift);
|
|
98
|
+
}
|
|
72
99
|
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
73
100
|
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
74
101
|
record: true,
|
|
75
102
|
cdpPort: runtimeOptions.cdpPort
|
|
76
103
|
}) : void 0;
|
|
77
104
|
let result;
|
|
105
|
+
let executionError;
|
|
78
106
|
try {
|
|
79
107
|
result = await runner.run(runRequest);
|
|
80
108
|
} catch (error) {
|
|
109
|
+
executionError = error;
|
|
81
110
|
try {
|
|
82
111
|
await stopRecipeRecording(recording);
|
|
83
|
-
} catch (
|
|
112
|
+
} catch (recordingError2) {
|
|
84
113
|
console.error(
|
|
85
|
-
`WARN: recipe and video recording both failed; preserving recipe failure: ${
|
|
114
|
+
`WARN: recipe and video recording both failed; preserving recipe failure: ${recordingError2 instanceof Error ? recordingError2.message : String(recordingError2)}`
|
|
86
115
|
);
|
|
87
116
|
}
|
|
88
|
-
throw error;
|
|
89
117
|
} finally {
|
|
90
118
|
if (adapter === "mobile") {
|
|
91
119
|
const { hideMobileHudOnTeardown } = await import("../adapters.js");
|
|
@@ -95,8 +123,36 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
95
123
|
);
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
let recordingError;
|
|
127
|
+
if (executionError === void 0 && result) {
|
|
128
|
+
try {
|
|
129
|
+
await stopRecipeRecording(recording, result);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
recordingError = error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const end = await captureExecutionProvenance(provenanceInput, "end");
|
|
135
|
+
provenanceSnapshots.push(end);
|
|
136
|
+
const endDrift = executionProvenanceDrift(provenanceSnapshots[0], end);
|
|
137
|
+
const provenancePath = writeExecutionProvenance(
|
|
138
|
+
absoluteArtifactsDir,
|
|
139
|
+
provenanceSnapshots,
|
|
140
|
+
endDrift,
|
|
141
|
+
result?.artifactManifestPath
|
|
142
|
+
);
|
|
143
|
+
if (endDrift.length > 0) {
|
|
144
|
+
throw new ProvenanceDriftError(provenancePath, endDrift, executionError);
|
|
145
|
+
}
|
|
146
|
+
if (executionError !== void 0) throw executionError;
|
|
147
|
+
if (recordingError !== void 0) throw recordingError;
|
|
148
|
+
if (!result) throw new Error("Recipe execution returned no result.");
|
|
149
|
+
const finalized = finishRunDiagnostics(diagnosticBaseline, result);
|
|
150
|
+
persistRunEffects(
|
|
151
|
+
finalized.summaryPath,
|
|
152
|
+
finalized.artifactManifestPath,
|
|
153
|
+
effectsState
|
|
154
|
+
);
|
|
155
|
+
return finalized;
|
|
100
156
|
} finally {
|
|
101
157
|
restoreRuntimeEnvironment();
|
|
102
158
|
}
|
|
@@ -122,6 +178,23 @@ async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actio
|
|
|
122
178
|
restoreRuntimeEnvironment();
|
|
123
179
|
}
|
|
124
180
|
}
|
|
181
|
+
function persistRunEffects(summaryPath, artifactManifestPath, state) {
|
|
182
|
+
for (const filePath of [summaryPath, artifactManifestPath]) {
|
|
183
|
+
let document;
|
|
184
|
+
try {
|
|
185
|
+
document = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
186
|
+
} catch {
|
|
187
|
+
document = null;
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(document)) {
|
|
190
|
+
throw new Error(`Recipe evidence file is not valid JSON: ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
document.recovered = [...state.recovered];
|
|
193
|
+
document.mutations = state.mutations.map((mutation) => ({ ...mutation }));
|
|
194
|
+
fs.writeFileSync(filePath, `${JSON.stringify(document, null, 2)}
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
125
198
|
async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
|
|
126
199
|
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
127
200
|
const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
|
|
@@ -173,7 +246,7 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
173
246
|
});
|
|
174
247
|
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
175
248
|
const runRequest = {
|
|
176
|
-
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
249
|
+
...recipeDocument !== void 0 ? { recipeDocument } : absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
177
250
|
artifactsDir: absoluteArtifactsDir,
|
|
178
251
|
projectRoot,
|
|
179
252
|
inheritProcessEnv: false,
|
|
@@ -196,13 +269,139 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
196
269
|
trustedMutation: authorizedMutation
|
|
197
270
|
});
|
|
198
271
|
}
|
|
272
|
+
const provenanceInput = {
|
|
273
|
+
adapter,
|
|
274
|
+
projectRoot,
|
|
275
|
+
recipeDocument: recipeDocument ?? recipe,
|
|
276
|
+
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : {},
|
|
277
|
+
...librarySources ? { librarySources } : {},
|
|
278
|
+
excludedProductRoots: [absoluteArtifactsDir],
|
|
279
|
+
helperPaths: commandHelperPaths(recipeDocument ?? recipe, projectRoot)
|
|
280
|
+
};
|
|
281
|
+
const startProvenance = await captureExecutionProvenance(
|
|
282
|
+
provenanceInput,
|
|
283
|
+
"start"
|
|
284
|
+
);
|
|
199
285
|
return {
|
|
200
286
|
runner,
|
|
201
287
|
absoluteArtifactsDir,
|
|
202
288
|
useFramedExtensionRecording,
|
|
203
|
-
runRequest
|
|
289
|
+
runRequest,
|
|
290
|
+
provenanceInput,
|
|
291
|
+
provenanceSnapshots: [startProvenance]
|
|
204
292
|
};
|
|
205
293
|
}
|
|
294
|
+
function commandHelperPaths(recipe, projectRoot) {
|
|
295
|
+
const helpers = /* @__PURE__ */ new Set();
|
|
296
|
+
visitRecipeValues(recipe, (value) => {
|
|
297
|
+
if (!isRecord(value) || value.action !== "command" || typeof value.cmd !== "string") return;
|
|
298
|
+
const candidates = commandHelperTokens(value.cmd);
|
|
299
|
+
for (const token of candidates) {
|
|
300
|
+
if (/[$`{}]/u.test(token)) {
|
|
301
|
+
throw new Error(`Command helper path cannot be bound before execution: ${token}`);
|
|
302
|
+
}
|
|
303
|
+
if (!isHelperToken(token)) continue;
|
|
304
|
+
const absolute = path.isAbsolute(token) ? path.resolve(token) : path.resolve(projectRoot, token);
|
|
305
|
+
let stat;
|
|
306
|
+
try {
|
|
307
|
+
stat = fs.lstatSync(absolute);
|
|
308
|
+
} catch {
|
|
309
|
+
throw new Error(`Command helper source does not exist: ${token}`);
|
|
310
|
+
}
|
|
311
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
312
|
+
throw new Error(`Command helper source must be a regular file: ${token}`);
|
|
313
|
+
}
|
|
314
|
+
helpers.add(absolute);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return [...helpers].sort();
|
|
318
|
+
}
|
|
319
|
+
function commandHelperTokens(command, depth = 0) {
|
|
320
|
+
if (depth > 4) {
|
|
321
|
+
throw new Error("Command helper sources exceed the supported shell nesting depth.");
|
|
322
|
+
}
|
|
323
|
+
const tokens = shellTokens(command);
|
|
324
|
+
const candidates = new Set(tokens.filter(isHelperToken));
|
|
325
|
+
for (const token of tokens) {
|
|
326
|
+
const assignment = /^(?:--)?(?:config|configuration|manifest|project|recipe|schema|tsconfig)=([^=]+)$/iu.exec(
|
|
327
|
+
token
|
|
328
|
+
);
|
|
329
|
+
const value = assignment?.[1];
|
|
330
|
+
if (value && (isHelperToken(value) || isConfigInputToken(value))) {
|
|
331
|
+
candidates.add(value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
335
|
+
if (!isScriptInterpreter(tokens[index])) continue;
|
|
336
|
+
const option = tokens[index + 1];
|
|
337
|
+
if (isShellInterpreter(tokens[index]) && /^-[a-z]*c[a-z]*$/iu.test(option) && tokens[index + 2]) {
|
|
338
|
+
for (const nested of commandHelperTokens(tokens[index + 2], depth + 1)) {
|
|
339
|
+
candidates.add(nested);
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!option.startsWith("-")) candidates.add(option);
|
|
344
|
+
}
|
|
345
|
+
return candidates;
|
|
346
|
+
}
|
|
347
|
+
function visitRecipeValues(value, visit) {
|
|
348
|
+
visit(value);
|
|
349
|
+
if (Array.isArray(value)) {
|
|
350
|
+
for (const child of value) visitRecipeValues(child, visit);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!isRecord(value)) return;
|
|
354
|
+
for (const child of Object.values(value)) visitRecipeValues(child, visit);
|
|
355
|
+
}
|
|
356
|
+
function isHelperToken(token) {
|
|
357
|
+
return !token.startsWith("-") && !token.includes("=") && !/^[a-z][a-z0-9+.-]*:\/\//iu.test(token) && /^\S+\.(?:cjs|mjs|js|jsx|ts|tsx|sh|py)$/u.test(token);
|
|
358
|
+
}
|
|
359
|
+
function isConfigInputToken(token) {
|
|
360
|
+
return !token.startsWith("-") && !token.includes("=") && /^\S+\.(?:json|json5|ya?ml|toml)$/u.test(token);
|
|
361
|
+
}
|
|
362
|
+
function isScriptInterpreter(token) {
|
|
363
|
+
return /^(?:node|bash|sh|python|python3)$/u.test(path.basename(token));
|
|
364
|
+
}
|
|
365
|
+
function isShellInterpreter(token) {
|
|
366
|
+
return /^(?:bash|sh)$/u.test(path.basename(token));
|
|
367
|
+
}
|
|
368
|
+
function shellTokens(command) {
|
|
369
|
+
const tokens = [];
|
|
370
|
+
let token = "";
|
|
371
|
+
let quote = null;
|
|
372
|
+
let escaped = false;
|
|
373
|
+
const push = () => {
|
|
374
|
+
if (token) tokens.push(token);
|
|
375
|
+
token = "";
|
|
376
|
+
};
|
|
377
|
+
for (const character of command) {
|
|
378
|
+
if (escaped) {
|
|
379
|
+
token += character;
|
|
380
|
+
escaped = false;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (character === "\\" && quote !== "'") {
|
|
384
|
+
escaped = true;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (quote) {
|
|
388
|
+
if (character === quote) quote = null;
|
|
389
|
+
else token += character;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (character === "'" || character === '"') {
|
|
393
|
+
quote = character;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (/\s|[;&|<>]/u.test(character)) push();
|
|
397
|
+
else token += character;
|
|
398
|
+
}
|
|
399
|
+
if (quote || escaped) {
|
|
400
|
+
throw new Error("Command helper sources cannot be bound from an unterminated shell token.");
|
|
401
|
+
}
|
|
402
|
+
push();
|
|
403
|
+
return tokens;
|
|
404
|
+
}
|
|
206
405
|
function runtimeRecipeDocument(recipe, absoluteRecipePath) {
|
|
207
406
|
if (typeof recipe !== "string") return recipe;
|
|
208
407
|
if (!absoluteRecipePath || !fs.existsSync(absoluteRecipePath)) return void 0;
|
|
@@ -241,7 +440,24 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
|
|
|
241
440
|
const previousAndroidPackageId = process.env.ANDROID_PACKAGE_ID;
|
|
242
441
|
const previousAdbSerial = process.env.ADB_SERIAL;
|
|
243
442
|
const previousAndroidSerial = process.env.ANDROID_SERIAL;
|
|
443
|
+
const explicitMobileDeviceEnv = adapter === "mobile" && process.env.MM_HARNESS_EXPLICIT_PLATFORM ? Object.fromEntries(
|
|
444
|
+
[
|
|
445
|
+
"PLATFORM",
|
|
446
|
+
"MM_HARNESS_EXPLICIT_PLATFORM",
|
|
447
|
+
"IOS_SIMULATOR",
|
|
448
|
+
"SIM_UDID",
|
|
449
|
+
"ADB_SERIAL",
|
|
450
|
+
"ANDROID_SERIAL",
|
|
451
|
+
"ANDROID_DEVICE",
|
|
452
|
+
"ANDROID_TARGET_DEVICE_NAME"
|
|
453
|
+
].map((key) => [key, process.env[key]])
|
|
454
|
+
) : void 0;
|
|
244
455
|
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
456
|
+
if (explicitMobileDeviceEnv) {
|
|
457
|
+
for (const [key, value] of Object.entries(explicitMobileDeviceEnv)) {
|
|
458
|
+
restoreEnv(key, value);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
245
461
|
if (runtimeOptions.cdpPort) {
|
|
246
462
|
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
247
463
|
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
@@ -921,19 +1137,15 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
921
1137
|
target,
|
|
922
1138
|
heal,
|
|
923
1139
|
state,
|
|
924
|
-
json
|
|
925
|
-
{
|
|
926
|
-
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
927
|
-
}
|
|
1140
|
+
json
|
|
928
1141
|
);
|
|
929
1142
|
if (current !== null) return current;
|
|
930
1143
|
}
|
|
931
1144
|
return { state, heal };
|
|
932
1145
|
}
|
|
933
|
-
async function ensureMobileProofRuntime(target,
|
|
1146
|
+
async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
|
|
934
1147
|
const source = await import("../adapters/mobile/source-freshness.js");
|
|
935
1148
|
const check = deps.check ?? (() => source.mobileSourceCheck(target));
|
|
936
|
-
const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
|
|
937
1149
|
const initial = check();
|
|
938
1150
|
if (initial.status === "unavailable") {
|
|
939
1151
|
if (json) {
|
|
@@ -958,99 +1170,26 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
958
1170
|
if (initial.status === "current") return null;
|
|
959
1171
|
const targetArg = JSON.stringify(path.resolve(target));
|
|
960
1172
|
const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
)
|
|
976
|
-
);
|
|
977
|
-
} else {
|
|
978
|
-
console.error(`\u2717 ${message}`);
|
|
979
|
-
console.error(` Next: ${userAction}`);
|
|
980
|
-
}
|
|
981
|
-
return EXIT.infra;
|
|
982
|
-
}
|
|
983
|
-
const recoveryCode = "mobile.source_reloaded";
|
|
984
|
-
state.attemptedRecoveries.push(recoveryCode);
|
|
985
|
-
deps.onRecovery?.(recoveryCode);
|
|
986
|
-
if (!json)
|
|
987
|
-
console.error(
|
|
988
|
-
`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`
|
|
989
|
-
);
|
|
990
|
-
const restart = deps.restart ?? (async () => {
|
|
991
|
-
const result2 = await runOneNode(
|
|
992
|
-
"mobile",
|
|
993
|
-
"app.lifecycle",
|
|
994
|
-
{ command: "restart" },
|
|
995
|
-
target,
|
|
996
|
-
void 0
|
|
1173
|
+
const message = `Mobile loaded source is not current (${initial.status}).`;
|
|
1174
|
+
if (json) {
|
|
1175
|
+
console.log(
|
|
1176
|
+
JSON.stringify(
|
|
1177
|
+
{
|
|
1178
|
+
schemaVersion: 1,
|
|
1179
|
+
status: "fail",
|
|
1180
|
+
recoverable: true,
|
|
1181
|
+
mutations: state.mutations,
|
|
1182
|
+
error: { code: "MOBILE_SOURCE_NOT_LOADED", message, userAction }
|
|
1183
|
+
},
|
|
1184
|
+
null,
|
|
1185
|
+
2
|
|
1186
|
+
)
|
|
997
1187
|
);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
};
|
|
1002
|
-
});
|
|
1003
|
-
const result = await restart();
|
|
1004
|
-
const platform = resolveRunMobilePlatform();
|
|
1005
|
-
const bridge = result.status === 0 ? await (deps.waitForBridge ? deps.waitForBridge() : spawnScriptStreaming(
|
|
1006
|
-
path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh"),
|
|
1007
|
-
["--target", target, "--platform", platform],
|
|
1008
|
-
target
|
|
1009
|
-
)) : { status: result.status, output: result.output };
|
|
1010
|
-
const recorded = result.status === 0 && bridge.status === 0 && record(initial.fingerprint);
|
|
1011
|
-
const verified = recorded ? check() : initial;
|
|
1012
|
-
if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
|
|
1013
|
-
const message = bridge.status !== 0 ? "Mobile source restart completed but the platform-matched bridge did not become ready." : "Mobile source restart did not produce a current loaded-source baseline.";
|
|
1014
|
-
if (json) {
|
|
1015
|
-
console.log(
|
|
1016
|
-
JSON.stringify(
|
|
1017
|
-
{
|
|
1018
|
-
schemaVersion: 1,
|
|
1019
|
-
status: "fail",
|
|
1020
|
-
recoverable: false,
|
|
1021
|
-
attemptedRecoveries: state.attemptedRecoveries,
|
|
1022
|
-
error: {
|
|
1023
|
-
code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED",
|
|
1024
|
-
message,
|
|
1025
|
-
userAction,
|
|
1026
|
-
originalError: bridge.output || result.output
|
|
1027
|
-
}
|
|
1028
|
-
},
|
|
1029
|
-
null,
|
|
1030
|
-
2
|
|
1031
|
-
)
|
|
1032
|
-
);
|
|
1033
|
-
} else {
|
|
1034
|
-
console.error(`\u2717 ${message}`);
|
|
1035
|
-
console.error(
|
|
1036
|
-
` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
return EXIT.infra;
|
|
1040
|
-
}
|
|
1041
|
-
state.recovered.push(recoveryCode);
|
|
1042
|
-
state.mutations.push({
|
|
1043
|
-
type: "runtime",
|
|
1044
|
-
action: "restarted",
|
|
1045
|
-
reasonCode: `source-${initial.status}`
|
|
1046
|
-
});
|
|
1047
|
-
return null;
|
|
1048
|
-
}
|
|
1049
|
-
function resolveRunMobilePlatform() {
|
|
1050
|
-
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
1051
|
-
return process.env.PLATFORM;
|
|
1188
|
+
} else {
|
|
1189
|
+
console.error(`\u2717 ${message}`);
|
|
1190
|
+
console.error(` Next: ${userAction}`);
|
|
1052
1191
|
}
|
|
1053
|
-
return
|
|
1192
|
+
return EXIT.infra;
|
|
1054
1193
|
}
|
|
1055
1194
|
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
1056
1195
|
const decide = deps.decide ?? (async () => {
|
|
@@ -1246,26 +1385,10 @@ async function recoverRunInfra(adapter, target, json, runtimeOptions = {}) {
|
|
|
1246
1385
|
return launchExtension(target, "quick", false);
|
|
1247
1386
|
}
|
|
1248
1387
|
if (adapter === "mobile") {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
await relaunchAndroidReleaseArtifactRuntime(artifact);
|
|
1254
|
-
return {
|
|
1255
|
-
status: 0,
|
|
1256
|
-
output: `Relaunched ${artifact.packageId} ${artifact.versionName} (${artifact.versionCode}) on ${artifact.deviceSerial}.`
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
} catch (error) {
|
|
1260
|
-
return {
|
|
1261
|
-
status: 1,
|
|
1262
|
-
output: `Release artifact recovery could not revalidate and relaunch the pinned Android package: ${error instanceof Error ? error.message : String(error)}
|
|
1263
|
-
Next: rerun runtime-launch with the same artifact source.`
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
const { launchMobile } = await import("./launch/mobile.js");
|
|
1267
|
-
const platform = resolveMobileRecoveryPlatform();
|
|
1268
|
-
return launchMobile(target, platform, "quick", json);
|
|
1388
|
+
return {
|
|
1389
|
+
status: 1,
|
|
1390
|
+
output: "Automatic Mobile runtime recovery is disabled.\nNext: use an explicit app.lifecycle action when the recipe intends to relaunch the app."
|
|
1391
|
+
};
|
|
1269
1392
|
}
|
|
1270
1393
|
return { status: 0, output: "headless run retry" };
|
|
1271
1394
|
} finally {
|
|
@@ -1273,15 +1396,6 @@ Next: rerun runtime-launch with the same artifact source.`
|
|
|
1273
1396
|
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
1274
1397
|
}
|
|
1275
1398
|
}
|
|
1276
|
-
function resolveMobileRecoveryPlatform(env = process.env) {
|
|
1277
|
-
if (env.PLATFORM === "android" || env.PLATFORM === "ios") return env.PLATFORM;
|
|
1278
|
-
const androidPinned = Boolean(
|
|
1279
|
-
env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE
|
|
1280
|
-
);
|
|
1281
|
-
const iosPinned = Boolean(env.IOS_SIMULATOR || env.SIM_UDID);
|
|
1282
|
-
if (androidPinned && !iosPinned) return "android";
|
|
1283
|
-
return "ios";
|
|
1284
|
-
}
|
|
1285
1399
|
function readRunFailureText(result) {
|
|
1286
1400
|
try {
|
|
1287
1401
|
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
@@ -1303,11 +1417,22 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
1303
1417
|
state
|
|
1304
1418
|
);
|
|
1305
1419
|
if (violation !== null) return { result, violation };
|
|
1420
|
+
if (adapter === "mobile") return { result, violation: null };
|
|
1306
1421
|
if (heal === "off") return { result, violation: null };
|
|
1307
1422
|
const recoveryCode = RUN_RECOVERY_CODE[adapter];
|
|
1308
1423
|
state.attemptedRecoveries.push(recoveryCode);
|
|
1309
1424
|
onRecovery(recoveryCode);
|
|
1310
|
-
await recover();
|
|
1425
|
+
const recovery = await recover();
|
|
1426
|
+
if (recovery.status !== 0) {
|
|
1427
|
+
return { result, violation: null };
|
|
1428
|
+
}
|
|
1429
|
+
if (adapter === "extension") {
|
|
1430
|
+
state.mutations.push({
|
|
1431
|
+
type: "runtime",
|
|
1432
|
+
action: "reloaded",
|
|
1433
|
+
reasonCode: recoveryCode
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1311
1436
|
result = await exec();
|
|
1312
1437
|
if (result.status === "pass") {
|
|
1313
1438
|
state.recovered.push(recoveryCode);
|
|
@@ -1362,6 +1487,7 @@ function countRecipeNodes(recipe) {
|
|
|
1362
1487
|
return nodes ? Object.keys(nodes).length : void 0;
|
|
1363
1488
|
}
|
|
1364
1489
|
export {
|
|
1490
|
+
activateRecipeRuntimeEnvironment,
|
|
1365
1491
|
countRecipeNodes,
|
|
1366
1492
|
describeRunnableRecipe,
|
|
1367
1493
|
emitHealViolation,
|
|
@@ -1369,12 +1495,12 @@ export {
|
|
|
1369
1495
|
ensureMobileProofRuntime,
|
|
1370
1496
|
executeWithHealBounds,
|
|
1371
1497
|
listRunnableRecipes,
|
|
1498
|
+
persistRunEffects,
|
|
1372
1499
|
preflightRecipe,
|
|
1373
1500
|
prepareHeal,
|
|
1374
1501
|
prepareRuntimeIfNeeded,
|
|
1375
1502
|
recoverRunInfra,
|
|
1376
1503
|
resolveMetaMaskLibrarySources,
|
|
1377
|
-
resolveMobileRecoveryPlatform,
|
|
1378
1504
|
resolveRunRecipeArg,
|
|
1379
1505
|
runOneNode,
|
|
1380
1506
|
runRecipe,
|