@deeeed/metamask-harness 0.5.0 → 0.6.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 +51 -20
- package/adapters/core/inject.sh +1 -0
- package/adapters/extension/cleanup.mjs +1 -1
- package/adapters/extension/inject.mjs +4 -1
- package/adapters/extension/launch-browser.cjs +18 -4
- package/adapters/extension/live.sh +14 -2
- package/adapters/extension/refresh-build.sh +2 -2
- package/adapters/extension/sidepanel-toggle.sh +33 -12
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/wallet-fixture-state.cjs +38 -24
- package/adapters/manifest.json +32 -3
- package/adapters/mobile/inject.sh +1 -0
- package/adapters/mobile/start-metro.sh +1 -1
- package/adapters/mobile/stop-metro.sh +20 -0
- package/adapters/mobile/verify.sh +1 -1
- package/adapters/shared/activate-repo-node.sh +1 -1
- package/adapters/shared/cli-ux.sh +44 -25
- package/adapters/shared/ensure-runner-deps.sh +30 -0
- package/adapters/shared/log-tui.mjs +4 -4
- package/adapters/shared/open-debug.mjs +1 -1
- package/adapters/shared/reap-checkout-metros.sh +53 -0
- package/adapters/shared/recipe-harness-root.mjs +23 -0
- package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
- package/adapters/shared/resolve-farmslot-ports.sh +19 -126
- package/bin/mm-harness +45 -4
- package/dist/adapters/extension/runtime.js +3 -1
- package/dist/adapters/mobile/provision.js +34 -1
- package/dist/adapters/mobile/runtime-decision.js +3 -1
- package/dist/adapters/resolve-farmslot-ports.js +22 -0
- package/dist/adapters/slot-ports.js +18 -29
- package/dist/cli.js +32 -1463
- package/dist/commands/call.js +183 -0
- package/dist/commands/completion-candidates.js +58 -0
- package/dist/commands/doctor.js +101 -0
- package/dist/commands/ensure-ready.js +24 -0
- package/dist/commands/flows.js +62 -0
- package/dist/commands/launch/extension.js +40 -0
- package/dist/commands/{launch.js → launch/index.js} +15 -47
- package/dist/commands/launch/mobile.js +10 -0
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/parse-args.js +189 -0
- package/dist/commands/provision.js +136 -0
- package/dist/commands/resolve-extension.js +23 -0
- package/dist/commands/run-engine.js +341 -0
- package/dist/commands/run.js +217 -0
- package/dist/commands/runtime-decision.js +58 -0
- package/dist/commands/runtime-health.js +25 -0
- package/dist/commands/runtime-launch.js +139 -0
- package/dist/commands/self-test.js +52 -0
- package/dist/commands/stop.js +52 -0
- package/dist/harness.js +8 -48
- package/dist/mm-harness-cli.js +13 -8
- package/docs/CLI-SPEC.md +1 -1
- package/docs/CODE-MAP.md +62 -0
- package/library/README.md +14 -0
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -2
- package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
- package/library/actions/harness-exports.mjs +27 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
- package/library/actions/mobile/wallet/setup.mjs +1 -1
- package/package.json +6 -7
- package/src/adapters/core/surface.ts +0 -71
- package/src/adapters/extension/ensure-ready.ts +0 -185
- package/src/adapters/extension/extension-id.ts +0 -107
- package/src/adapters/extension/runtime-decision.ts +0 -445
- package/src/adapters/extension/runtime.ts +0 -407
- package/src/adapters/extension/surface.ts +0 -88
- package/src/adapters/mobile/deps-markers.ts +0 -21
- package/src/adapters/mobile/prepare.ts +0 -246
- package/src/adapters/mobile/provision.ts +0 -594
- package/src/adapters/mobile/runtime-decision.ts +0 -459
- package/src/adapters/mobile/surface.ts +0 -71
- package/src/adapters/slot-ports.ts +0 -165
- package/src/adapters/surface.ts +0 -117
- package/src/adapters.ts +0 -601
- package/src/cli-color.ts +0 -92
- package/src/cli-commands.ts +0 -250
- package/src/cli-version.ts +0 -141
- package/src/cli.ts +0 -2091
- package/src/commands/debug.ts +0 -65
- package/src/commands/fixtures.ts +0 -198
- package/src/commands/launch.ts +0 -470
- package/src/commands/logs.ts +0 -99
- package/src/commands/shared.ts +0 -235
- package/src/commands/update.ts +0 -316
- package/src/completions-cache.ts +0 -86
- package/src/doctor.ts +0 -215
- package/src/harness.ts +0 -797
- package/src/heal-bounds.ts +0 -198
- package/src/index.ts +0 -15
- package/src/leaf-invoke.ts +0 -28
- package/src/live-adapter-contract.ts +0 -274
- package/src/manifest.ts +0 -47
- package/src/mm-harness-cli.ts +0 -655
- package/src/paths.ts +0 -198
- package/src/progress.ts +0 -117
- package/src/recording-target.ts +0 -147
- package/src/run-recording.ts +0 -329
- package/src/runner.ts +0 -108
- package/src/types.ts +0 -57
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { loadActionManifest } from "../manifest.js";
|
|
6
|
+
import { importRecipeProtocol } from "../paths.js";
|
|
7
|
+
import { recipeRunning } from "../heal-bounds.js";
|
|
8
|
+
import { EXIT } from "./shared.js";
|
|
9
|
+
import {
|
|
10
|
+
parseArgs,
|
|
11
|
+
optionFlag,
|
|
12
|
+
optionString,
|
|
13
|
+
resolveAdapter,
|
|
14
|
+
runtimeOptionsFromCli,
|
|
15
|
+
usageError
|
|
16
|
+
} from "./parse-args.js";
|
|
17
|
+
import {
|
|
18
|
+
emitHealViolation,
|
|
19
|
+
executeWithHealBounds,
|
|
20
|
+
prepareHeal,
|
|
21
|
+
resolveMetaMaskLibrarySources,
|
|
22
|
+
runRecipe,
|
|
23
|
+
synthesizeOneNodeRecipe,
|
|
24
|
+
validateRecipeAdapterAware
|
|
25
|
+
} from "./run-engine.js";
|
|
26
|
+
async function handleCall(argv) {
|
|
27
|
+
if (argv.length > 0 && argv[0].startsWith("--")) {
|
|
28
|
+
const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
|
|
29
|
+
console.error(message);
|
|
30
|
+
return EXIT.usage;
|
|
31
|
+
}
|
|
32
|
+
const { action: shortName, args, rest } = parseCallArgs(argv);
|
|
33
|
+
const { options } = parseArgs(rest, "call");
|
|
34
|
+
const json = optionFlag(options, "json");
|
|
35
|
+
if (!shortName) {
|
|
36
|
+
let example = "mm-harness call <action>";
|
|
37
|
+
let discovery = "mm-harness actions";
|
|
38
|
+
try {
|
|
39
|
+
const { adapter: adapter2 } = resolveAdapter(options);
|
|
40
|
+
const manifest2 = loadActionManifest(adapter2, optionString(options, "actionManifest"));
|
|
41
|
+
const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
|
|
42
|
+
const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
|
|
43
|
+
example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
|
|
44
|
+
discovery = `mm-harness actions --adapter ${adapter2}`;
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
const message = `call requires <action>. Example: ${example}`;
|
|
48
|
+
const userAction = `${example} # see the vocabulary: ${discovery}`;
|
|
49
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", error: { code: "USAGE", message, userAction } }, null, 2));
|
|
50
|
+
else console.error(`${message}
|
|
51
|
+
See the vocabulary: ${discovery}`);
|
|
52
|
+
return EXIT.usage;
|
|
53
|
+
}
|
|
54
|
+
const { adapter, target } = resolveAdapter(options);
|
|
55
|
+
if (recipeRunning(target)) {
|
|
56
|
+
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
57
|
+
if (json) {
|
|
58
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
|
|
59
|
+
} else {
|
|
60
|
+
console.error(`\u2717 mm-harness call: ${msg}`);
|
|
61
|
+
}
|
|
62
|
+
return EXIT.bounded;
|
|
63
|
+
}
|
|
64
|
+
const actionManifestOverride = optionString(options, "actionManifest");
|
|
65
|
+
const manifest = loadActionManifest(adapter, actionManifestOverride);
|
|
66
|
+
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
67
|
+
const names = getRecipeActionManifestActionNames(manifest);
|
|
68
|
+
const resolution = resolveActionName(shortName, names);
|
|
69
|
+
if (resolution.status === "unknown") {
|
|
70
|
+
const message = `\u2717 call: unknown action "${shortName}" for the ${adapter} adapter.
|
|
71
|
+
See the vocabulary: mm-harness actions --adapter ${adapter} --json`;
|
|
72
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message } }, null, 2));
|
|
73
|
+
else console.error(message);
|
|
74
|
+
return EXIT.usage;
|
|
75
|
+
}
|
|
76
|
+
if (resolution.status === "ambiguous") {
|
|
77
|
+
const message = `\u2717 call: "${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
|
|
78
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates } }, null, 2));
|
|
79
|
+
else console.error(message);
|
|
80
|
+
return EXIT.usage;
|
|
81
|
+
}
|
|
82
|
+
const resolvedAction = resolution.resolved;
|
|
83
|
+
const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
|
|
84
|
+
const validation = await validateRecipeAdapterAware(recipe, manifest);
|
|
85
|
+
if (validation.status === "invalid") {
|
|
86
|
+
const message = `\u2717 call ${resolvedAction}: recipe validation failed`;
|
|
87
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message } }, null, 2));
|
|
88
|
+
else {
|
|
89
|
+
console.error(message);
|
|
90
|
+
for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
91
|
+
}
|
|
92
|
+
return EXIT.validation;
|
|
93
|
+
}
|
|
94
|
+
const prepared = await prepareHeal(adapter, target, options, json);
|
|
95
|
+
if (typeof prepared === "number") return prepared;
|
|
96
|
+
const { state, heal } = prepared;
|
|
97
|
+
const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
|
|
98
|
+
const recipeFile = path.join(scratch, "call.recipe.json");
|
|
99
|
+
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
100
|
+
`);
|
|
101
|
+
const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
|
|
102
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
103
|
+
const callRuntimeOptions = {
|
|
104
|
+
...runtimeOptionsFromCli(options),
|
|
105
|
+
...librarySources ? { librarySources } : {},
|
|
106
|
+
stdoutIsMachineContract: json
|
|
107
|
+
};
|
|
108
|
+
const { result, violation } = await executeWithHealBounds(
|
|
109
|
+
() => runRecipe(adapter, recipeFile, artifactsDir, target, actionManifestOverride, callRuntimeOptions),
|
|
110
|
+
adapter,
|
|
111
|
+
target,
|
|
112
|
+
heal,
|
|
113
|
+
state
|
|
114
|
+
);
|
|
115
|
+
if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
|
|
116
|
+
if (json) {
|
|
117
|
+
console.log(
|
|
118
|
+
JSON.stringify(
|
|
119
|
+
{
|
|
120
|
+
schemaVersion: 1,
|
|
121
|
+
command: "call",
|
|
122
|
+
adapter,
|
|
123
|
+
action: shortName,
|
|
124
|
+
resolvedAction,
|
|
125
|
+
args,
|
|
126
|
+
status: result.status,
|
|
127
|
+
summaryPath: result.summaryPath,
|
|
128
|
+
tracePath: result.tracePath,
|
|
129
|
+
artifactManifestPath: result.artifactManifestPath,
|
|
130
|
+
recovered: state.recovered,
|
|
131
|
+
mutations: state.mutations,
|
|
132
|
+
exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
|
|
133
|
+
},
|
|
134
|
+
null,
|
|
135
|
+
2
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
console.log(`call ${resolvedAction}: ${result.status}
|
|
140
|
+
Artifacts: ${result.artifactManifestPath}`);
|
|
141
|
+
}
|
|
142
|
+
return result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
143
|
+
}
|
|
144
|
+
function parseCallArgs(argv) {
|
|
145
|
+
const args = {};
|
|
146
|
+
const rest = [];
|
|
147
|
+
let action;
|
|
148
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
149
|
+
const arg = argv[i];
|
|
150
|
+
if (arg === "--arg" || arg.startsWith("--arg=")) {
|
|
151
|
+
const pair = arg === "--arg" ? argv[i += 1] : arg.slice("--arg=".length);
|
|
152
|
+
if (pair === void 0) throw usageError("--arg requires k=v.");
|
|
153
|
+
const eq = pair.indexOf("=");
|
|
154
|
+
if (eq === -1) throw usageError(`--arg must be k=v: ${pair}`);
|
|
155
|
+
args[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!arg.startsWith("--") && action === void 0) {
|
|
159
|
+
action = arg;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
rest.push(arg);
|
|
163
|
+
}
|
|
164
|
+
return { action, args, rest };
|
|
165
|
+
}
|
|
166
|
+
function resolveActionName(shortName, names) {
|
|
167
|
+
if (names.includes(shortName)) return { status: "ok", resolved: shortName, candidates: [shortName] };
|
|
168
|
+
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
169
|
+
const exactSegment = names.filter((name) => finalSegment(name) === shortName);
|
|
170
|
+
const tier = exactSegment.length > 0 ? exactSegment : names.filter((name) => finalSegment(name).includes(shortName));
|
|
171
|
+
if (tier.length === 1) return { status: "ok", resolved: tier[0], candidates: tier };
|
|
172
|
+
if (tier.length > 1) return { status: "ambiguous", resolved: "", candidates: tier };
|
|
173
|
+
return { status: "unknown", resolved: "", candidates: [] };
|
|
174
|
+
}
|
|
175
|
+
function pickCallExampleAction(names) {
|
|
176
|
+
const walletish = names.find((name) => /wallet|unlock/u.test(name));
|
|
177
|
+
if (walletish) return walletish;
|
|
178
|
+
if (names.includes("command")) return "command";
|
|
179
|
+
return names[0] ?? "command";
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
handleCall
|
|
183
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { loadActionManifest } from "../manifest.js";
|
|
2
|
+
import {
|
|
3
|
+
invalidateCompletionCache,
|
|
4
|
+
readFreshCandidates,
|
|
5
|
+
writeCompletionCandidates
|
|
6
|
+
} from "../completions-cache.js";
|
|
7
|
+
import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
|
|
8
|
+
import { EXIT } from "./shared.js";
|
|
9
|
+
import {
|
|
10
|
+
parseArgs,
|
|
11
|
+
optionFlag,
|
|
12
|
+
optionString,
|
|
13
|
+
resolveAdapter,
|
|
14
|
+
targetPath
|
|
15
|
+
} from "./parse-args.js";
|
|
16
|
+
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
17
|
+
async function handleCompletionCandidates(argv) {
|
|
18
|
+
const kind = argv[0];
|
|
19
|
+
const { options } = parseArgs(argv.slice(1), "completion-candidates");
|
|
20
|
+
const json = optionFlag(options, "json");
|
|
21
|
+
if (kind !== "actions" && kind !== "flows") {
|
|
22
|
+
console.error("completion-candidates requires <actions|flows>.");
|
|
23
|
+
return EXIT.usage;
|
|
24
|
+
}
|
|
25
|
+
const target = targetPath(options);
|
|
26
|
+
let adapter;
|
|
27
|
+
let candidates;
|
|
28
|
+
if (kind === "actions") {
|
|
29
|
+
adapter = resolveAdapter(options).adapter;
|
|
30
|
+
candidates = readFreshCandidates(target, "actions");
|
|
31
|
+
if (!candidates) {
|
|
32
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
33
|
+
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
34
|
+
candidates = getRecipeActionManifestActionNames(manifest);
|
|
35
|
+
writeCompletionCandidates(target, "actions", candidates);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
candidates = readFreshCandidates(target, "flows");
|
|
39
|
+
if (!candidates) {
|
|
40
|
+
const sources = await resolveMetaMaskLibrarySources(void 0).catch(() => void 0);
|
|
41
|
+
if (!sources) {
|
|
42
|
+
candidates = [];
|
|
43
|
+
} else {
|
|
44
|
+
const harness = await importRecipeHarness();
|
|
45
|
+
const resolution = await harness.loadRecipeLibraries(sources);
|
|
46
|
+
candidates = [...resolution.flows.keys()].sort();
|
|
47
|
+
writeCompletionCandidates(target, "flows", candidates);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, kind, adapter, candidates }, null, 2));
|
|
52
|
+
else for (const candidate of candidates) console.log(candidate);
|
|
53
|
+
return EXIT.ok;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
handleCompletionCandidates,
|
|
57
|
+
invalidateCompletionCache
|
|
58
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { color } from "../cli-color.js";
|
|
4
|
+
import { createDoctorReport, renderRuntimeContext } from "../doctor.js";
|
|
5
|
+
import { detectAdapter } from "../harness.js";
|
|
6
|
+
import { assertAdapter, runnerDir } from "../paths.js";
|
|
7
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
8
|
+
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
9
|
+
import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
|
|
10
|
+
import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
|
|
11
|
+
import {
|
|
12
|
+
actionManifestPathOption,
|
|
13
|
+
applyRuntimeDirOption,
|
|
14
|
+
optionFlag,
|
|
15
|
+
optionString,
|
|
16
|
+
targetPath
|
|
17
|
+
} from "./parse-args.js";
|
|
18
|
+
async function handleDoctor({ options }) {
|
|
19
|
+
applyRuntimeDirOption(options);
|
|
20
|
+
const target = targetPath(options);
|
|
21
|
+
const json = optionFlag(options, "json");
|
|
22
|
+
const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
|
|
23
|
+
const adapter = explicitAdapter ?? detectAdapter(target);
|
|
24
|
+
if (!adapter) {
|
|
25
|
+
return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
26
|
+
}
|
|
27
|
+
assertAdapter(adapter);
|
|
28
|
+
const actionManifestPath = actionManifestPathOption(options, adapter);
|
|
29
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
30
|
+
const manifestValidation = await validateManifest(manifest);
|
|
31
|
+
if (optionFlag(options, "fix")) {
|
|
32
|
+
const { fixed, failed } = await runDoctorFix(adapter, target, manifestValidation, json);
|
|
33
|
+
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
34
|
+
if (json) console.log(JSON.stringify({ ...result2, fixed, failed }, null, 2));
|
|
35
|
+
else console.log(`${result2.status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
36
|
+
return failed.length === 0 ? 0 : 1;
|
|
37
|
+
}
|
|
38
|
+
const result = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
39
|
+
let runtime;
|
|
40
|
+
try {
|
|
41
|
+
const surface = getAdapterSurface(adapter);
|
|
42
|
+
surface.resolveSlotPorts(target);
|
|
43
|
+
runtime = await surface.runtimeStatus(target);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target) : [];
|
|
47
|
+
if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros }, null, 2));
|
|
48
|
+
else {
|
|
49
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
50
|
+
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
51
|
+
console.log(`${out(result.status === "pass" ? "ok" : "err", result.status)} ${out("bold", adapter)} ${result.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
|
|
52
|
+
if (runtime) {
|
|
53
|
+
const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
|
|
54
|
+
const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
|
|
55
|
+
const depsStyle = provisionedDepsPending ? "info" : stateStyle(runtime.deps, "current");
|
|
56
|
+
const devServerStyle = provisionedDepsPending ? "info" : void 0;
|
|
57
|
+
const devServer = runtime.devServer ? ` ${runtime.devServer.label}=${out(devServerStyle ?? stateStyle(runtime.devServer.status, "up"), runtime.devServer.status)}` : "";
|
|
58
|
+
console.log(
|
|
59
|
+
`${out("label", "runtime:")} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out("dim", `(${runtime.reasonCode})`)}` : ""} deps=${out(depsStyle, runtime.deps ?? "unknown")}${devServer}`
|
|
60
|
+
);
|
|
61
|
+
for (const reason of runtime.reasons) console.log(` ${out("dim", reason)}`);
|
|
62
|
+
}
|
|
63
|
+
console.log(renderRuntimeContext(result.runtimeContext));
|
|
64
|
+
if (orphanMetros.length > 0) {
|
|
65
|
+
console.log(
|
|
66
|
+
`${out("warn", "orphan Metro:")} ${orphanMetros.length} leaked bundler(s) for this checkout (pids ${orphanMetros.join(", ")})`
|
|
67
|
+
);
|
|
68
|
+
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result.status === "pass" ? 0 : 1;
|
|
72
|
+
}
|
|
73
|
+
function detectOrphanMetros(target) {
|
|
74
|
+
try {
|
|
75
|
+
const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
|
|
76
|
+
const out = execFileSync("bash", ["-c", `. "$1"; detect_checkout_metros "$2"`, "mm-harness-doctor", script, target], {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
timeout: 5e3
|
|
79
|
+
});
|
|
80
|
+
return out.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/u.test(line));
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function runDoctorFix(adapter, target, manifestValidation, json) {
|
|
86
|
+
const fixed = [];
|
|
87
|
+
const failed = [];
|
|
88
|
+
if (recipeRunning(target)) {
|
|
89
|
+
failed.push("recipe-running");
|
|
90
|
+
return { fixed, failed };
|
|
91
|
+
}
|
|
92
|
+
if (Number(manifestValidation.summary?.errors ?? 0) > 0) failed.push("manifest");
|
|
93
|
+
const state = newHealState();
|
|
94
|
+
const ensured = await ensureOverlay(adapter, target, "infra-only", state, json);
|
|
95
|
+
if (!ensured.ok) failed.push("overlay");
|
|
96
|
+
else if (state.mutations.length > 0) fixed.push("overlay");
|
|
97
|
+
return { fixed, failed };
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
handleDoctor
|
|
101
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ensureExtensionReady } from "../adapters/extension/ensure-ready.js";
|
|
2
|
+
import {
|
|
3
|
+
adapterOption,
|
|
4
|
+
optionFlag,
|
|
5
|
+
optionString,
|
|
6
|
+
parsePort,
|
|
7
|
+
targetPath
|
|
8
|
+
} from "./parse-args.js";
|
|
9
|
+
async function handleEnsureReady({ options }) {
|
|
10
|
+
const adapter = adapterOption(options);
|
|
11
|
+
if (adapter !== "extension") throw new Error("ensure-ready currently applies to the extension adapter.");
|
|
12
|
+
const target = targetPath(options);
|
|
13
|
+
const cdpPort = parsePort(
|
|
14
|
+
optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
|
|
15
|
+
"ensure-ready requires --cdp-port <port>."
|
|
16
|
+
);
|
|
17
|
+
const result = await ensureExtensionReady(target, { cdpPort });
|
|
18
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
|
|
19
|
+
else console.log(`${result.ready ? "READY" : "NOT-READY"} ${result.reasonCode} \u2014 id=${result.extensionId} homeTabs ${result.homeTabs.before}\u2192${result.homeTabs.after} (closed ${result.homeTabs.closed})`);
|
|
20
|
+
return result.ready ? 0 : 1;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
handleEnsureReady
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { color } from "../cli-color.js";
|
|
3
|
+
import { importRecipeHarnessCli } from "../paths.js";
|
|
4
|
+
import { EXIT } from "./shared.js";
|
|
5
|
+
import { usageError } from "./parse-args.js";
|
|
6
|
+
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
7
|
+
const FLOWS_SUBCOMMANDS = ["list", "promote"];
|
|
8
|
+
function stripTargetFlag(argv) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg === "--target") {
|
|
13
|
+
i += 1;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (arg.startsWith("--target=")) continue;
|
|
17
|
+
out.push(arg);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function serializeLibrarySources(sources) {
|
|
22
|
+
return sources.map((source) => source.name ? `${source.name}=${source.root}` : source.root).join(":");
|
|
23
|
+
}
|
|
24
|
+
function flowSourceTier(source, index, total) {
|
|
25
|
+
if (index === total - 1 && source.name === "metamask") return "canonical";
|
|
26
|
+
if (source.name === "personal") return "personal";
|
|
27
|
+
return "team";
|
|
28
|
+
}
|
|
29
|
+
function restoreEnv(key, value) {
|
|
30
|
+
if (value === void 0) delete process.env[key];
|
|
31
|
+
else process.env[key] = value;
|
|
32
|
+
}
|
|
33
|
+
async function handleFlows(argv) {
|
|
34
|
+
const sources = await resolveMetaMaskLibrarySources(void 0);
|
|
35
|
+
if (!sources) {
|
|
36
|
+
throw usageError(
|
|
37
|
+
"flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support; flows resolve personal > team > canonical). Next: update the dependency and run yarn install."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const forwarded = stripTargetFlag(argv);
|
|
41
|
+
const jsonMode = forwarded.includes("--json");
|
|
42
|
+
if (!jsonMode) {
|
|
43
|
+
console.error(color("label", "flows resolve by precedence (highest wins; local shadows canonical):"));
|
|
44
|
+
sources.forEach((source, index) => {
|
|
45
|
+
const name = source.name ?? path.basename(source.root);
|
|
46
|
+
console.error(` ${index + 1}. ${color("cmd", name)} ${color("dim", `[${flowSourceTier(source, index, sources.length)}]`)}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const withSubcommand = FLOWS_SUBCOMMANDS.includes(forwarded[0]) ? forwarded : ["list", ...forwarded];
|
|
50
|
+
const { runRecipeHarnessCli } = await importRecipeHarnessCli();
|
|
51
|
+
const previousLibraryPath = process.env.RECIPE_LIBRARY_PATH;
|
|
52
|
+
process.env.RECIPE_LIBRARY_PATH = serializeLibrarySources(sources);
|
|
53
|
+
try {
|
|
54
|
+
await runRecipeHarnessCli(["flows", ...withSubcommand]);
|
|
55
|
+
} finally {
|
|
56
|
+
restoreEnv("RECIPE_LIBRARY_PATH", previousLibraryPath);
|
|
57
|
+
}
|
|
58
|
+
return EXIT.ok;
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
handleFlows
|
|
62
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
4
|
+
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
5
|
+
import { spawnScriptStreaming } from "../shared.js";
|
|
6
|
+
async function launchExtension(target, tier, wantWatch) {
|
|
7
|
+
if (!wantWatch && tier === "quick") {
|
|
8
|
+
return extensionRebuild(target);
|
|
9
|
+
}
|
|
10
|
+
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
11
|
+
const watchArgs = ["--target", target];
|
|
12
|
+
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
13
|
+
console.error(`\u2192 extension ${wantWatch ? "watch" : "build"} \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
|
|
14
|
+
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
15
|
+
}
|
|
16
|
+
async function extensionRebuild(target) {
|
|
17
|
+
const runtimeDirRel = recipeRuntimeDir();
|
|
18
|
+
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
19
|
+
const rebuildLog = path.join(runtimeAbs, "rebuild.log");
|
|
20
|
+
stopExtensionWatcher(target);
|
|
21
|
+
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
|
22
|
+
fs.writeFileSync(rebuildLog, "");
|
|
23
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
24
|
+
const liveArgs = ["--target", target, "--start-watch"];
|
|
25
|
+
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
26
|
+
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
27
|
+
console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
|
|
28
|
+
const result = await spawnScriptStreaming(liveScript, liveArgs, target);
|
|
29
|
+
if (result.output) {
|
|
30
|
+
try {
|
|
31
|
+
fs.appendFileSync(rebuildLog, result.output);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
extensionRebuild,
|
|
39
|
+
launchExtension
|
|
40
|
+
};
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { color } from "
|
|
5
|
-
import { handleHarness } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { getAdapterSurface } from "../adapters/surface.js";
|
|
9
|
-
import { stopExtensionWatcher } from "../adapters/slot-ports.js";
|
|
4
|
+
import { color } from "../../cli-color.js";
|
|
5
|
+
import { handleHarness } from "../../harness.js";
|
|
6
|
+
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
7
|
+
import { getAdapterSurface } from "../../adapters/surface.js";
|
|
10
8
|
import {
|
|
11
9
|
ADAPTER_DETECT_NEXT,
|
|
12
10
|
EXIT,
|
|
@@ -14,18 +12,19 @@ import {
|
|
|
14
12
|
parseFlags,
|
|
15
13
|
resolveAdapter,
|
|
16
14
|
spawnScript,
|
|
17
|
-
spawnScriptStreaming,
|
|
18
15
|
str,
|
|
19
16
|
targetOf,
|
|
20
17
|
usageOut
|
|
21
|
-
} from "
|
|
18
|
+
} from "../shared.js";
|
|
22
19
|
import {
|
|
23
20
|
RECOVERY_CODE,
|
|
24
21
|
checkHealBounds,
|
|
25
22
|
ensureOverlay,
|
|
26
23
|
newHealState,
|
|
27
24
|
parseHeal
|
|
28
|
-
} from "
|
|
25
|
+
} from "../../heal-bounds.js";
|
|
26
|
+
import { launchExtension } from "./extension.js";
|
|
27
|
+
import { launchMobile } from "./mobile.js";
|
|
29
28
|
const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
30
29
|
"build",
|
|
31
30
|
"watch",
|
|
@@ -84,6 +83,9 @@ async function handleLaunch(argv) {
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
86
|
+
if (adapter === "extension" && typeof options["url"] === "string" && options["url"]) {
|
|
87
|
+
process.env.EXTENSION_START_URL = options["url"];
|
|
88
|
+
}
|
|
87
89
|
const tier = wantVerify ? "verify" : wantBuild ? "build" : "quick";
|
|
88
90
|
const state = newHealState();
|
|
89
91
|
if (tier === "quick" && nativeInputsChanged(target, adapter)) {
|
|
@@ -107,8 +109,7 @@ async function handleLaunch(argv) {
|
|
|
107
109
|
const liveArgs = ["live", "--adapter", adapter, "--target", target];
|
|
108
110
|
if (adapter === "mobile" && mobileTarget) liveArgs.push("--platform", mobileTarget);
|
|
109
111
|
if (json) liveArgs.push("--json");
|
|
110
|
-
|
|
111
|
-
return code;
|
|
112
|
+
return handleHarness(liveArgs);
|
|
112
113
|
}
|
|
113
114
|
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
114
115
|
if (attempt.status === 0) {
|
|
@@ -194,47 +195,16 @@ function nativeInputsChanged(target, adapter) {
|
|
|
194
195
|
}
|
|
195
196
|
async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json) {
|
|
196
197
|
if (adapter === "mobile") {
|
|
197
|
-
|
|
198
|
-
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
199
|
-
const preflightMode = tier === "build" ? "auto" : "fast";
|
|
200
|
-
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
201
|
-
}
|
|
202
|
-
if (!wantWatch && tier === "quick") {
|
|
203
|
-
return extensionRebuild(target, json);
|
|
204
|
-
}
|
|
205
|
-
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
206
|
-
const watchArgs = ["--target", target];
|
|
207
|
-
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
208
|
-
console.error(`\u2192 extension ${wantWatch ? "watch" : "build"} \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
|
|
209
|
-
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
210
|
-
}
|
|
211
|
-
async function extensionRebuild(target, json) {
|
|
212
|
-
const runtimeDirRel = recipeRuntimeDir();
|
|
213
|
-
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
214
|
-
const rebuildLog = path.join(runtimeAbs, "rebuild.log");
|
|
215
|
-
stopExtensionWatcher(target);
|
|
216
|
-
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
|
217
|
-
fs.writeFileSync(rebuildLog, "");
|
|
218
|
-
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
219
|
-
const liveArgs = ["--target", target, "--start-watch"];
|
|
220
|
-
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
221
|
-
if (process.env.WATCHER_PORT) liveArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
222
|
-
console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
|
|
223
|
-
const result = await spawnScriptStreaming(liveScript, liveArgs, target);
|
|
224
|
-
if (result.output) {
|
|
225
|
-
try {
|
|
226
|
-
fs.appendFileSync(rebuildLog, result.output);
|
|
227
|
-
} catch {
|
|
228
|
-
}
|
|
198
|
+
return launchMobile(target, mobileTarget, tier, json);
|
|
229
199
|
}
|
|
230
|
-
return
|
|
200
|
+
return launchExtension(target, tier, wantWatch);
|
|
231
201
|
}
|
|
232
202
|
function finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state) {
|
|
233
203
|
if (adapter === "extension" && displayMode === "sidepanel") {
|
|
234
204
|
const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
|
|
235
205
|
const sidepanelArgs = ["open"];
|
|
236
206
|
if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
237
|
-
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
|
|
207
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json, { REPO: target });
|
|
238
208
|
if (sidepanel.status !== 0) {
|
|
239
209
|
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
240
210
|
code: "SIDEPANEL_OPEN_FAILED",
|
|
@@ -300,8 +270,6 @@ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
|
|
|
300
270
|
message: failure.message,
|
|
301
271
|
retryable: failure.recoverable,
|
|
302
272
|
userAction: failure.userAction ?? null,
|
|
303
|
-
// The classification note is in `message`; the REAL failure text lives
|
|
304
|
-
// here verbatim so it is never lost.
|
|
305
273
|
originalError: failure.originalError ?? null
|
|
306
274
|
}
|
|
307
275
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { prepareMobile } from "../../adapters/mobile/prepare.js";
|
|
2
|
+
async function launchMobile(target, mobileTarget, tier, json) {
|
|
3
|
+
const platform = mobileTarget ?? "ios";
|
|
4
|
+
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
5
|
+
const preflightMode = tier === "build" ? "auto" : "fast";
|
|
6
|
+
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
launchMobile
|
|
10
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
2
|
+
import {
|
|
3
|
+
actionManifestPathOption,
|
|
4
|
+
isRecord,
|
|
5
|
+
optionFlag,
|
|
6
|
+
optionString,
|
|
7
|
+
resolveAdapter
|
|
8
|
+
} from "./parse-args.js";
|
|
9
|
+
async function handleManifest({ options }) {
|
|
10
|
+
const { adapter } = resolveAdapter(options);
|
|
11
|
+
const actionManifestPath = actionManifestPathOption(options, adapter);
|
|
12
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
13
|
+
await validateManifest(manifest);
|
|
14
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
|
|
15
|
+
else console.log(actionManifestPath);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
async function handleActions({ options }) {
|
|
19
|
+
const { adapter } = resolveAdapter(options);
|
|
20
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
21
|
+
await validateManifest(manifest);
|
|
22
|
+
const action = optionString(options, "action");
|
|
23
|
+
const actions = describeManifestActions(manifest, action);
|
|
24
|
+
if (optionFlag(options, "json")) {
|
|
25
|
+
console.log(JSON.stringify({ adapter, actions }, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
for (const entry of actions) {
|
|
28
|
+
const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
|
|
29
|
+
console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
function describeManifestActions(manifest, filterAction) {
|
|
35
|
+
const manifestRecord = isRecord(manifest) ? manifest : {};
|
|
36
|
+
const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
|
|
37
|
+
const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
|
|
38
|
+
const custom = Array.isArray(manifestRecord.custom_actions) ? manifestRecord.custom_actions.flatMap((entry) => {
|
|
39
|
+
if (typeof entry === "string") return [{ name: entry, metadata: metadata[entry] }];
|
|
40
|
+
if (isRecord(entry) && typeof entry.name === "string") {
|
|
41
|
+
const entryMetadata = { ...entry };
|
|
42
|
+
const metadataOverride = metadata[entry.name];
|
|
43
|
+
if (isRecord(metadataOverride)) Object.assign(entryMetadata, metadataOverride);
|
|
44
|
+
return [{ name: entry.name, metadata: entryMetadata }];
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}) : [];
|
|
48
|
+
const entries = [
|
|
49
|
+
...official.map((name) => describeManifestAction(name, "official", metadata[name])),
|
|
50
|
+
...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata))
|
|
51
|
+
].filter((entry) => !filterAction || entry.name === filterAction);
|
|
52
|
+
if (filterAction && entries.length === 0) throw new Error(`Action not found in manifest: ${filterAction}`);
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
function describeManifestAction(name, kind, metadata) {
|
|
56
|
+
const record = isRecord(metadata) ? metadata : {};
|
|
57
|
+
const schema = record.schema;
|
|
58
|
+
const schemaRecord = isRecord(schema) ? schema : {};
|
|
59
|
+
const properties = isRecord(schemaRecord.properties) ? Object.keys(schemaRecord.properties).sort() : [];
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
kind,
|
|
63
|
+
description: typeof record.description === "string" ? record.description : "",
|
|
64
|
+
fields: properties,
|
|
65
|
+
schema,
|
|
66
|
+
examples: record.examples
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
handleActions,
|
|
71
|
+
handleManifest
|
|
72
|
+
};
|