@deeeed/metamask-harness 0.4.0 → 0.5.1
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 +33 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/refresh-build.sh +2 -2
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/start-metro.sh +1 -1
- package/adapters/mobile/verify.sh +1 -1
- package/adapters/shared/activate-repo-node.sh +1 -1
- package/adapters/shared/cli-ux.sh +24 -23
- package/adapters/shared/log-tui.mjs +4 -4
- package/adapters/shared/open-debug.mjs +1 -1
- package/adapters/shared/resolve-farmslot-ports-core.mjs +196 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
- package/adapters/shared/resolve-farmslot-ports.sh +19 -126
- package/dist/adapters/core/surface.js +53 -0
- package/dist/adapters/extension/ensure-ready.js +109 -0
- package/dist/adapters/extension/extension-id.js +62 -0
- package/dist/adapters/extension/runtime-decision.js +305 -0
- package/dist/adapters/extension/runtime.js +324 -0
- package/dist/adapters/extension/surface.js +69 -0
- package/dist/adapters/mobile/deps-markers.js +22 -0
- package/dist/adapters/mobile/prepare.js +146 -0
- package/dist/adapters/mobile/provision.js +465 -0
- package/dist/adapters/mobile/runtime-decision.js +317 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/resolve-farmslot-ports.js +22 -0
- package/dist/adapters/slot-ports.js +140 -0
- package/dist/adapters/surface.js +14 -0
- package/dist/adapters.js +485 -0
- package/dist/cli-color.js +79 -0
- package/dist/cli-commands.js +224 -0
- package/dist/cli-version.js +111 -0
- package/dist/cli.js +1571 -0
- package/dist/commands/debug.js +56 -0
- package/dist/commands/fixtures.js +153 -0
- package/dist/commands/launch.js +325 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/shared.js +157 -0
- package/dist/commands/update.js +243 -0
- package/dist/completions-cache.js +53 -0
- package/dist/doctor.js +169 -0
- package/dist/harness.js +627 -0
- package/dist/heal-bounds.js +120 -0
- package/dist/index.js +25 -0
- package/dist/leaf-invoke.js +19 -0
- package/dist/live-adapter-contract.js +240 -0
- package/dist/manifest.js +37 -0
- package/dist/mm-harness-cli.js +521 -0
- package/dist/paths.js +179 -0
- package/dist/progress.js +94 -0
- package/dist/recording-target.js +133 -0
- package/dist/run-recording.js +271 -0
- package/dist/runner.js +88 -0
- package/dist/types.js +0 -0
- package/docs/CLI-SPEC.md +26 -3
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +1 -1
- package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/harness-exports.mjs +27 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/wallet/setup.mjs +1 -1
- package/package.json +5 -1
- package/src/adapters/core/surface.ts +15 -0
- package/src/adapters/extension/surface.ts +20 -3
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/runtime-decision.ts +8 -1
- package/src/adapters/mobile/surface.ts +16 -4
- package/src/adapters/resolve-farmslot-ports.ts +13 -0
- package/src/adapters/slot-ports.ts +18 -25
- package/src/adapters/surface.ts +35 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +149 -6
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +52 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# resolve-farmslot-ports.sh — bash-compat wrapper over resolve-farmslot-ports.mjs
|
|
2
3
|
#
|
|
3
4
|
# Farmslot pool JSON is authoritative when a slot repo matches; otherwise fall back
|
|
4
5
|
# to the local-extension-N formula (6660+N / 9010+N).
|
|
@@ -8,85 +9,15 @@
|
|
|
8
9
|
# resolve_extension_runtime_ports /path/to/metamask-extension-N
|
|
9
10
|
# -> prints CDP_PORT=... WATCHER_PORT=... SLOT_ID=... lines
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
local farmslot_root="${FARMSLOT_ROOT:-}"
|
|
14
|
-
[ -n "$repo" ] || return 1
|
|
15
|
-
repo="$(cd "$repo" && pwd -P)"
|
|
16
|
-
|
|
17
|
-
local root candidate match
|
|
18
|
-
for root in "$farmslot_root" "$HOME/dev/farmslot"; do
|
|
19
|
-
[ -n "$root" ] && [ -d "$root/pool" ] || continue
|
|
20
|
-
match="$(POOL_ROOT="$root" REPO_TARGET="$repo" python3 - <<'PY'
|
|
21
|
-
import glob, json, os, re, sys
|
|
22
|
-
|
|
23
|
-
pool_root = os.environ["POOL_ROOT"]
|
|
24
|
-
repo = os.path.realpath(os.environ["REPO_TARGET"])
|
|
25
|
-
base = os.path.basename(repo)
|
|
26
|
-
slot_suffix = None
|
|
27
|
-
m = re.search(r"-(\d+)$", base)
|
|
28
|
-
if m:
|
|
29
|
-
slot_suffix = m.group(1)
|
|
30
|
-
|
|
31
|
-
def score_slot(slot):
|
|
32
|
-
resources = slot.get("resources") or {}
|
|
33
|
-
cdp = (resources.get("browser") or {}).get("cdp_port")
|
|
34
|
-
port = (resources.get("dev-server") or {}).get("port")
|
|
35
|
-
session = slot.get("session") or ""
|
|
36
|
-
slot_id = slot.get("id") or ""
|
|
37
|
-
score = 0
|
|
38
|
-
if cdp is not None:
|
|
39
|
-
score += 100
|
|
40
|
-
if port is not None:
|
|
41
|
-
score += 10
|
|
42
|
-
if slot_suffix:
|
|
43
|
-
if session == f"mme-{slot_suffix}":
|
|
44
|
-
score += 50
|
|
45
|
-
if slot_id.endswith(f"mme-{slot_suffix}") or f"-mme-{slot_suffix}" in slot_id:
|
|
46
|
-
score += 40
|
|
47
|
-
if "demo" not in slot_id.lower():
|
|
48
|
-
score += 5
|
|
49
|
-
return score, cdp, port, slot_id
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
_RESOLVE_PORTS_MJS="$SCRIPT_DIR/resolve-farmslot-ports.mjs"
|
|
50
14
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
continue
|
|
55
|
-
try:
|
|
56
|
-
data = json.load(open(path, encoding="utf-8"))
|
|
57
|
-
except Exception:
|
|
58
|
-
continue
|
|
59
|
-
for slot in data.get("slots") or []:
|
|
60
|
-
slot_repo = slot.get("repo") or ""
|
|
61
|
-
if not slot_repo:
|
|
62
|
-
continue
|
|
63
|
-
try:
|
|
64
|
-
if os.path.realpath(slot_repo) != repo:
|
|
65
|
-
continue
|
|
66
|
-
except Exception:
|
|
67
|
-
continue
|
|
68
|
-
scored = score_slot(slot)
|
|
69
|
-
if best is None or scored[0] > best[0]:
|
|
70
|
-
best = scored
|
|
71
|
-
|
|
72
|
-
if best is None:
|
|
73
|
-
sys.exit(1)
|
|
15
|
+
_resolve_ports_cli() {
|
|
16
|
+
node "$_RESOLVE_PORTS_MJS" "$1" "${2:-.}"
|
|
17
|
+
}
|
|
74
18
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
print(f"CDP_PORT={cdp}")
|
|
78
|
-
if port is not None:
|
|
79
|
-
print(f"WATCHER_PORT={port}")
|
|
80
|
-
if slot_id:
|
|
81
|
-
print(f"SLOT_ID={slot_id}")
|
|
82
|
-
sys.exit(0)
|
|
83
|
-
PY
|
|
84
|
-
)" || continue
|
|
85
|
-
[ -n "$match" ] || continue
|
|
86
|
-
printf '%s\n' "$match"
|
|
87
|
-
return 0
|
|
88
|
-
done
|
|
89
|
-
return 1
|
|
19
|
+
resolve_farmslot_ports_by_repo() {
|
|
20
|
+
_resolve_ports_cli resolve_farmslot_ports_by_repo "$1"
|
|
90
21
|
}
|
|
91
22
|
|
|
92
23
|
infer_extension_slot_suffix() {
|
|
@@ -100,27 +31,18 @@ infer_extension_slot_suffix() {
|
|
|
100
31
|
}
|
|
101
32
|
|
|
102
33
|
resolve_default_extension_ports() {
|
|
103
|
-
|
|
104
|
-
n="$(infer_extension_slot_suffix "$repo" || true)"
|
|
105
|
-
[ -n "$n" ] || return 1
|
|
106
|
-
printf 'CDP_PORT=%s\n' "$((6660 + n))"
|
|
107
|
-
printf 'WATCHER_PORT=%s\n' "$((9010 + n))"
|
|
108
|
-
printf 'SLOT_ID=local-extension-%s\n' "$n"
|
|
34
|
+
_resolve_ports_cli resolve_default_extension_ports "$1"
|
|
109
35
|
}
|
|
110
36
|
|
|
111
37
|
resolve_extension_runtime_ports() {
|
|
112
|
-
|
|
113
|
-
if resolve_farmslot_ports_by_repo "$repo"; then
|
|
114
|
-
return 0
|
|
115
|
-
fi
|
|
116
|
-
resolve_default_extension_ports "$repo"
|
|
38
|
+
_resolve_ports_cli resolve_extension_runtime_ports "${1:-.}"
|
|
117
39
|
}
|
|
118
40
|
|
|
119
41
|
apply_resolved_extension_ports() {
|
|
120
42
|
local repo="${1:-.}" line key val from_pool=false resolved=""
|
|
121
|
-
if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
43
|
+
if resolved="$(_resolve_ports_cli resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
122
44
|
from_pool=true
|
|
123
|
-
elif resolved="$(resolve_default_extension_ports "$repo" 2>/dev/null)"; then
|
|
45
|
+
elif resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)"; then
|
|
124
46
|
from_pool=false
|
|
125
47
|
else
|
|
126
48
|
return 0
|
|
@@ -143,52 +65,23 @@ apply_resolved_extension_ports() {
|
|
|
143
65
|
done <<< "$resolved"
|
|
144
66
|
}
|
|
145
67
|
|
|
146
|
-
# Mobile slot defaults: derive Metro port + iOS simulator name from directory suffix.
|
|
147
|
-
# Port formula: 8060 + N (slot 1 → 8061). iOS simulator: mm-N.
|
|
148
|
-
# Mirrors the extension convention (6660+N / 9010+N) for multi-slot mobile farms.
|
|
149
68
|
resolve_mobile_slot_defaults() {
|
|
150
|
-
|
|
151
|
-
n="$(infer_extension_slot_suffix "$repo" || true)"
|
|
152
|
-
[ -n "$n" ] || return 1
|
|
153
|
-
printf 'WATCHER_PORT=%s\n' "$((8060 + n))"
|
|
154
|
-
printf 'IOS_SIMULATOR=mm-%s\n' "$n"
|
|
155
|
-
printf 'SLOT_ID=local-mobile-%s\n' "$n"
|
|
69
|
+
_resolve_ports_cli resolve_mobile_slot_defaults "$1"
|
|
156
70
|
}
|
|
157
71
|
|
|
158
|
-
# Slot context the orchestrator's prepare wrote INTO the checkout — the
|
|
159
|
-
# authoritative device/port mapping for this repo (survives pool renames that
|
|
160
|
-
# break any naming formula). Highest-priority detection source.
|
|
161
72
|
resolve_mobile_runtime_context() {
|
|
162
|
-
|
|
163
|
-
ctx="$repo/${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}/agentic-runtime.json"
|
|
164
|
-
[ -f "$ctx" ] || return 1
|
|
165
|
-
node -e '
|
|
166
|
-
try {
|
|
167
|
-
const c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
168
|
-
if (!c.simulator && !c.metroPort) process.exit(1);
|
|
169
|
-
if (c.metroPort) console.log(`WATCHER_PORT=${c.metroPort}`);
|
|
170
|
-
if (c.simulator) console.log(`IOS_SIMULATOR=${c.simulator}`);
|
|
171
|
-
if (c.slotId) console.log(`SLOT_ID=${c.slotId}`);
|
|
172
|
-
} catch { process.exit(1); }
|
|
173
|
-
' "$ctx" 2>/dev/null
|
|
73
|
+
_resolve_ports_cli resolve_mobile_runtime_context "$1"
|
|
174
74
|
}
|
|
175
75
|
|
|
176
76
|
resolve_mobile_runtime_ports() {
|
|
177
|
-
|
|
178
|
-
if resolve_mobile_runtime_context "$repo"; then
|
|
179
|
-
return 0
|
|
180
|
-
fi
|
|
181
|
-
if resolve_farmslot_ports_by_repo "$repo"; then
|
|
182
|
-
return 0
|
|
183
|
-
fi
|
|
184
|
-
resolve_mobile_slot_defaults "$repo"
|
|
77
|
+
_resolve_ports_cli resolve_mobile_runtime_ports "${1:-.}"
|
|
185
78
|
}
|
|
186
79
|
|
|
187
80
|
apply_resolved_mobile_ports() {
|
|
188
81
|
local repo="${1:-.}" line key val from_pool=false resolved=""
|
|
189
|
-
if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
82
|
+
if resolved="$(_resolve_ports_cli resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
190
83
|
from_pool=true
|
|
191
|
-
elif resolved="$(resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
|
|
84
|
+
elif resolved="$(_resolve_ports_cli resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
|
|
192
85
|
from_pool=false
|
|
193
86
|
else
|
|
194
87
|
return 0
|
|
@@ -209,4 +102,4 @@ apply_resolved_mobile_ports() {
|
|
|
209
102
|
;;
|
|
210
103
|
esac
|
|
211
104
|
done <<< "$resolved"
|
|
212
|
-
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
3
|
+
const coreSurface = {
|
|
4
|
+
adapter: "core",
|
|
5
|
+
headless: true,
|
|
6
|
+
resolveSlotPorts() {
|
|
7
|
+
},
|
|
8
|
+
async runtimeStatus(target) {
|
|
9
|
+
const deps = depsCheck(path.resolve(target));
|
|
10
|
+
const ready = deps.status === "current";
|
|
11
|
+
return {
|
|
12
|
+
decision: ready ? "ready" : "install",
|
|
13
|
+
reasonCode: ready ? "deps-present" : `deps-${deps.status}`,
|
|
14
|
+
reasons: [
|
|
15
|
+
ready ? "Core is headless; dependencies are installed. Run recipes with mm-harness run." : "Core is headless; dependencies are not fully installed."
|
|
16
|
+
],
|
|
17
|
+
deps: deps.status
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
runwayProvision: {
|
|
21
|
+
async run(target, options) {
|
|
22
|
+
return {
|
|
23
|
+
schemaVersion: 1,
|
|
24
|
+
command: "provision",
|
|
25
|
+
adapter: "core",
|
|
26
|
+
target: path.resolve(target),
|
|
27
|
+
status: "fail",
|
|
28
|
+
exitCode: 2,
|
|
29
|
+
error: { code: "UNSUPPORTED_ADAPTER", message: "core is headless and has no mobile simulator to provision.", userAction: options.rerunCommand || "mm-harness run <recipe> --adapter core" }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
devServer: {
|
|
34
|
+
describe: () => "no dev server (headless)",
|
|
35
|
+
stop() {
|
|
36
|
+
return {
|
|
37
|
+
kind: "headless",
|
|
38
|
+
message: "core is headless \u2014 no dev server runs for a core checkout",
|
|
39
|
+
userAction: "use mm-harness verify (readiness) or mm-harness run (execute a recipe)"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
logSources() {
|
|
44
|
+
return [];
|
|
45
|
+
},
|
|
46
|
+
hints: {
|
|
47
|
+
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
48
|
+
relaunch: "mm-harness verify"
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export {
|
|
52
|
+
coreSurface
|
|
53
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveExtensionId } from "./extension-id.js";
|
|
3
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
async function jsonList(port) {
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(5e3) });
|
|
7
|
+
if (!res.ok) return [];
|
|
8
|
+
const value = await res.json();
|
|
9
|
+
return Array.isArray(value) ? value : [];
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function closeTab(port, id) {
|
|
15
|
+
try {
|
|
16
|
+
await fetch(`http://127.0.0.1:${port}/json/close/${id}`, { signal: AbortSignal.timeout(4e3) });
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function activateTab(port, id) {
|
|
21
|
+
try {
|
|
22
|
+
await fetch(`http://127.0.0.1:${port}/json/activate/${id}`, { signal: AbortSignal.timeout(4e3) });
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isStrayTab(url) {
|
|
27
|
+
if (typeof url !== "string") return false;
|
|
28
|
+
return /^chrome:\/\/newtab\/?$/.test(url) || /^chrome:\/\/new-tab-page\b/.test(url) || url === "about:blank";
|
|
29
|
+
}
|
|
30
|
+
async function openHome(port, extensionId) {
|
|
31
|
+
const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(`chrome-extension://${extensionId}/home.html`)}`;
|
|
32
|
+
try {
|
|
33
|
+
let res = await fetch(endpoint, { method: "PUT", signal: AbortSignal.timeout(8e3) });
|
|
34
|
+
if (res.status === 404 || res.status === 405) res = await fetch(endpoint, { signal: AbortSignal.timeout(8e3) });
|
|
35
|
+
return res.ok;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function homePages(targets, extensionId) {
|
|
41
|
+
const prefix = `chrome-extension://${extensionId}`;
|
|
42
|
+
return targets.filter(
|
|
43
|
+
(t) => t.type === "page" && typeof t.url === "string" && t.url.startsWith(prefix) && t.url.includes("/home.html") && t.id
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
async function ensureExtensionReady(target, options) {
|
|
47
|
+
const resolved = path.resolve(target);
|
|
48
|
+
const { cdpPort } = options;
|
|
49
|
+
const base = (extra) => ({
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
adapter: "extension",
|
|
52
|
+
target: resolved,
|
|
53
|
+
cdpPort,
|
|
54
|
+
extensionId: null,
|
|
55
|
+
opened: false,
|
|
56
|
+
homeTabs: { before: 0, closed: 0, after: 0 },
|
|
57
|
+
ready: false,
|
|
58
|
+
reasonCode: "unknown",
|
|
59
|
+
health: { status: "unknown", findings: [] },
|
|
60
|
+
...extra
|
|
61
|
+
});
|
|
62
|
+
const { extensionId } = await resolveExtensionId(resolved, { cdpPort });
|
|
63
|
+
if (!extensionId) return base({ reasonCode: "no-extension-id" });
|
|
64
|
+
const homes = homePages(await jsonList(cdpPort), extensionId);
|
|
65
|
+
const before = homes.length;
|
|
66
|
+
let opened = false;
|
|
67
|
+
let closed = 0;
|
|
68
|
+
if (homes.length > 1) {
|
|
69
|
+
for (const h of homes) {
|
|
70
|
+
await closeTab(cdpPort, String(h.id));
|
|
71
|
+
closed += 1;
|
|
72
|
+
}
|
|
73
|
+
await sleep(500);
|
|
74
|
+
opened = await openHome(cdpPort, extensionId);
|
|
75
|
+
await sleep(1500);
|
|
76
|
+
} else if (homes.length === 0) {
|
|
77
|
+
opened = await openHome(cdpPort, extensionId);
|
|
78
|
+
await sleep(1500);
|
|
79
|
+
}
|
|
80
|
+
const converged = await jsonList(cdpPort);
|
|
81
|
+
const homesNow = homePages(converged, extensionId);
|
|
82
|
+
if (homesNow.length >= 1) {
|
|
83
|
+
for (const t of converged) {
|
|
84
|
+
if (t.type === "page" && t.id && isStrayTab(t.url)) await closeTab(cdpPort, String(t.id));
|
|
85
|
+
}
|
|
86
|
+
if (homesNow[0]?.id) await activateTab(cdpPort, String(homesNow[0].id));
|
|
87
|
+
}
|
|
88
|
+
const after = homePages(await jsonList(cdpPort), extensionId).length;
|
|
89
|
+
let health = { status: "unknown", findings: [] };
|
|
90
|
+
try {
|
|
91
|
+
const { checkExtensionRuntimeHealth } = await import("./runtime.js");
|
|
92
|
+
const report = await checkExtensionRuntimeHealth(resolved, cdpPort);
|
|
93
|
+
health = { status: report.status, findings: report.findings };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
health = { status: "FAIL", findings: [error instanceof Error ? error.message : String(error)] };
|
|
96
|
+
}
|
|
97
|
+
const ready = health.status === "PASS" && after === 1;
|
|
98
|
+
return base({
|
|
99
|
+
extensionId,
|
|
100
|
+
opened,
|
|
101
|
+
homeTabs: { before, closed, after },
|
|
102
|
+
ready,
|
|
103
|
+
reasonCode: ready ? "ready" : after !== 1 ? "tab-count" : "unhealthy",
|
|
104
|
+
health
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
ensureExtensionReady
|
|
109
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const DIST_MANIFEST = "dist/chrome/manifest.json";
|
|
5
|
+
function extensionIdFromKey(keyBase64) {
|
|
6
|
+
const der = Buffer.from(keyBase64, "base64");
|
|
7
|
+
const hash = crypto.createHash("sha256").update(der).digest();
|
|
8
|
+
let id = "";
|
|
9
|
+
for (let i = 0; i < 16; i += 1) {
|
|
10
|
+
id += String.fromCharCode(97 + (hash[i] >> 4));
|
|
11
|
+
id += String.fromCharCode(97 + (hash[i] & 15));
|
|
12
|
+
}
|
|
13
|
+
return id;
|
|
14
|
+
}
|
|
15
|
+
function idFromDistManifest(target) {
|
|
16
|
+
const manifestPath = path.join(target, DIST_MANIFEST);
|
|
17
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
18
|
+
try {
|
|
19
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
20
|
+
return typeof manifest.key === "string" && manifest.key.length > 0 ? extensionIdFromKey(manifest.key) : null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function cdpExtensionIds(cdpPort) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`http://127.0.0.1:${cdpPort}/json/list`, { signal: AbortSignal.timeout(4e3) });
|
|
28
|
+
if (!res.ok) return [];
|
|
29
|
+
const targets = await res.json();
|
|
30
|
+
const ids = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const t of targets) {
|
|
32
|
+
const m = /^chrome-extension:\/\/([^/]+)/u.exec(String(t.url ?? ""));
|
|
33
|
+
if (m) ids.add(m[1]);
|
|
34
|
+
}
|
|
35
|
+
return [...ids];
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function resolveExtensionId(target, options = {}) {
|
|
41
|
+
const resolved = path.resolve(target);
|
|
42
|
+
const fromKey = idFromDistManifest(resolved);
|
|
43
|
+
let cdpIds = null;
|
|
44
|
+
if (options.cdpPort) cdpIds = await cdpExtensionIds(options.cdpPort);
|
|
45
|
+
if (fromKey) {
|
|
46
|
+
return {
|
|
47
|
+
adapter: "extension",
|
|
48
|
+
target: resolved,
|
|
49
|
+
extensionId: fromKey,
|
|
50
|
+
source: "manifest-key",
|
|
51
|
+
verified: cdpIds ? cdpIds.includes(fromKey) : null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (cdpIds && cdpIds.length === 1) {
|
|
55
|
+
return { adapter: "extension", target: resolved, extensionId: cdpIds[0], source: "cdp-target", verified: true };
|
|
56
|
+
}
|
|
57
|
+
return { adapter: "extension", target: resolved, extensionId: null, source: "none", verified: cdpIds ? false : null };
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
extensionIdFromKey,
|
|
61
|
+
resolveExtensionId
|
|
62
|
+
};
|