@deeeed/metamask-harness 0.74.0 → 0.76.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 +39 -1
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/adapters/shared/reap-checkout-metros.sh +9 -0
- package/dist/action-values.js +15 -0
- package/dist/adapters/core/actions.js +20 -0
- package/dist/adapters/core/live-script-env.js +36 -0
- package/dist/adapters/core/overlay.js +34 -0
- package/dist/adapters/core/readiness.js +8 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +12 -1
- package/dist/adapters/extension/action-inputs.js +206 -0
- package/dist/adapters/extension/actions.js +51 -0
- package/dist/adapters/extension/console-capture.js +13 -1
- package/dist/adapters/extension/doctor-checks.js +135 -0
- package/dist/adapters/extension/fixtures.js +103 -0
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/overlay.js +55 -0
- package/dist/adapters/extension/readiness.js +32 -0
- package/dist/adapters/extension/reload.js +40 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +31 -3
- package/dist/adapters/harness-freshness.js +4 -2
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/devices.js +21 -0
- package/dist/adapters/mobile/doctor-checks.js +40 -0
- package/dist/adapters/mobile/fixtures.js +141 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/overlay.js +27 -0
- package/dist/adapters/mobile/prepare-target.js +38 -0
- package/dist/adapters/mobile/readiness.js +39 -0
- package/dist/adapters/mobile/reload.js +15 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +39 -3
- package/dist/adapters/mobile/ui.js +701 -0
- package/dist/adapters/performance/cdp-trace.js +24 -6
- package/dist/adapters/performance/device-platform.js +10 -0
- package/dist/adapters/registry.js +1 -1
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/doctor-checks.js +22 -0
- package/dist/adapters/terminal/overlay.js +18 -0
- package/dist/adapters/terminal/readiness.js +11 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +21 -3
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +3 -1
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/debug.js +6 -25
- package/dist/commands/device-target.js +2 -1
- package/dist/commands/doctor.js +26 -137
- package/dist/commands/fixtures.js +13 -217
- package/dist/commands/launch/device.js +2 -6
- package/dist/commands/launch/extension.js +3 -303
- package/dist/commands/launch/index.js +1 -1
- package/dist/commands/launch/mobile.js +2 -190
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/parse-args.js +9 -4
- package/dist/commands/pin.js +226 -0
- package/dist/commands/prepare.js +23 -53
- package/dist/commands/provision.js +3 -7
- package/dist/commands/reload.js +8 -47
- package/dist/commands/run-engine.js +38 -138
- package/dist/commands/run-report.js +4 -48
- package/dist/commands/run.js +6 -48
- package/dist/commands/runtime-decision.js +5 -27
- package/dist/commands/self-test.js +1 -3
- package/dist/commands/status.js +20 -28
- package/dist/commands/stop.js +1 -1
- package/dist/doctor.js +12 -120
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/harness.js +31 -100
- package/dist/heal-bounds.js +3 -2
- package/dist/live-adapter-contract.js +8 -34
- package/dist/manifest.js +3 -5
- package/dist/metamask-action-validation.js +5 -196
- package/dist/mm-harness-cli.js +30 -4
- package/dist/network-observation.js +10 -3
- package/dist/paths.js +8 -2
- package/dist/performance-observation.js +15 -22
- package/dist/pr-body/render.js +3 -1
- package/dist/recording-target.js +4 -39
- package/dist/run-diagnostics.js +21 -60
- package/dist/run-recording.js +5 -4
- package/dist/runner.js +20 -368
- package/dist/runtime-context.js +12 -17
- package/docs/CONTRIBUTING.md +29 -7
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
package/dist/run-diagnostics.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
|
-
import { recipeRuntimePath, runnerDir } from "./paths.js";
|
|
7
|
-
import { ensureExtensionConsoleCapture, extensionCaptureFiles, extensionCdpPort } from "./adapters/extension/console-capture.js";
|
|
8
|
-
import {
|
|
9
|
-
ensureTerminalConsoleCapture,
|
|
10
|
-
terminalCaptureFiles,
|
|
11
|
-
terminalCdpPort,
|
|
12
|
-
terminalRuntimeFile
|
|
13
|
-
} from "./adapters/terminal/console-capture.js";
|
|
14
5
|
import {
|
|
15
6
|
collectorOwnerMarker,
|
|
16
|
-
collectorStatus
|
|
17
|
-
verifyCapture
|
|
7
|
+
collectorStatus
|
|
18
8
|
} from "../library/actions/shared/console-collector.mjs";
|
|
19
9
|
import {
|
|
20
10
|
allowlistMatch,
|
|
@@ -39,7 +29,7 @@ function readRunDiagnosticsDocument(diagnosticsPath) {
|
|
|
39
29
|
}
|
|
40
30
|
function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
41
31
|
if (!diagnostics) {
|
|
42
|
-
return [adapter
|
|
32
|
+
return [getAdapterSurface(adapter).headless ? `N/A \u2014 ${adapter.charAt(0).toUpperCase()}${adapter.slice(1)} is headless and has no application log source.` : "UNAVAILABLE \u2014 application diagnostics could not be collected for this run."];
|
|
43
33
|
}
|
|
44
34
|
const status = diagnostics.status === "clean" ? "CLEAN" : diagnostics.status === "review" ? "REVIEW" : "UNAVAILABLE";
|
|
45
35
|
const lines = [`${status} \u2014 ${diagnostics.note}`];
|
|
@@ -52,25 +42,27 @@ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
|
52
42
|
return lines;
|
|
53
43
|
}
|
|
54
44
|
async function beginRunDiagnostics(adapter, projectRoot) {
|
|
55
|
-
const
|
|
45
|
+
const surface = getAdapterSurface(adapter);
|
|
46
|
+
const source = surface.appLogSource(projectRoot);
|
|
56
47
|
if (!source) return null;
|
|
57
|
-
const
|
|
48
|
+
const diagnostics = surface.diagnostics;
|
|
49
|
+
const issueBuffer = diagnostics?.issueBuffer?.arm(projectRoot) ? { adapter, projectRoot } : void 0;
|
|
58
50
|
let capture;
|
|
59
|
-
if (
|
|
60
|
-
const
|
|
61
|
-
const startError = await start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
62
|
-
const { extensionLog, pageLog, pidFile } =
|
|
63
|
-
const cdpPort =
|
|
51
|
+
if (diagnostics?.console) {
|
|
52
|
+
const collector = diagnostics.console;
|
|
53
|
+
const startError = await collector.start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
54
|
+
const { extensionLog, pageLog, pidFile } = collector.files(projectRoot);
|
|
55
|
+
const cdpPort = collector.cdpPort();
|
|
64
56
|
capture = { adapter, projectRoot, extensionLog, pageLog, pidFile, ...cdpPort ? { cdpPort } : {}, ...startError ? { startError } : {} };
|
|
65
57
|
}
|
|
66
|
-
const walletLogPath =
|
|
58
|
+
const walletLogPath = diagnostics?.walletLog?.(projectRoot) ?? null;
|
|
67
59
|
const walletStat = walletLogPath ? safeStat(walletLogPath) : null;
|
|
68
60
|
const stat = safeStat(source.path);
|
|
69
61
|
return {
|
|
70
62
|
source,
|
|
71
63
|
offset: stat?.size ?? 0,
|
|
72
64
|
...stat ? { inode: stat.ino } : {},
|
|
73
|
-
...
|
|
65
|
+
...issueBuffer ? { issueBuffer } : {},
|
|
74
66
|
...capture ? { capture } : {},
|
|
75
67
|
...walletLogPath ? { walletLog: { path: walletLogPath, offset: walletStat?.size ?? 0, ...walletStat ? { inode: walletStat.ino } : {} } } : {}
|
|
76
68
|
};
|
|
@@ -85,20 +77,13 @@ async function verifyConsoleCapture(capture) {
|
|
|
85
77
|
cdpPort: capture.cdpPort
|
|
86
78
|
});
|
|
87
79
|
if (!status.ok) return { verified: false, detail: status.detail };
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
|
|
97
|
-
} else {
|
|
98
|
-
const browser = readJsonRecord(terminalRuntimeFile(capture.projectRoot, "browser.json"));
|
|
99
|
-
if (typeof browser?.appOrigin !== "string") return { verified: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
|
|
100
|
-
control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
|
|
101
|
-
}
|
|
80
|
+
const control = await getAdapterSurface(capture.adapter).diagnostics.console.verifyControl({
|
|
81
|
+
projectRoot: capture.projectRoot,
|
|
82
|
+
cdpPort: capture.cdpPort,
|
|
83
|
+
extensionLog: capture.extensionLog,
|
|
84
|
+
pageLog: capture.pageLog,
|
|
85
|
+
pidFile: capture.pidFile
|
|
86
|
+
});
|
|
102
87
|
return { verified: control.ok, detail: control.ok ? `${status.detail}; ${control.detail}` : control.detail };
|
|
103
88
|
}
|
|
104
89
|
function isRunDiagnosticsDocument(value) {
|
|
@@ -109,7 +94,7 @@ function isRunDiagnosticsDocument(value) {
|
|
|
109
94
|
async function finishRunDiagnostics(baseline, result) {
|
|
110
95
|
if (!baseline) return result;
|
|
111
96
|
try {
|
|
112
|
-
const bufferedIssues = baseline.
|
|
97
|
+
const bufferedIssues = baseline.issueBuffer ? getAdapterSurface(baseline.issueBuffer.adapter).diagnostics?.issueBuffer?.collect(baseline.issueBuffer.projectRoot) : void 0;
|
|
113
98
|
const allowlist = loadConsoleAllowlist(recipeLibraryRoots(result.summaryPath));
|
|
114
99
|
const capture = baseline.capture ? await verifyConsoleCapture(baseline.capture) : void 0;
|
|
115
100
|
const { diagnostics, rawAppLog } = collectRunDiagnosticsCapture(baseline, bufferedIssues, allowlist, capture);
|
|
@@ -271,30 +256,6 @@ function applyConsoleAllowlist(findings, allowlist) {
|
|
|
271
256
|
finding.fingerprint = createHash("sha256").update(`${finding.fingerprint}|info|${match.file}#${match.index}`).digest("hex").slice(0, 12);
|
|
272
257
|
}
|
|
273
258
|
}
|
|
274
|
-
function armMobileIssueBuffer(projectRoot) {
|
|
275
|
-
const armed = runMobileIssueCommand(projectRoot, "issues-arm");
|
|
276
|
-
if (!armed) return false;
|
|
277
|
-
return runMobileIssueCommand(projectRoot, "issues-collect") !== null;
|
|
278
|
-
}
|
|
279
|
-
function collectMobileIssueBuffer(projectRoot) {
|
|
280
|
-
const result = runMobileIssueCommand(projectRoot, "issues-collect");
|
|
281
|
-
return isRecord(result) && Array.isArray(result.entries) ? result.entries : null;
|
|
282
|
-
}
|
|
283
|
-
function runMobileIssueCommand(projectRoot, command) {
|
|
284
|
-
const bridge = path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
285
|
-
const result = spawnSync(process.execPath, [bridge, command], {
|
|
286
|
-
cwd: projectRoot,
|
|
287
|
-
env: { ...process.env, APP_ROOT: projectRoot, CDP_TIMEOUT: "5000" },
|
|
288
|
-
encoding: "utf8",
|
|
289
|
-
timeout: 1e4
|
|
290
|
-
});
|
|
291
|
-
if (result.status !== 0) return null;
|
|
292
|
-
try {
|
|
293
|
-
return JSON.parse(result.stdout.trim());
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
259
|
function redactPreview(value) {
|
|
299
260
|
return redactDiagnosticText(value).slice(0, MAX_PREVIEW_CHARS);
|
|
300
261
|
}
|
package/dist/run-recording.js
CHANGED
|
@@ -2,10 +2,10 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { optionalVideoTiming, readCaptureHelperTiming, writeRecordingTimeline } from "@farmslot/recipe-harness";
|
|
5
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
5
6
|
import {
|
|
6
7
|
captureHelperSupportsRecordSessionSnapshots,
|
|
7
|
-
captureHelperSupportsCapability
|
|
8
|
-
resolveExtensionBrowserPid
|
|
8
|
+
captureHelperSupportsCapability
|
|
9
9
|
} from "./recording-target.js";
|
|
10
10
|
const activeRecordingsByPid = /* @__PURE__ */ new Map();
|
|
11
11
|
const ACTIVE_EXTENSION_RECORDING_PID = "METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID";
|
|
@@ -17,7 +17,8 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
|
|
|
17
17
|
);
|
|
18
18
|
return void 0;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const framed = getAdapterSurface(adapter).recording?.framed;
|
|
21
|
+
if (!framed) return void 0;
|
|
21
22
|
const cdpPort = options.cdpPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
22
23
|
if (!captureHelperSupportsRecordSessionSnapshots(projectRoot)) {
|
|
23
24
|
console.error(
|
|
@@ -25,7 +26,7 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
|
|
|
25
26
|
);
|
|
26
27
|
return void 0;
|
|
27
28
|
}
|
|
28
|
-
const pid =
|
|
29
|
+
const pid = framed.browserPid(projectRoot, artifactsDir, cdpPort);
|
|
29
30
|
if (!pid) {
|
|
30
31
|
console.error(
|
|
31
32
|
`WARN: framed extension recording could not resolve browser PID from CDP port ${cdpPort ?? "<unset>"}; falling back to harness --record-video.`
|
package/dist/runner.js
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
2
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
3
|
-
import { createAndroidMirrorVideoRecorder } from "@farmslot/recipe-harness";
|
|
4
3
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
5
|
-
import {
|
|
6
|
-
bridgeCommand,
|
|
7
|
-
MOBILE_BRIDGE_ERROR_CODES
|
|
8
|
-
} from "../library/actions/mobile/platform/bridge.mjs";
|
|
9
|
-
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
10
|
-
import {
|
|
11
|
-
mobileSourceFingerprint,
|
|
12
|
-
recordMobileSourceBaseline
|
|
13
|
-
} from "./adapters/mobile/source-freshness.js";
|
|
14
|
-
import { readAndroidNativeRuntimeIdentity } from "./adapters/mobile/release-artifact.js";
|
|
15
|
-
import { readMobileReleaseArtifactState } from "./adapters/mobile/release-artifact-state.js";
|
|
16
|
-
import {
|
|
17
|
-
createAndroidVideoRecorder,
|
|
18
|
-
createIosSimulatorVideoRecorder
|
|
19
|
-
} from "./adapters/mobile/video-recorder.js";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
20
5
|
import {
|
|
21
6
|
bindMetaMaskMobileLifecycleDeadline,
|
|
22
7
|
createMetaMaskMobileLifecycleCommandRunner,
|
|
23
|
-
MOBILE_LIFECYCLE_DEADLINE_FIELD,
|
|
24
|
-
MOBILE_LIFECYCLE_PREPARED,
|
|
25
8
|
resolveMetaMaskMobileLifecycleTarget
|
|
26
9
|
} from "./app-lifecycle.js";
|
|
27
10
|
import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
|
|
@@ -29,11 +12,7 @@ import { actionSourceIntegrity, metaMaskActionExecutionCapabilities } from "./re
|
|
|
29
12
|
import {
|
|
30
13
|
prepareLiveAdapterScript
|
|
31
14
|
} from "./live-adapter-contract.js";
|
|
32
|
-
import {
|
|
33
|
-
createMetaMaskRecordingTargetProvider,
|
|
34
|
-
resolveAndroidDeviceSerial,
|
|
35
|
-
resolveMobileIosSimulatorName
|
|
36
|
-
} from "./recording-target.js";
|
|
15
|
+
import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
|
|
37
16
|
import {
|
|
38
17
|
importMetaMaskNativeUiProvider,
|
|
39
18
|
importRecipeHarness,
|
|
@@ -92,16 +71,15 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
92
71
|
}, preparedLiveAdapters, { hudPolicy: options.autoHud === true ? "show" : "auto" })
|
|
93
72
|
});
|
|
94
73
|
const existing = new Set([...core, ...ui].map((entry) => entry.action));
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
103
|
-
)
|
|
74
|
+
const surface = getAdapterSurface(adapter);
|
|
75
|
+
const lifecycleAdapters = bindMetaMaskMobileLifecycleDeadline(
|
|
76
|
+
createAppLifecycleAdapters({
|
|
77
|
+
actions,
|
|
78
|
+
commandRunner: createMetaMaskMobileLifecycleCommandRunner(),
|
|
79
|
+
targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
|
|
80
|
+
})
|
|
104
81
|
);
|
|
82
|
+
const lifecycle = surface.actions.lifecycle ? await surface.actions.lifecycle(lifecycleAdapters) : lifecycleAdapters;
|
|
105
83
|
const custom = createMetaMaskAdapters(
|
|
106
84
|
adapter,
|
|
107
85
|
customActions,
|
|
@@ -117,31 +95,22 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
117
95
|
options.quietStdout ? new console.Console(process.stderr) : console,
|
|
118
96
|
options.suppressLibraryResolutionLogs
|
|
119
97
|
);
|
|
120
|
-
const
|
|
121
|
-
const
|
|
98
|
+
const videoRecorder = await surface.recording?.videoRecorder?.();
|
|
99
|
+
const secured = [...core, ...ui, ...lifecycle, ...custom].map((entry) => ({
|
|
100
|
+
...entry,
|
|
101
|
+
...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
|
|
102
|
+
}));
|
|
103
|
+
const mutationBound = surface.actions.trustedMutation ? withTrustedMutationContext(secured, options.trustedMutation, externalMutationActions) : secured;
|
|
122
104
|
return createRecipeRunner({
|
|
123
105
|
actionManifest,
|
|
124
106
|
adapters: withActionProgress(
|
|
125
|
-
|
|
126
|
-
adapter,
|
|
127
|
-
withTrustedMutationContext(
|
|
128
|
-
[...core, ...ui, ...lifecycle, ...custom].map((entry) => ({
|
|
129
|
-
...entry,
|
|
130
|
-
...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
|
|
131
|
-
})),
|
|
132
|
-
options.trustedMutation,
|
|
133
|
-
externalMutationActions,
|
|
134
|
-
adapter
|
|
135
|
-
)
|
|
136
|
-
),
|
|
107
|
+
surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
|
|
137
108
|
options.onActionEvent
|
|
138
109
|
),
|
|
139
110
|
logger,
|
|
140
111
|
recording: {
|
|
141
112
|
targetProvider: createMetaMaskRecordingTargetProvider(adapter),
|
|
142
|
-
...
|
|
143
|
-
videoRecorder: createIosSimulatorVideoRecorder(iosRecordingSimulator)
|
|
144
|
-
} : {}
|
|
113
|
+
...videoRecorder ? { videoRecorder } : {}
|
|
145
114
|
},
|
|
146
115
|
runner: runnerProvenance(),
|
|
147
116
|
defaultSource: {
|
|
@@ -161,38 +130,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
161
130
|
}
|
|
162
131
|
});
|
|
163
132
|
}
|
|
164
|
-
function
|
|
165
|
-
state: readMobileReleaseArtifactState,
|
|
166
|
-
read: readAndroidNativeRuntimeIdentity
|
|
167
|
-
}) {
|
|
168
|
-
if (adapter !== "mobile") return adapters;
|
|
169
|
-
return adapters.map((entry) => ({
|
|
170
|
-
...entry,
|
|
171
|
-
async execute(node, context) {
|
|
172
|
-
const state = identity.state(context.projectRoot);
|
|
173
|
-
if (!state) {
|
|
174
|
-
if (context.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") {
|
|
175
|
-
throw new Error("Mobile release-artifact actions require current trusted runtime state. Next: rerun runtime-launch with the exact artifact.");
|
|
176
|
-
}
|
|
177
|
-
return entry.execute(node, context);
|
|
178
|
-
}
|
|
179
|
-
identity.read(state);
|
|
180
|
-
const result = await entry.execute(node, context);
|
|
181
|
-
const nativeRuntimeIdentity = identity.read(state);
|
|
182
|
-
const output = result.output && typeof result.output === "object" && !Array.isArray(result.output) ? result.output : { result: result.output };
|
|
183
|
-
return {
|
|
184
|
-
...result,
|
|
185
|
-
output: {
|
|
186
|
-
...output,
|
|
187
|
-
nativeRuntimeIdentity,
|
|
188
|
-
runtimeSessionId: nativeRuntimeIdentity.runtimeSessionId
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}));
|
|
193
|
-
}
|
|
194
|
-
function withTrustedMutationContext(adapters, base, externalMutationActions, adapter = "extension") {
|
|
195
|
-
if (adapter !== "extension") return adapters;
|
|
133
|
+
function withTrustedMutationContext(adapters, base, externalMutationActions) {
|
|
196
134
|
let selectedIdentitySha256;
|
|
197
135
|
const identityProofAction = "metamask.wallet.import";
|
|
198
136
|
const identityInvalidatingActions = /* @__PURE__ */ new Set([
|
|
@@ -353,289 +291,6 @@ function isAutomaticHudProgress(action, node, nodeId) {
|
|
|
353
291
|
const progress = payload.progress;
|
|
354
292
|
return nodeId === "recipe-complete" && progress !== null && typeof progress === "object" && !Array.isArray(progress) && progress.complete === true;
|
|
355
293
|
}
|
|
356
|
-
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe = probeMobileLifecycleRuntime, sourceFreshness = {
|
|
357
|
-
fingerprint: mobileSourceFingerprint,
|
|
358
|
-
record: recordMobileSourceBaseline
|
|
359
|
-
}, processIdentity = {
|
|
360
|
-
readIosPid: readIosAppPid,
|
|
361
|
-
readAndroidPid: readAndroidAppPid
|
|
362
|
-
}, runtimeMarker = readMobileLifecycleRuntimeIdentity) {
|
|
363
|
-
if (adapter !== "mobile") return lifecycle;
|
|
364
|
-
return lifecycle.map((entry) => ({
|
|
365
|
-
...entry,
|
|
366
|
-
async execute(node, context) {
|
|
367
|
-
const command = node.command ?? node.event ?? node.state;
|
|
368
|
-
const opaqueRuntime = context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
|
|
369
|
-
const requiresProcessContinuity = command !== "restart" && node.require_process_continuity === true;
|
|
370
|
-
if (opaqueRuntime && !requiresProcessContinuity) {
|
|
371
|
-
return entry.execute(node, context);
|
|
372
|
-
}
|
|
373
|
-
const reloadsSource = !opaqueRuntime && (command === "launch" || command === "foreground" || command === "restart");
|
|
374
|
-
const recordsSource = !opaqueRuntime && command === "restart";
|
|
375
|
-
const androidLifecycle = isAndroidLifecycle(node, context.env ?? {});
|
|
376
|
-
const checksIosContinuity = command === "foreground" && !androidLifecycle;
|
|
377
|
-
const checksRequiredContinuity = requiresProcessContinuity;
|
|
378
|
-
const recordsProcessIdentity = checksIosContinuity || checksRequiredContinuity;
|
|
379
|
-
const readProcessId = androidLifecycle ? processIdentity.readAndroidPid ?? readAndroidAppPid : processIdentity.readIosPid;
|
|
380
|
-
const processBefore = recordsProcessIdentity ? readProcessId(node, context) : void 0;
|
|
381
|
-
const fingerprint = recordsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
|
|
382
|
-
const previousRuntimeIdentity = recordsSource ? await runtimeMarker(node, context) : void 0;
|
|
383
|
-
const androidRestart = command === "restart" && androidLifecycle;
|
|
384
|
-
const initialResult = await entry.execute(node, context);
|
|
385
|
-
const readinessDeadline = reloadsSource ? mobileLifecycleReadinessDeadline(command, node) : void 0;
|
|
386
|
-
let result = initialResult;
|
|
387
|
-
let runtimeReady = false;
|
|
388
|
-
if (androidRestart) {
|
|
389
|
-
try {
|
|
390
|
-
const remainingMs = Math.max(
|
|
391
|
-
1,
|
|
392
|
-
(readinessDeadline ?? Date.now()) - Date.now()
|
|
393
|
-
);
|
|
394
|
-
const handoffDeadline = Math.min(
|
|
395
|
-
readinessDeadline ?? Number.POSITIVE_INFINITY,
|
|
396
|
-
Date.now() + Math.min(1e4, Math.max(1, remainingMs / 2))
|
|
397
|
-
);
|
|
398
|
-
await readinessProbe(
|
|
399
|
-
node,
|
|
400
|
-
context,
|
|
401
|
-
previousRuntimeIdentity,
|
|
402
|
-
handoffDeadline
|
|
403
|
-
);
|
|
404
|
-
runtimeReady = true;
|
|
405
|
-
} catch (error) {
|
|
406
|
-
if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
|
|
407
|
-
throw error;
|
|
408
|
-
}
|
|
409
|
-
const foregroundTimeoutMs = Math.max(
|
|
410
|
-
1,
|
|
411
|
-
(readinessDeadline ?? Date.now() + 1) - Date.now()
|
|
412
|
-
);
|
|
413
|
-
const foregroundResult = await entry.execute(
|
|
414
|
-
{
|
|
415
|
-
...node,
|
|
416
|
-
command: "foreground",
|
|
417
|
-
settle_ms: 0,
|
|
418
|
-
timeout_ms: foregroundTimeoutMs,
|
|
419
|
-
[MOBILE_LIFECYCLE_DEADLINE_FIELD]: readinessDeadline,
|
|
420
|
-
[MOBILE_LIFECYCLE_PREPARED]: true
|
|
421
|
-
},
|
|
422
|
-
context
|
|
423
|
-
);
|
|
424
|
-
result = mergeAndroidRestartResults(initialResult, foregroundResult);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (reloadsSource && !runtimeReady) {
|
|
428
|
-
if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
|
|
429
|
-
throw new Error(
|
|
430
|
-
`Mobile lifecycle ${String(command)} exhausted its runtime readiness deadline before recovery completed.`
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
await readinessProbe(
|
|
434
|
-
node,
|
|
435
|
-
context,
|
|
436
|
-
previousRuntimeIdentity,
|
|
437
|
-
readinessDeadline
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
if (recordsProcessIdentity) {
|
|
441
|
-
const processAfter = readProcessId(node, context);
|
|
442
|
-
const processPreserved = processBefore !== null && processBefore === processAfter;
|
|
443
|
-
const actualTransition = processPreserved ? command === "background" ? "background_same_process" : "foreground_resume" : processAfter !== null ? "cold_relaunch" : "unknown";
|
|
444
|
-
const processContinuity = processPreserved ? "preserved" : processAfter === null ? "unknown" : "changed";
|
|
445
|
-
const resultRecord = asRecord(result);
|
|
446
|
-
result = {
|
|
447
|
-
...resultRecord,
|
|
448
|
-
output: {
|
|
449
|
-
...asRecord(resultRecord.output),
|
|
450
|
-
actualTransition,
|
|
451
|
-
processContinuity,
|
|
452
|
-
processBefore,
|
|
453
|
-
processAfter
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
if (checksRequiredContinuity && !processPreserved) {
|
|
457
|
-
const platform = androidLifecycle ? "Android" : "iOS";
|
|
458
|
-
throw new Error(
|
|
459
|
-
`${platform} ${String(command)} did not prove app-process continuity (actualTransition=${actualTransition}, before=${String(processBefore)}, after=${String(processAfter)}); this lifecycle sample cannot be classified as a same-process reconnect.`
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
if (fingerprint !== void 0 && !sourceFreshness.record(context.projectRoot, fingerprint)) {
|
|
464
|
-
throw new Error(
|
|
465
|
-
"Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
return result;
|
|
469
|
-
}
|
|
470
|
-
}));
|
|
471
|
-
}
|
|
472
|
-
function readAndroidAppPid(node, context) {
|
|
473
|
-
const env = context.env ?? {};
|
|
474
|
-
const device = node.adb_serial ?? node.android_device ?? node.device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL;
|
|
475
|
-
const packageId = node.package_id ?? node.packageName ?? node.app_id ?? env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask";
|
|
476
|
-
if (typeof device !== "string" || typeof packageId !== "string") return null;
|
|
477
|
-
try {
|
|
478
|
-
const adb = resolveMobileToolPath("adb", { required: true });
|
|
479
|
-
const output = execFileSync(
|
|
480
|
-
adb,
|
|
481
|
-
["-s", device, "shell", "pidof", packageId],
|
|
482
|
-
{ encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
|
|
483
|
-
);
|
|
484
|
-
const pid = Number.parseInt(output.trim().split(/\s+/u)[0] ?? "", 10);
|
|
485
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
486
|
-
} catch {
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
function readIosAppPid(node, context) {
|
|
491
|
-
const env = context.env ?? {};
|
|
492
|
-
const device = node.simulator ?? node.ios_simulator ?? env.SIM_UDID ?? env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
|
|
493
|
-
const bundleId = node.bundle_id ?? node.bundleId ?? env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
|
|
494
|
-
if (typeof device !== "string" || typeof bundleId !== "string") return null;
|
|
495
|
-
try {
|
|
496
|
-
const output = execFileSync(
|
|
497
|
-
"xcrun",
|
|
498
|
-
["simctl", "spawn", device, "launchctl", "list"],
|
|
499
|
-
{ encoding: "utf8", timeout: 5e3 }
|
|
500
|
-
);
|
|
501
|
-
const labelPrefix = `UIKitApplication:${bundleId}[`;
|
|
502
|
-
const line = output.split("\n").find((candidate) => candidate.includes(labelPrefix));
|
|
503
|
-
if (!line) return null;
|
|
504
|
-
const pid = Number.parseInt(line.trim().split(/\s+/u)[0] ?? "", 10);
|
|
505
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
506
|
-
} catch {
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
function isAndroidLifecycle(node, env) {
|
|
511
|
-
const platform = node.platform ?? env.PLATFORM ?? process.env.PLATFORM;
|
|
512
|
-
if (platform === "android") return true;
|
|
513
|
-
if (platform === "ios") return false;
|
|
514
|
-
return Boolean(
|
|
515
|
-
node.adb_serial ?? node.android_device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
function mergeAndroidRestartResults(initialResult, foregroundResult) {
|
|
519
|
-
const initial = asRecord(initialResult);
|
|
520
|
-
const foreground = asRecord(foregroundResult);
|
|
521
|
-
const initialOutput = asRecord(initial.output);
|
|
522
|
-
const foregroundOutput = asRecord(foreground.output);
|
|
523
|
-
return {
|
|
524
|
-
...initial,
|
|
525
|
-
output: {
|
|
526
|
-
...initialOutput,
|
|
527
|
-
command: "restart",
|
|
528
|
-
calls: [
|
|
529
|
-
...Array.isArray(initialOutput.calls) ? initialOutput.calls : [],
|
|
530
|
-
...Array.isArray(foregroundOutput.calls) ? foregroundOutput.calls : []
|
|
531
|
-
],
|
|
532
|
-
settleMs: foregroundOutput.settleMs ?? initialOutput.settleMs ?? 0,
|
|
533
|
-
foregroundEnsured: true
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
async function probeMobileLifecycleRuntime(node, context, previousRuntimeIdentity, deadlineMs) {
|
|
538
|
-
const command = node.command ?? node.event ?? node.state;
|
|
539
|
-
const timeoutMs = resolveMobileLifecycleReadinessTimeout(
|
|
540
|
-
command,
|
|
541
|
-
node.runtime_ready_timeout_ms
|
|
542
|
-
);
|
|
543
|
-
const startedAt = Date.now();
|
|
544
|
-
const deadline = deadlineMs ?? startedAt + timeoutMs;
|
|
545
|
-
let lastError;
|
|
546
|
-
while (Date.now() < deadline) {
|
|
547
|
-
try {
|
|
548
|
-
const status = await bridgeCommand(
|
|
549
|
-
{
|
|
550
|
-
action: "app.lifecycle",
|
|
551
|
-
node: {
|
|
552
|
-
...node,
|
|
553
|
-
cdp_timeout_ms: Math.min(8e3, Math.max(1, deadline - Date.now()))
|
|
554
|
-
},
|
|
555
|
-
context
|
|
556
|
-
},
|
|
557
|
-
["status-selected"]
|
|
558
|
-
);
|
|
559
|
-
if (status?.agenticPresent === true && status.route && (!previousRuntimeIdentity || status.runtimeIdentity !== previousRuntimeIdentity)) {
|
|
560
|
-
return status;
|
|
561
|
-
}
|
|
562
|
-
lastError = new Error(
|
|
563
|
-
previousRuntimeIdentity && status?.runtimeIdentity === previousRuntimeIdentity ? "retiring Mobile runtime is still selected" : "selected Mobile runtime is not ready"
|
|
564
|
-
);
|
|
565
|
-
} catch (error) {
|
|
566
|
-
lastError = error;
|
|
567
|
-
}
|
|
568
|
-
const remainingMs = deadline - Date.now();
|
|
569
|
-
if (remainingMs > 0) {
|
|
570
|
-
await new Promise(
|
|
571
|
-
(resolve) => setTimeout(resolve, Math.min(1e3, remainingMs))
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
throw new Error(
|
|
576
|
-
`Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${Math.max(0, deadline - startedAt)}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
async function readMobileLifecycleRuntimeIdentity(node, context) {
|
|
580
|
-
const deadline = Date.now() + 5e3;
|
|
581
|
-
let lastError;
|
|
582
|
-
do {
|
|
583
|
-
try {
|
|
584
|
-
const status = await bridgeCommand(
|
|
585
|
-
{
|
|
586
|
-
action: "app.lifecycle",
|
|
587
|
-
node: {
|
|
588
|
-
...node,
|
|
589
|
-
cdp_timeout_ms: Math.min(1e3, Math.max(1, deadline - Date.now()))
|
|
590
|
-
},
|
|
591
|
-
context
|
|
592
|
-
},
|
|
593
|
-
["target-identity-selected"]
|
|
594
|
-
);
|
|
595
|
-
if (typeof status?.runtimeIdentity === "string") {
|
|
596
|
-
return status.runtimeIdentity;
|
|
597
|
-
}
|
|
598
|
-
lastError = new Error("selected Mobile runtime has no stable identity");
|
|
599
|
-
} catch (error) {
|
|
600
|
-
if (error instanceof Error && "code" in error && error.code === MOBILE_BRIDGE_ERROR_CODES.NO_TARGET) {
|
|
601
|
-
return void 0;
|
|
602
|
-
}
|
|
603
|
-
lastError = error;
|
|
604
|
-
}
|
|
605
|
-
const remainingMs = deadline - Date.now();
|
|
606
|
-
if (remainingMs <= 0) {
|
|
607
|
-
break;
|
|
608
|
-
}
|
|
609
|
-
await new Promise(
|
|
610
|
-
(resolve) => setTimeout(resolve, Math.min(250, remainingMs))
|
|
611
|
-
);
|
|
612
|
-
} while (Date.now() < deadline);
|
|
613
|
-
throw new Error(
|
|
614
|
-
`Mobile lifecycle restart could not identify the retiring runtime: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
function mobileLifecycleReadinessDeadline(command, node) {
|
|
618
|
-
const timeoutMs = resolveMobileLifecycleReadinessTimeout(
|
|
619
|
-
command,
|
|
620
|
-
node.runtime_ready_timeout_ms
|
|
621
|
-
);
|
|
622
|
-
return Date.now() + timeoutMs;
|
|
623
|
-
}
|
|
624
|
-
function resolveMobileLifecycleReadinessTimeout(command, value) {
|
|
625
|
-
if (value === void 0) {
|
|
626
|
-
return command === "launch" || command === "restart" || command === "foreground" ? 6e4 : 3e4;
|
|
627
|
-
}
|
|
628
|
-
const timeout = Number(value);
|
|
629
|
-
if (!Number.isInteger(timeout) || timeout < 1e3 || timeout > 12e4) {
|
|
630
|
-
throw new Error(
|
|
631
|
-
"app.lifecycle.runtime_ready_timeout_ms must be an integer from 1000 through 120000."
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
return timeout;
|
|
635
|
-
}
|
|
636
|
-
function asRecord(value) {
|
|
637
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
638
|
-
}
|
|
639
294
|
function runnerProvenance() {
|
|
640
295
|
return {
|
|
641
296
|
source: runnerDir,
|
|
@@ -659,8 +314,5 @@ export {
|
|
|
659
314
|
createMetaMaskMobileRunner,
|
|
660
315
|
createMetaMaskRunner,
|
|
661
316
|
isAutomaticHudProgress,
|
|
662
|
-
mobileSourceAwareLifecycleAdapters,
|
|
663
|
-
resolveMobileLifecycleReadinessTimeout,
|
|
664
|
-
withMobileNativeRuntimeIdentity,
|
|
665
317
|
withTrustedMutationContext
|
|
666
318
|
};
|
package/dist/runtime-context.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
7
|
import { recipeRuntimeDir } from "./paths.js";
|
|
7
8
|
function ensureRuntimeContext(target, adapter) {
|
|
8
9
|
const repoRoot = fs.realpathSync(target);
|
|
@@ -91,7 +92,7 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
|
91
92
|
if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
const forbiddenResources = adapter
|
|
95
|
+
const forbiddenResources = getAdapterSurface(adapter).runtimeContext.forbiddenFields;
|
|
95
96
|
if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
|
|
96
97
|
return true;
|
|
97
98
|
}
|
|
@@ -104,11 +105,12 @@ function localDefaults(repoRoot, adapter, existing) {
|
|
|
104
105
|
const digest = createHash("sha256").update(repoRoot).digest("hex");
|
|
105
106
|
const offset = Number.parseInt(digest.slice(0, 8), 16) % 1e4;
|
|
106
107
|
const defaults = { slotId: `local-${adapter}-${digest.slice(0, 8)}` };
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const localPorts = getAdapterSurface(adapter).runtimeContext.localPorts ?? {};
|
|
109
|
+
if (localPorts.cdp !== void 0 && !numericEnv("RECIPE_CDP_PORT", "CDP_PORT") && !existing.cdpPort) {
|
|
110
|
+
defaults.cdpPort = claimLocalPort(repoRoot, "cdp", localPorts.cdp + offset);
|
|
109
111
|
}
|
|
110
|
-
if (
|
|
111
|
-
defaults.watcherPort = claimLocalPort(repoRoot, "watcher",
|
|
112
|
+
if (localPorts.watcher !== void 0 && !numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") && !existing.watcherPort && !existing.metroPort && !existing.devServerPort) {
|
|
113
|
+
defaults.watcherPort = claimLocalPort(repoRoot, "watcher", localPorts.watcher + offset);
|
|
112
114
|
}
|
|
113
115
|
return defaults;
|
|
114
116
|
}
|
|
@@ -158,21 +160,14 @@ function portClaimIsLive(claim, port) {
|
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
function runtimeResources(adapter, existing, defaults) {
|
|
161
|
-
|
|
163
|
+
const { resources } = getAdapterSurface(adapter).runtimeContext;
|
|
164
|
+
if (!resources) return {};
|
|
162
165
|
const watcherPort = numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") ?? existing.watcherPort ?? existing.metroPort ?? existing.devServerPort ?? defaults.watcherPort;
|
|
163
|
-
|
|
166
|
+
return {
|
|
164
167
|
watcherPort,
|
|
165
|
-
devServerPort: watcherPort
|
|
168
|
+
devServerPort: watcherPort,
|
|
169
|
+
...resources({ watcherPort, existing, defaults, envPort: numericEnv })
|
|
166
170
|
};
|
|
167
|
-
if (adapter === "extension" || adapter === "terminal") {
|
|
168
|
-
shared.cdpPort = numericEnv("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort;
|
|
169
|
-
} else {
|
|
170
|
-
shared.metroPort = watcherPort;
|
|
171
|
-
shared.simulator = process.env.IOS_SIMULATOR || existing.simulator;
|
|
172
|
-
shared.simulatorUdid = process.env.SIM_UDID || existing.simulatorUdid;
|
|
173
|
-
shared.adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || existing.adbSerial;
|
|
174
|
-
}
|
|
175
|
-
return shared;
|
|
176
171
|
}
|
|
177
172
|
function numericEnv(...names) {
|
|
178
173
|
for (const name of names) {
|