@deeeed/metamask-harness 0.27.0 → 0.29.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 +26 -0
- package/README.md +41 -0
- package/adapters/extension/build-lavamoat.sh +2 -1
- package/adapters/extension/ensure-browser.sh +82 -9
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +83 -1
- package/adapters/extension/lib/chrome-args.cjs +325 -1
- package/adapters/extension/lib/playwright-cdp.cjs +34 -0
- package/adapters/extension/lib/slot-title.cjs +2 -4
- package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
- package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
- package/adapters/extension/reattach.sh +2 -1
- package/adapters/extension/sidepanel-toggle.sh +14 -96
- package/adapters/extension/wallet-fixture-state.cjs +8 -31
- package/adapters/manifest.json +16 -0
- package/adapters/shared/private-atomic-write.cjs +47 -0
- package/adapters/shared/setup-base.sh +864 -0
- package/dist/adapters/extension/runtime.js +367 -24
- package/dist/adapters/extension/validation-process-ownership.js +10 -0
- package/dist/adapters.js +0 -11
- package/dist/cli-commands.js +2 -1
- package/dist/command-contract.js +13 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/launch/extension.js +130 -19
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/commands/setup-base.js +24 -0
- package/dist/mm-harness-cli.js +30 -2
- package/dist/recipe-security.js +1 -0
- package/library/actions/extension/analytics/consent.mjs +203 -0
- package/library/actions/extension/analytics/set_consent.mjs +19 -143
- package/library/actions/extension/perps/perps.mjs +2 -16
- package/library/actions/extension/perps/state.mjs +20 -0
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/extension/wallet/list_accounts.mjs +3 -25
- package/library/actions/extension/wallet/read_state.mjs +3 -23
- package/library/actions/extension/wallet/select_account.mjs +6 -33
- package/library/actions/extension/wallet/setup.mjs +2 -20
- package/library/actions/extension/wallet/state.mjs +111 -0
- package/library/actions/mobile/platform/observe-ui.mjs +21 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
- package/package.json +7 -4
- package/scripts/site-contrast.mjs +538 -0
- package/site/architecture.html +415 -0
- package/site/assets/progress.mjs +272 -0
- package/site/assets/style.css +808 -0
- package/site/cheatsheet.html +305 -0
- package/site/index.html +643 -0
- package/site/recipes.html +396 -0
- package/site/reviewers.html +374 -0
- package/site/tutorials/index.html +180 -0
- package/site/tutorials/v1.html +211 -0
- package/site/tutorials/v2.html +207 -0
- package/site/tutorials/v3.html +214 -0
- package/site/tutorials/v4.html +195 -0
- package/site/tutorials/v5.html +163 -0
- package/site/tutorials/v6.html +165 -0
- package/site/tutorials/v7.html +184 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# mm-harness setup-base — bootstrap the shared default MetaMask checkout layout.
|
|
3
|
+
#
|
|
4
|
+
# npx -p @deeeed/metamask-harness mm-harness setup-base
|
|
5
|
+
#
|
|
6
|
+
# Clones the MetaMask product repos in N copies each into one standard folder
|
|
7
|
+
# layout and runs each repo's own dependency install:
|
|
8
|
+
#
|
|
9
|
+
# <base>/metamask-extension-1..N
|
|
10
|
+
# <base>/metamask-mobile-1..N
|
|
11
|
+
# <base>/core-1..N
|
|
12
|
+
#
|
|
13
|
+
# Scope fence: clone + dependency install, nothing else. No platform toolchains,
|
|
14
|
+
# no simulators, no .env files, no builds. Environment readiness is a separate
|
|
15
|
+
# step — `mm-harness doctor`.
|
|
16
|
+
#
|
|
17
|
+
# This file is deliberately a plain, standalone bash script: it runs on a machine
|
|
18
|
+
# before anything else is set up, and you should be able to read the whole thing
|
|
19
|
+
# top to bottom and decide whether to trust it BEFORE you run it. It sources
|
|
20
|
+
# nothing from the package at runtime and pulls in no shell dependencies.
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
NODE_BIN="${MM_HARNESS_NODE:-node}"
|
|
25
|
+
PRIVATE_WRITER="$SCRIPT_DIR/private-atomic-write.cjs"
|
|
26
|
+
|
|
27
|
+
# Exit codes are stable and match the harness CLI, so another tool can branch on
|
|
28
|
+
# them: 0 ok · 1 a repo failed · 2 usage · 3 environment/disk refusal.
|
|
29
|
+
EXIT_OK=0
|
|
30
|
+
EXIT_RUNTIME=1
|
|
31
|
+
EXIT_USAGE=2
|
|
32
|
+
EXIT_INFRA=3
|
|
33
|
+
|
|
34
|
+
DEFAULT_BASE="$HOME/dev/metamask"
|
|
35
|
+
REPO_KEYS=(extension mobile core)
|
|
36
|
+
HARNESS_PACKAGE="@deeeed/metamask-harness"
|
|
37
|
+
|
|
38
|
+
COUNT_EXTENSION=2
|
|
39
|
+
COUNT_MOBILE=2
|
|
40
|
+
COUNT_CORE=2
|
|
41
|
+
|
|
42
|
+
BASE_DIR=""
|
|
43
|
+
BASE_SOURCE=""
|
|
44
|
+
SELECTED=()
|
|
45
|
+
SELECTION_SOURCE=""
|
|
46
|
+
DRY_RUN=0
|
|
47
|
+
FORCE=0
|
|
48
|
+
JSON_OUT=0
|
|
49
|
+
SKIP_HARNESS_UPDATE=0
|
|
50
|
+
SAW_ONLY=0
|
|
51
|
+
SAW_COUNTS=0
|
|
52
|
+
SHOW_CONFIG=0
|
|
53
|
+
RESET_CONFIG=0
|
|
54
|
+
RESULTS=()
|
|
55
|
+
FAILURES=0
|
|
56
|
+
EXIT_CODE="$EXIT_OK"
|
|
57
|
+
ERROR_CODE=""
|
|
58
|
+
ERROR_USER_ACTION=""
|
|
59
|
+
|
|
60
|
+
for arg in "$@"; do
|
|
61
|
+
[ "$arg" = "--json" ] && JSON_OUT=1
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
# --- config location ----------------------------------------------------------
|
|
65
|
+
# Same shape the harness already uses for persisted state: an explicit env
|
|
66
|
+
# override first, otherwise an XDG base directory plus an mm-harness/ folder.
|
|
67
|
+
config_file() {
|
|
68
|
+
if [ -n "${MM_HARNESS_SETUP_CONFIG:-}" ]; then
|
|
69
|
+
printf '%s' "$MM_HARNESS_SETUP_CONFIG"
|
|
70
|
+
return
|
|
71
|
+
fi
|
|
72
|
+
printf '%s/mm-harness/setup-base.json' "${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
last_run_file() {
|
|
76
|
+
printf '%s/last-run.json' "$(dirname "$(config_file)")"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
CONFIG_FILE="$(config_file)"
|
|
80
|
+
|
|
81
|
+
# --- UI -----------------------------------------------------------------------
|
|
82
|
+
# With --json, stdout carries the machine summary and nothing else, so every
|
|
83
|
+
# human line goes to stderr.
|
|
84
|
+
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
|
|
85
|
+
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
|
|
86
|
+
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
|
87
|
+
C_BLUE=$'\033[34m'; C_CYAN=$'\033[36m'
|
|
88
|
+
else
|
|
89
|
+
C_RESET=; C_BOLD=; C_RED=; C_GREEN=; C_YELLOW=; C_BLUE=; C_CYAN=
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
say() { printf '%s\n' "$*" >&2; }
|
|
93
|
+
ui_ok() { printf '%s[ OK ]%s %s\n' "$C_GREEN" "$C_RESET" "$*" >&2; }
|
|
94
|
+
ui_warn() { printf '%s[WARN]%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
|
|
95
|
+
ui_fail() { printf '%s[FAIL]%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
|
|
96
|
+
ui_info() { printf '%s[INFO]%s %s\n' "$C_CYAN" "$C_RESET" "$*" >&2; }
|
|
97
|
+
ui_section() { printf '\n%s== %s ==%s\n' "$C_BOLD$C_BLUE" "$*" "$C_RESET" >&2; }
|
|
98
|
+
ui_next() { printf ' %sNext:%s %s\n' "$C_BOLD" "$C_RESET" "$*" >&2; }
|
|
99
|
+
|
|
100
|
+
write_summary() {
|
|
101
|
+
local target="$1" code="$2" message="$3" rows="" generator idx
|
|
102
|
+
for ((idx = 0; idx < ${#RESULTS[@]}; idx++)); do
|
|
103
|
+
rows="$rows${RESULTS[$idx]}"$'\n'
|
|
104
|
+
done
|
|
105
|
+
generator='
|
|
106
|
+
const repos = (process.env.MM_ROWS || "").split("\n").filter(Boolean).map((line) => {
|
|
107
|
+
const [directory, clone, install] = line.split("\t");
|
|
108
|
+
return { directory, clone, install };
|
|
109
|
+
});
|
|
110
|
+
process.stdout.write(JSON.stringify({
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
command: "setup-base",
|
|
113
|
+
baseDir: process.env.MM_BASE || null,
|
|
114
|
+
configFile: process.env.MM_CONFIG,
|
|
115
|
+
exitCode: Number(process.env.MM_CODE),
|
|
116
|
+
status: Number(process.env.MM_CODE) === 0 ? "ok" : "fail",
|
|
117
|
+
message: process.env.MM_MESSAGE || undefined,
|
|
118
|
+
error: process.env.MM_ERROR_CODE ? {
|
|
119
|
+
code: process.env.MM_ERROR_CODE,
|
|
120
|
+
message: process.env.MM_MESSAGE,
|
|
121
|
+
userAction: process.env.MM_ERROR_USER_ACTION,
|
|
122
|
+
} : undefined,
|
|
123
|
+
repos,
|
|
124
|
+
finishedAt: new Date().toISOString(),
|
|
125
|
+
}, null, 2) + "\n");
|
|
126
|
+
'
|
|
127
|
+
if [ "$target" = "-" ]; then
|
|
128
|
+
MM_ROWS="$rows" MM_BASE="$BASE_DIR" MM_CONFIG="$CONFIG_FILE" \
|
|
129
|
+
MM_CODE="$code" MM_MESSAGE="$message" MM_ERROR_CODE="$ERROR_CODE" \
|
|
130
|
+
MM_ERROR_USER_ACTION="$ERROR_USER_ACTION" "$NODE_BIN" -e "$generator"
|
|
131
|
+
return
|
|
132
|
+
fi
|
|
133
|
+
MM_ROWS="$rows" MM_BASE="$BASE_DIR" MM_CONFIG="$CONFIG_FILE" \
|
|
134
|
+
MM_CODE="$code" MM_MESSAGE="$message" MM_ERROR_CODE="$ERROR_CODE" \
|
|
135
|
+
MM_ERROR_USER_ACTION="$ERROR_USER_ACTION" "$NODE_BIN" -e "$generator" \
|
|
136
|
+
| "$NODE_BIN" "$PRIVATE_WRITER" "$target"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
finish() {
|
|
140
|
+
local code="$1" message="${2:-}"
|
|
141
|
+
EXIT_CODE="$code"
|
|
142
|
+
local last_run
|
|
143
|
+
last_run="$(last_run_file)"
|
|
144
|
+
write_summary "$last_run" "$code" "$message" 2>/dev/null \
|
|
145
|
+
|| ui_warn "could not write the run summary to $last_run"
|
|
146
|
+
if [ "$JSON_OUT" = 1 ]; then
|
|
147
|
+
write_summary - "$code" "$message"
|
|
148
|
+
fi
|
|
149
|
+
exit "$code"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# die <exit-code> <message> <next-command...> — no failure is allowed to leave
|
|
153
|
+
# the caller without a concrete command to run next.
|
|
154
|
+
die() {
|
|
155
|
+
local code="$1" msg="$2"; shift 2
|
|
156
|
+
if [ "$code" = "$EXIT_USAGE" ]; then
|
|
157
|
+
case "$msg" in
|
|
158
|
+
"unknown option "*) ERROR_CODE="CLI_UNKNOWN_OPTION" ;;
|
|
159
|
+
*" needs a "*) ERROR_CODE="CLI_MISSING_OPTION_VALUE" ;;
|
|
160
|
+
*) ERROR_CODE="CLI_INVALID_OPTION_VALUE" ;;
|
|
161
|
+
esac
|
|
162
|
+
ERROR_USER_ACTION="${1:-mm-harness setup-base --help}"
|
|
163
|
+
fi
|
|
164
|
+
if [ "$JSON_OUT" != 1 ] || [ "$code" != "$EXIT_USAGE" ]; then
|
|
165
|
+
ui_fail "$msg"
|
|
166
|
+
local next
|
|
167
|
+
for next in "$@"; do ui_next "$next"; done
|
|
168
|
+
fi
|
|
169
|
+
finish "$code" "$msg"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# --- numbered steps -----------------------------------------------------------
|
|
173
|
+
STEP_N=0
|
|
174
|
+
STEP_TOTAL=0
|
|
175
|
+
step() {
|
|
176
|
+
STEP_N=$((STEP_N + 1))
|
|
177
|
+
printf '\n%s[%s/%s]%s %s%s%s\n' \
|
|
178
|
+
"$C_CYAN" "$STEP_N" "$STEP_TOTAL" "$C_RESET" "$C_BOLD" "$1" "$C_RESET" >&2
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
usage() {
|
|
182
|
+
if [ "$JSON_OUT" = 1 ]; then
|
|
183
|
+
cat >&2 <<USAGE
|
|
184
|
+
mm-harness setup-base — bootstrap the shared default MetaMask checkout layout.
|
|
185
|
+
|
|
186
|
+
Usage:
|
|
187
|
+
mm-harness setup-base [--dir <base>] [--counts <spec>] [--only <repos>]
|
|
188
|
+
[--dry-run] [--force] [--json] [--show-config]
|
|
189
|
+
[--reset-config] [--skip-harness-update] [--help]
|
|
190
|
+
USAGE
|
|
191
|
+
return
|
|
192
|
+
fi
|
|
193
|
+
cat <<USAGE
|
|
194
|
+
mm-harness setup-base — bootstrap the shared default MetaMask checkout layout.
|
|
195
|
+
|
|
196
|
+
Clones the MetaMask product repos in several copies into one standard folder
|
|
197
|
+
layout and runs each repo's own dependency install. Nothing else.
|
|
198
|
+
|
|
199
|
+
Usage:
|
|
200
|
+
mm-harness setup-base [--dir <base>] [--counts <spec>] [--only <repos>]
|
|
201
|
+
[--dry-run] [--force] [--json] [--show-config]
|
|
202
|
+
[--reset-config] [--skip-harness-update] [--help]
|
|
203
|
+
|
|
204
|
+
Flags:
|
|
205
|
+
--dir <base> Where the clones go.
|
|
206
|
+
--counts <spec> Copies per repo, e.g. extension=2,mobile=2,core=2 (min 1).
|
|
207
|
+
--only <repos> Comma-separated subset of: extension,mobile,core.
|
|
208
|
+
--dry-run Print the plan and disk estimate; change nothing.
|
|
209
|
+
--force Proceed despite an insufficient-disk estimate.
|
|
210
|
+
--json Emit the run summary as JSON on stdout (human output
|
|
211
|
+
goes to stderr). For scripted/parent-tool use.
|
|
212
|
+
--show-config Print the saved preferences and exit.
|
|
213
|
+
--reset-config Delete the saved preferences and exit.
|
|
214
|
+
--skip-harness-update Do not check whether $HARNESS_PACKAGE is current.
|
|
215
|
+
-h, --help Show this help and exit.
|
|
216
|
+
|
|
217
|
+
Layout produced under <base>:
|
|
218
|
+
metamask-extension-1..N metamask-mobile-1..N core-1..N
|
|
219
|
+
|
|
220
|
+
Base directory precedence (first match wins):
|
|
221
|
+
1. --dir <base>
|
|
222
|
+
2. \$MM_HARNESS_BASE_DIR
|
|
223
|
+
3. saved preferences file
|
|
224
|
+
4. $DEFAULT_BASE
|
|
225
|
+
|
|
226
|
+
Saved preferences:
|
|
227
|
+
Path: $(config_file)
|
|
228
|
+
(override with \$MM_HARNESS_SETUP_CONFIG)
|
|
229
|
+
Format: JSON — { "schemaVersion": 1, "baseDir": "<path>",
|
|
230
|
+
"counts": { "extension": N, "mobile": N, "core": N },
|
|
231
|
+
"only": ["extension", ...], "updatedAt": "<iso-8601>" }
|
|
232
|
+
A successful run saves the base dir and counts it used; later runs adopt them
|
|
233
|
+
as defaults. Inspect with --show-config, clear with --reset-config.
|
|
234
|
+
|
|
235
|
+
The last run's summary is written beside it as:
|
|
236
|
+
$(last_run_file)
|
|
237
|
+
|
|
238
|
+
Selecting what to install:
|
|
239
|
+
With no --only/--counts and no saved preferences, an interactive prompt asks
|
|
240
|
+
which projects and how many copies. Passing --only or --counts skips the
|
|
241
|
+
prompt entirely, which is how non-interactive and parent-tool runs should
|
|
242
|
+
invoke this.
|
|
243
|
+
|
|
244
|
+
Exit codes:
|
|
245
|
+
0 success · 1 a repo failed · 2 usage error · 3 environment or disk refusal
|
|
246
|
+
|
|
247
|
+
Scope: clone + dependency install only. Toolchains, simulators, .env files and
|
|
248
|
+
builds are out of scope — run \`mm-harness doctor\` for environment readiness.
|
|
249
|
+
USAGE
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# --- repo table (case functions, not associative arrays: macOS ships bash 3.2) -
|
|
253
|
+
repo_slug() {
|
|
254
|
+
case "$1" in
|
|
255
|
+
extension) printf 'metamask-extension' ;;
|
|
256
|
+
mobile) printf 'metamask-mobile' ;;
|
|
257
|
+
core) printf 'core' ;;
|
|
258
|
+
esac
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Disk estimate per clone in GB, after clone + dependency install, rounded up
|
|
262
|
+
# from a measurement for headroom.
|
|
263
|
+
# extension measured 5.1G (1.3G .git + 3.1G node_modules)
|
|
264
|
+
# core measured 1.6G (257M .git + 1.1G node_modules)
|
|
265
|
+
# mobile estimated — .git alone measures 1.8G and a working checkout
|
|
266
|
+
# measures 6.3G, so 8 is deliberately conservative.
|
|
267
|
+
# Dependencies only; a native build adds more and is out of scope here.
|
|
268
|
+
repo_gb() {
|
|
269
|
+
case "$1" in
|
|
270
|
+
extension) printf '6' ;;
|
|
271
|
+
mobile) printf '8' ;;
|
|
272
|
+
core) printf '2' ;;
|
|
273
|
+
esac
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
count_for() {
|
|
277
|
+
case "$1" in
|
|
278
|
+
extension) printf '%s' "$COUNT_EXTENSION" ;;
|
|
279
|
+
mobile) printf '%s' "$COUNT_MOBILE" ;;
|
|
280
|
+
core) printf '%s' "$COUNT_CORE" ;;
|
|
281
|
+
esac
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
set_count() {
|
|
285
|
+
case "$1" in
|
|
286
|
+
extension) COUNT_EXTENSION="$2" ;;
|
|
287
|
+
mobile) COUNT_MOBILE="$2" ;;
|
|
288
|
+
core) COUNT_CORE="$2" ;;
|
|
289
|
+
esac
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
is_repo_key() {
|
|
293
|
+
local k
|
|
294
|
+
for k in "${REPO_KEYS[@]}"; do [ "$k" = "$1" ] && return 0; done
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# --- argument parsing ---------------------------------------------------------
|
|
299
|
+
# Expand a leading ~/ that reached us quoted, so --dir "~/x" never creates a
|
|
300
|
+
# directory literally named "~".
|
|
301
|
+
expand_tilde() {
|
|
302
|
+
# The quoted ~/ below is the literal text being matched, not a path the shell
|
|
303
|
+
# should expand — expanding it is exactly this function's job.
|
|
304
|
+
# shellcheck disable=SC2088
|
|
305
|
+
case "$1" in
|
|
306
|
+
"~/"*) printf '%s/%s' "$HOME" "${1#"~/"}" ;;
|
|
307
|
+
"~") printf '%s' "$HOME" ;;
|
|
308
|
+
*) printf '%s' "$1" ;;
|
|
309
|
+
esac
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
parse_counts() {
|
|
313
|
+
local spec="$1" pair key value
|
|
314
|
+
local -a pairs=()
|
|
315
|
+
IFS=',' read -r -a pairs <<< "$spec"
|
|
316
|
+
[ "${#pairs[@]}" -gt 0 ] || die "$EXIT_USAGE" "--counts got an empty value" \
|
|
317
|
+
"mm-harness setup-base --counts extension=2,mobile=2,core=2"
|
|
318
|
+
for pair in "${pairs[@]}"; do
|
|
319
|
+
case "$pair" in
|
|
320
|
+
*=*) ;;
|
|
321
|
+
*) die "$EXIT_USAGE" "--counts entry '$pair' is not <repo>=<n>" \
|
|
322
|
+
"mm-harness setup-base --counts extension=2,mobile=2,core=2" ;;
|
|
323
|
+
esac
|
|
324
|
+
key="${pair%%=*}"
|
|
325
|
+
value="${pair#*=}"
|
|
326
|
+
is_repo_key "$key" || die "$EXIT_USAGE" "--counts names unknown repo '$key'" \
|
|
327
|
+
"mm-harness setup-base --counts extension=2,mobile=2,core=2 # valid: ${REPO_KEYS[*]}"
|
|
328
|
+
case "$value" in
|
|
329
|
+
"" | *[!0-9]*) die "$EXIT_USAGE" "--counts value for '$key' is not a whole number: '$value'" \
|
|
330
|
+
"mm-harness setup-base --counts $key=2" ;;
|
|
331
|
+
esac
|
|
332
|
+
[ "$value" -ge 1 ] || die "$EXIT_USAGE" "--counts value for '$key' must be at least 1 (got $value)" \
|
|
333
|
+
"mm-harness setup-base --only $key # or drop '$key' from --only to skip it"
|
|
334
|
+
set_count "$key" "$value"
|
|
335
|
+
done
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
parse_only() {
|
|
339
|
+
local spec="$1" key
|
|
340
|
+
local -a keys=()
|
|
341
|
+
IFS=',' read -r -a keys <<< "$spec"
|
|
342
|
+
[ "${#keys[@]}" -gt 0 ] || die "$EXIT_USAGE" "--only got an empty value" \
|
|
343
|
+
"mm-harness setup-base --only extension,core"
|
|
344
|
+
for key in "${keys[@]}"; do
|
|
345
|
+
is_repo_key "$key" || die "$EXIT_USAGE" "--only names unknown repo '$key'" \
|
|
346
|
+
"mm-harness setup-base --only extension,core # valid: ${REPO_KEYS[*]}"
|
|
347
|
+
done
|
|
348
|
+
SELECTED=("${keys[@]}")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
require_value() {
|
|
352
|
+
local flag="$1" value="${2:-}" example="$3"
|
|
353
|
+
case "$value" in
|
|
354
|
+
"" | -*) die "$EXIT_USAGE" "$flag needs a value" "$example" ;;
|
|
355
|
+
esac
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
FLAG_DIR=""
|
|
359
|
+
while [ "$#" -gt 0 ]; do
|
|
360
|
+
case "$1" in
|
|
361
|
+
--dir)
|
|
362
|
+
require_value "--dir" "${2:-}" "mm-harness setup-base --dir ~/dev/metamask"
|
|
363
|
+
FLAG_DIR="$(expand_tilde "$2")"; shift 2 ;;
|
|
364
|
+
--dir=*)
|
|
365
|
+
require_value "--dir" "${1#*=}" "mm-harness setup-base --dir ~/dev/metamask"
|
|
366
|
+
FLAG_DIR="$(expand_tilde "${1#*=}")"; shift ;;
|
|
367
|
+
--counts)
|
|
368
|
+
require_value "--counts" "${2:-}" \
|
|
369
|
+
"mm-harness setup-base --counts extension=2,mobile=2,core=2"
|
|
370
|
+
parse_counts "$2"; SAW_COUNTS=1; shift 2 ;;
|
|
371
|
+
--counts=*)
|
|
372
|
+
require_value "--counts" "${1#*=}" \
|
|
373
|
+
"mm-harness setup-base --counts extension=2,mobile=2,core=2"
|
|
374
|
+
parse_counts "${1#*=}"; SAW_COUNTS=1; shift ;;
|
|
375
|
+
--only)
|
|
376
|
+
require_value "--only" "${2:-}" "mm-harness setup-base --only extension,core"
|
|
377
|
+
parse_only "$2"; SAW_ONLY=1; shift 2 ;;
|
|
378
|
+
--only=*)
|
|
379
|
+
require_value "--only" "${1#*=}" "mm-harness setup-base --only extension,core"
|
|
380
|
+
parse_only "${1#*=}"; SAW_ONLY=1; shift ;;
|
|
381
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
382
|
+
--force) FORCE=1; shift ;;
|
|
383
|
+
--json) JSON_OUT=1; shift ;;
|
|
384
|
+
--show-config) SHOW_CONFIG=1; shift ;;
|
|
385
|
+
--reset-config) RESET_CONFIG=1; shift ;;
|
|
386
|
+
--skip-harness-update) SKIP_HARNESS_UPDATE=1; shift ;;
|
|
387
|
+
-h | --help) usage; finish "$EXIT_OK" "help" ;;
|
|
388
|
+
*) die "$EXIT_USAGE" "unknown option '$1'" "mm-harness setup-base --help" ;;
|
|
389
|
+
esac
|
|
390
|
+
done
|
|
391
|
+
|
|
392
|
+
# --- saved preferences --------------------------------------------------------
|
|
393
|
+
# node owns the JSON so quoting and escaping are correct in both directions. It
|
|
394
|
+
# ships with the package that ships this script, so it is always present; a
|
|
395
|
+
# missing node is caught by the prerequisite step with a teaching error.
|
|
396
|
+
config_get() {
|
|
397
|
+
[ -f "$CONFIG_FILE" ] && [ ! -L "$CONFIG_FILE" ] || return 1
|
|
398
|
+
"$NODE_BIN" -e '
|
|
399
|
+
const fs = require("fs");
|
|
400
|
+
try {
|
|
401
|
+
const c = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
402
|
+
const key = process.argv[2];
|
|
403
|
+
if (key === "baseDir") { if (typeof c.baseDir === "string") process.stdout.write(c.baseDir); }
|
|
404
|
+
else if (key === "only") { if (Array.isArray(c.only)) process.stdout.write(c.only.join(",")); }
|
|
405
|
+
else if (typeof c.counts?.[key] === "number") process.stdout.write(String(c.counts[key]));
|
|
406
|
+
} catch { process.exit(1); }
|
|
407
|
+
' "$CONFIG_FILE" "$1" 2>/dev/null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
config_save() {
|
|
411
|
+
MM_BASE="$BASE_DIR" MM_ONLY="$(IFS=,; printf '%s' "${SELECTED[*]}")" \
|
|
412
|
+
MM_EXT="$COUNT_EXTENSION" MM_MOB="$COUNT_MOBILE" MM_CORE="$COUNT_CORE" \
|
|
413
|
+
"$NODE_BIN" -e '
|
|
414
|
+
process.stdout.write(JSON.stringify({
|
|
415
|
+
schemaVersion: 1,
|
|
416
|
+
baseDir: process.env.MM_BASE,
|
|
417
|
+
counts: {
|
|
418
|
+
extension: Number(process.env.MM_EXT),
|
|
419
|
+
mobile: Number(process.env.MM_MOB),
|
|
420
|
+
core: Number(process.env.MM_CORE),
|
|
421
|
+
},
|
|
422
|
+
only: process.env.MM_ONLY ? process.env.MM_ONLY.split(",") : [],
|
|
423
|
+
updatedAt: new Date().toISOString(),
|
|
424
|
+
}, null, 2) + "\n");
|
|
425
|
+
' | "$NODE_BIN" "$PRIVATE_WRITER" "$CONFIG_FILE" 2>/dev/null \
|
|
426
|
+
|| ui_warn "could not write $CONFIG_FILE — preferences not saved."
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if [ "$SHOW_CONFIG" = 1 ]; then
|
|
430
|
+
[ ! -L "$CONFIG_FILE" ] || die "$EXIT_INFRA" "$CONFIG_FILE is a symbolic link" \
|
|
431
|
+
"mm-harness setup-base --reset-config"
|
|
432
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
433
|
+
say "$CONFIG_FILE"
|
|
434
|
+
cat "$CONFIG_FILE" >&2
|
|
435
|
+
else
|
|
436
|
+
ui_info "No saved preferences yet ($CONFIG_FILE)."
|
|
437
|
+
ui_next "mm-harness setup-base --only core --counts core=1 # a successful run saves them"
|
|
438
|
+
fi
|
|
439
|
+
finish "$EXIT_OK" "show-config"
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
if [ "$RESET_CONFIG" = 1 ]; then
|
|
443
|
+
if [ -e "$CONFIG_FILE" ] || [ -L "$CONFIG_FILE" ]; then
|
|
444
|
+
rm -f "$CONFIG_FILE"
|
|
445
|
+
ui_ok "Removed $CONFIG_FILE"
|
|
446
|
+
else
|
|
447
|
+
ui_info "Nothing to reset ($CONFIG_FILE does not exist)."
|
|
448
|
+
fi
|
|
449
|
+
finish "$EXIT_OK" "reset-config"
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
# --- resolve the base directory (flag > env > saved > default) ----------------
|
|
453
|
+
if [ -n "$FLAG_DIR" ]; then
|
|
454
|
+
BASE_DIR="$FLAG_DIR"; BASE_SOURCE="--dir"
|
|
455
|
+
elif [ -n "${MM_HARNESS_BASE_DIR:-}" ]; then
|
|
456
|
+
BASE_DIR="$(expand_tilde "$MM_HARNESS_BASE_DIR")"; BASE_SOURCE="\$MM_HARNESS_BASE_DIR"
|
|
457
|
+
elif SAVED_BASE="$(config_get baseDir)" && [ -n "$SAVED_BASE" ]; then
|
|
458
|
+
BASE_DIR="$SAVED_BASE"; BASE_SOURCE="saved preferences"
|
|
459
|
+
else
|
|
460
|
+
BASE_DIR="$DEFAULT_BASE"; BASE_SOURCE="default"
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
# --- resolve the selection (flags > saved > interactive) ----------------------
|
|
464
|
+
HAVE_SAVED_SELECTION=0
|
|
465
|
+
if [ "$SAW_ONLY" = 0 ] && [ -f "$CONFIG_FILE" ]; then
|
|
466
|
+
if SAVED_ONLY="$(config_get only)" && [ -n "$SAVED_ONLY" ]; then
|
|
467
|
+
parse_only "$SAVED_ONLY"
|
|
468
|
+
HAVE_SAVED_SELECTION=1
|
|
469
|
+
fi
|
|
470
|
+
fi
|
|
471
|
+
if [ "$SAW_COUNTS" = 0 ] && [ -f "$CONFIG_FILE" ]; then
|
|
472
|
+
for key in "${REPO_KEYS[@]}"; do
|
|
473
|
+
if saved_count="$(config_get "$key")" && [ -n "$saved_count" ]; then
|
|
474
|
+
set_count "$key" "$saved_count"
|
|
475
|
+
HAVE_SAVED_SELECTION=1
|
|
476
|
+
fi
|
|
477
|
+
done
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
if [ "$SAW_ONLY" = 1 ] || [ "$SAW_COUNTS" = 1 ]; then
|
|
481
|
+
SELECTION_SOURCE="flags"
|
|
482
|
+
[ "$SAW_ONLY" = 1 ] || SELECTED=("${REPO_KEYS[@]}")
|
|
483
|
+
elif [ "$HAVE_SAVED_SELECTION" = 1 ]; then
|
|
484
|
+
SELECTION_SOURCE="saved preferences"
|
|
485
|
+
[ "${#SELECTED[@]}" -gt 0 ] || SELECTED=("${REPO_KEYS[@]}")
|
|
486
|
+
else
|
|
487
|
+
SELECTION_SOURCE="interactive"
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
# A prompt needs someone able to answer it. stdin being a terminal is the signal
|
|
491
|
+
# that distinguishes a human at a shell from a script, a pipe, or CI. Questions
|
|
492
|
+
# are asked on stderr and answered on stdin, so nothing here competes with the
|
|
493
|
+
# --json summary on stdout.
|
|
494
|
+
have_tty() { [ -t 0 ]; }
|
|
495
|
+
|
|
496
|
+
if [ "$SELECTION_SOURCE" = "interactive" ] && ! have_tty; then
|
|
497
|
+
die "$EXIT_USAGE" "no terminal available and nothing selected to set up" \
|
|
498
|
+
"mm-harness setup-base --only extension,mobile,core --counts extension=2,mobile=2,core=2" \
|
|
499
|
+
"mm-harness setup-base --only core --counts core=1 # a smaller start" \
|
|
500
|
+
"mm-harness setup-base --help # every flag"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# --- step plan ----------------------------------------------------------------
|
|
504
|
+
STEP_TOTAL=4
|
|
505
|
+
[ "$SKIP_HARNESS_UPDATE" = 1 ] || STEP_TOTAL=$((STEP_TOTAL + 1))
|
|
506
|
+
[ "$SELECTION_SOURCE" = "interactive" ] && STEP_TOTAL=$((STEP_TOTAL + 1))
|
|
507
|
+
|
|
508
|
+
# --- step: prerequisites ------------------------------------------------------
|
|
509
|
+
step "Prerequisites"
|
|
510
|
+
command -v git >/dev/null 2>&1 || die "$EXIT_INFRA" "git is not installed — nothing can be cloned" \
|
|
511
|
+
"xcode-select --install # macOS" \
|
|
512
|
+
"brew install git"
|
|
513
|
+
ui_ok "git $(git --version 2>/dev/null | awk '{print $3}')"
|
|
514
|
+
|
|
515
|
+
[ -x "$NODE_BIN" ] || command -v "$NODE_BIN" >/dev/null 2>&1 \
|
|
516
|
+
|| die "$EXIT_INFRA" "node is not installed" \
|
|
517
|
+
"brew install node" \
|
|
518
|
+
"asdf install nodejs latest # or: nvm install --lts"
|
|
519
|
+
ui_ok "node $("$NODE_BIN" -v 2>/dev/null)"
|
|
520
|
+
|
|
521
|
+
YARN_CMD=()
|
|
522
|
+
if command -v yarn >/dev/null 2>&1 && yarn --version >/dev/null 2>&1; then
|
|
523
|
+
YARN_CMD=(yarn)
|
|
524
|
+
ui_ok "yarn $(yarn --version 2>/dev/null)"
|
|
525
|
+
elif command -v corepack >/dev/null 2>&1; then
|
|
526
|
+
if corepack yarn --version >/dev/null 2>&1; then
|
|
527
|
+
YARN_CMD=(corepack yarn)
|
|
528
|
+
ui_ok "corepack yarn $(corepack yarn --version 2>/dev/null)"
|
|
529
|
+
else
|
|
530
|
+
die "$EXIT_INFRA" "corepack cannot run yarn" \
|
|
531
|
+
"corepack enable" \
|
|
532
|
+
"corepack prepare yarn@stable --activate"
|
|
533
|
+
fi
|
|
534
|
+
else
|
|
535
|
+
die "$EXIT_INFRA" "neither yarn nor a usable corepack is on PATH" \
|
|
536
|
+
"npm install -g corepack && corepack enable"
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
# --- step: harness currency ---------------------------------------------------
|
|
540
|
+
# This script is the first thing a new machine runs, so it is also the natural
|
|
541
|
+
# place to make sure the tool it hands off to is present and current.
|
|
542
|
+
if [ "$SKIP_HARNESS_UPDATE" = 0 ]; then
|
|
543
|
+
step "Harness"
|
|
544
|
+
if command -v mm-harness >/dev/null 2>&1; then
|
|
545
|
+
ui_ok "mm-harness $(mm-harness --version 2>/dev/null | head -1)"
|
|
546
|
+
ui_info "Update it any time with: npm install -g $HARNESS_PACKAGE@latest"
|
|
547
|
+
else
|
|
548
|
+
ui_warn "mm-harness is not on PATH."
|
|
549
|
+
ui_next "npm install -g $HARNESS_PACKAGE@latest"
|
|
550
|
+
say " (this script still works without it; you need it for the next step,"
|
|
551
|
+
say " \`mm-harness doctor\`.)"
|
|
552
|
+
fi
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
# --- step: selection ----------------------------------------------------------
|
|
556
|
+
if [ "$SELECTION_SOURCE" = "interactive" ]; then
|
|
557
|
+
step "What to set up"
|
|
558
|
+
say "Which projects do you want? Enter numbers separated by commas."
|
|
559
|
+
say " 1) extension 2) mobile 3) core"
|
|
560
|
+
printf '%sProjects [default: all]:%s ' "$C_BOLD" "$C_RESET" >&2
|
|
561
|
+
reply=""; read -r reply || reply=""
|
|
562
|
+
if [ -z "$reply" ]; then
|
|
563
|
+
SELECTED=("${REPO_KEYS[@]}")
|
|
564
|
+
else
|
|
565
|
+
chosen=()
|
|
566
|
+
IFS=',' read -r -a picks <<< "$reply"
|
|
567
|
+
for pick in "${picks[@]}"; do
|
|
568
|
+
pick="$(printf '%s' "$pick" | tr -d '[:space:]')"
|
|
569
|
+
case "$pick" in
|
|
570
|
+
1 | extension) chosen+=(extension) ;;
|
|
571
|
+
2 | mobile) chosen+=(mobile) ;;
|
|
572
|
+
3 | core) chosen+=(core) ;;
|
|
573
|
+
"") ;;
|
|
574
|
+
*) die "$EXIT_USAGE" "'$pick' is not one of 1, 2, or 3" \
|
|
575
|
+
"mm-harness setup-base --only extension,core # skip the prompt entirely" ;;
|
|
576
|
+
esac
|
|
577
|
+
done
|
|
578
|
+
[ "${#chosen[@]}" -gt 0 ] || die "$EXIT_USAGE" "nothing selected" \
|
|
579
|
+
"mm-harness setup-base --only extension,mobile,core"
|
|
580
|
+
SELECTED=("${chosen[@]}")
|
|
581
|
+
fi
|
|
582
|
+
for key in "${SELECTED[@]}"; do
|
|
583
|
+
printf '%sHow many copies of %s? [%s]:%s ' "$C_BOLD" "$key" "$(count_for "$key")" "$C_RESET" >&2
|
|
584
|
+
reply=""; read -r reply || reply=""
|
|
585
|
+
if [ -n "$reply" ]; then
|
|
586
|
+
case "$reply" in
|
|
587
|
+
"" | *[!0-9]*) die "$EXIT_USAGE" "'$reply' is not a whole number" \
|
|
588
|
+
"mm-harness setup-base --counts $key=2" ;;
|
|
589
|
+
esac
|
|
590
|
+
[ "$reply" -ge 1 ] || die "$EXIT_USAGE" "copies for '$key' must be at least 1" \
|
|
591
|
+
"mm-harness setup-base --only $key --counts $key=1"
|
|
592
|
+
set_count "$key" "$reply"
|
|
593
|
+
fi
|
|
594
|
+
done
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
# --- step: plan + disk sanity -------------------------------------------------
|
|
598
|
+
step "Plan"
|
|
599
|
+
say " Base directory: $BASE_DIR ($BASE_SOURCE)"
|
|
600
|
+
[ "$SELECTION_SOURCE" = "saved preferences" ] \
|
|
601
|
+
&& ui_info "Using saved preferences — override with --dir/--only/--counts, inspect with --show-config."
|
|
602
|
+
|
|
603
|
+
PLAN_DIRS=()
|
|
604
|
+
PLAN_KEYS=()
|
|
605
|
+
NEW_CLONES=0
|
|
606
|
+
EST_GB=0
|
|
607
|
+
for key in "${SELECTED[@]}"; do
|
|
608
|
+
slug="$(repo_slug "$key")"
|
|
609
|
+
count="$(count_for "$key")"
|
|
610
|
+
for i in $(seq 1 "$count"); do
|
|
611
|
+
dest="$BASE_DIR/$slug-$i"
|
|
612
|
+
PLAN_KEYS+=("$key")
|
|
613
|
+
PLAN_DIRS+=("$dest")
|
|
614
|
+
if [ ! -e "$dest" ]; then
|
|
615
|
+
NEW_CLONES=$((NEW_CLONES + 1))
|
|
616
|
+
EST_GB=$((EST_GB + $(repo_gb "$key")))
|
|
617
|
+
fi
|
|
618
|
+
done
|
|
619
|
+
say " $(printf '%-10s' "$key") $count copies -> $BASE_DIR/$slug-1..$count"
|
|
620
|
+
done
|
|
621
|
+
|
|
622
|
+
[ "${#PLAN_DIRS[@]}" -gt 0 ] || die "$EXIT_USAGE" "nothing selected to set up" \
|
|
623
|
+
"mm-harness setup-base --only extension,mobile,core"
|
|
624
|
+
|
|
625
|
+
say " $NEW_CLONES of ${#PLAN_DIRS[@]} target directories need a fresh clone (~${EST_GB}GB)."
|
|
626
|
+
|
|
627
|
+
probe="$BASE_DIR"
|
|
628
|
+
while [ ! -d "$probe" ] && [ "$probe" != "/" ]; do probe="$(dirname "$probe")"; done
|
|
629
|
+
avail_kb="$(df -Pk "$probe" 2>/dev/null | awk 'NR==2 {print $4}')"
|
|
630
|
+
if [ "$EST_GB" -eq 0 ]; then
|
|
631
|
+
ui_info "Every target directory already exists — no new clones, so no disk check needed."
|
|
632
|
+
elif [ -z "${avail_kb:-}" ]; then
|
|
633
|
+
ui_warn "Could not read free space for $probe — skipping the disk check."
|
|
634
|
+
else
|
|
635
|
+
avail_gb=$((avail_kb / 1024 / 1024))
|
|
636
|
+
say " Free space on $probe: ${avail_gb}GB"
|
|
637
|
+
if [ "$avail_gb" -lt "$EST_GB" ]; then
|
|
638
|
+
if [ "$FORCE" = 1 ]; then
|
|
639
|
+
ui_warn "Only ${avail_gb}GB free for an estimated ${EST_GB}GB — continuing because --force was given."
|
|
640
|
+
else
|
|
641
|
+
die "$EXIT_INFRA" "not enough disk space: ~${EST_GB}GB needed, ${avail_gb}GB free on $probe" \
|
|
642
|
+
"mm-harness setup-base --counts extension=1,mobile=1,core=1 # fewer copies" \
|
|
643
|
+
"mm-harness setup-base --only core # smaller subset" \
|
|
644
|
+
"mm-harness setup-base --force # override this check"
|
|
645
|
+
fi
|
|
646
|
+
fi
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
650
|
+
ui_section "Dry run"
|
|
651
|
+
for idx in "${!PLAN_DIRS[@]}"; do
|
|
652
|
+
dest="${PLAN_DIRS[$idx]}"
|
|
653
|
+
slug="$(repo_slug "${PLAN_KEYS[$idx]}")"
|
|
654
|
+
if [ -e "$dest" ]; then
|
|
655
|
+
say " fetch + install $dest"
|
|
656
|
+
RESULTS+=("$(basename "$dest") planned-existing planned")
|
|
657
|
+
else
|
|
658
|
+
say " clone + install $dest (git@github.com:MetaMask/$slug.git)"
|
|
659
|
+
RESULTS+=("$(basename "$dest") planned-clone planned")
|
|
660
|
+
fi
|
|
661
|
+
done
|
|
662
|
+
ui_info "Dry run — nothing was created, fetched, or installed."
|
|
663
|
+
finish "$EXIT_OK" "dry-run"
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
# --- step: clone + install ----------------------------------------------------
|
|
667
|
+
step "Clone + install"
|
|
668
|
+
mkdir -p "$BASE_DIR" 2>/dev/null || die "$EXIT_INFRA" "cannot create $BASE_DIR" \
|
|
669
|
+
"mkdir -p '$BASE_DIR' # then re-run, or pass a writable --dir"
|
|
670
|
+
[ -w "$BASE_DIR" ] || die "$EXIT_INFRA" "$BASE_DIR is not writable" \
|
|
671
|
+
"mm-harness setup-base --dir \"\$HOME/dev/metamask\""
|
|
672
|
+
|
|
673
|
+
# clone_repo <slug> <dest> — clone into private staging, then publish atomically.
|
|
674
|
+
clone_repo() {
|
|
675
|
+
local slug="$1" dest="$2" staging candidate
|
|
676
|
+
staging="$(mktemp -d "$BASE_DIR/.mm-harness-clone.XXXXXX")" || return 1
|
|
677
|
+
chmod 700 "$staging"
|
|
678
|
+
candidate="$staging/ssh"
|
|
679
|
+
if ! git clone "git@github.com:MetaMask/$slug.git" "$candidate" >&2; then
|
|
680
|
+
ui_warn "SSH clone of $slug failed — retrying over HTTPS."
|
|
681
|
+
candidate="$staging/https"
|
|
682
|
+
if ! git clone "https://github.com/MetaMask/$slug.git" "$candidate" >&2; then
|
|
683
|
+
rm -rf "$staging"
|
|
684
|
+
return 1
|
|
685
|
+
fi
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
if ! "$NODE_BIN" -e '
|
|
689
|
+
const fs = require("fs");
|
|
690
|
+
const source = process.argv[1];
|
|
691
|
+
const destination = process.argv[2];
|
|
692
|
+
const sourceMode = fs.statSync(source).mode & 0o777;
|
|
693
|
+
let reservation;
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
fs.mkdirSync(destination, { mode: sourceMode });
|
|
697
|
+
reservation = fs.lstatSync(destination);
|
|
698
|
+
} catch (error) {
|
|
699
|
+
if (error && error.code === "EEXIST") process.exit(2);
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
const current = fs.lstatSync(destination);
|
|
705
|
+
if (current.dev !== reservation.dev || current.ino !== reservation.ino) {
|
|
706
|
+
throw new Error("destination reservation changed before publication");
|
|
707
|
+
}
|
|
708
|
+
fs.renameSync(source, destination);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
try {
|
|
711
|
+
const current = fs.lstatSync(destination);
|
|
712
|
+
if (current.dev === reservation.dev && current.ino === reservation.ino) {
|
|
713
|
+
fs.rmdirSync(destination);
|
|
714
|
+
}
|
|
715
|
+
} catch {}
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
' "$candidate" "$dest" 2>/dev/null; then
|
|
719
|
+
ui_fail "$(basename "$dest") appeared while cloning — left untouched."
|
|
720
|
+
rm -rf "$staging"
|
|
721
|
+
return 1
|
|
722
|
+
fi
|
|
723
|
+
rm -rf "$staging"
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
origin_matches() {
|
|
727
|
+
local slug="$1" origin="$2"
|
|
728
|
+
case "$origin" in
|
|
729
|
+
"git@github.com:MetaMask/$slug.git" | \
|
|
730
|
+
"ssh://git@github.com/MetaMask/$slug.git" | \
|
|
731
|
+
"https://github.com/MetaMask/$slug.git" | \
|
|
732
|
+
"https://github.com/MetaMask/$slug") return 0 ;;
|
|
733
|
+
*) return 1 ;;
|
|
734
|
+
esac
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
# install_deps <dest> — run the repo's own dependency install, following the
|
|
738
|
+
# lockfile it ships. Dependencies only.
|
|
739
|
+
install_deps() {
|
|
740
|
+
local dest="$1"
|
|
741
|
+
if [ -f "$dest/yarn.lock" ]; then
|
|
742
|
+
(cd "$dest" && "${YARN_CMD[@]}" install --immutable >&2)
|
|
743
|
+
elif [ -f "$dest/package-lock.json" ]; then
|
|
744
|
+
(cd "$dest" && npm ci >&2)
|
|
745
|
+
elif [ -f "$dest/pnpm-lock.yaml" ]; then
|
|
746
|
+
(cd "$dest" && pnpm install --frozen-lockfile >&2)
|
|
747
|
+
elif [ -f "$dest/package.json" ]; then
|
|
748
|
+
ui_warn "$(basename "$dest"): no lockfile — skipping install."
|
|
749
|
+
else
|
|
750
|
+
ui_warn "$(basename "$dest"): no package.json — skipping install."
|
|
751
|
+
fi
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
for idx in "${!PLAN_DIRS[@]}"; do
|
|
755
|
+
dest="${PLAN_DIRS[$idx]}"
|
|
756
|
+
key="${PLAN_KEYS[$idx]}"
|
|
757
|
+
slug="$(repo_slug "$key")"
|
|
758
|
+
name="$(basename "$dest")"
|
|
759
|
+
clone_state=""
|
|
760
|
+
install_state=""
|
|
761
|
+
|
|
762
|
+
if [ -L "$dest" ]; then
|
|
763
|
+
ui_fail "$name is a symbolic link — left untouched."
|
|
764
|
+
ui_next "move it aside, then re-run: mv $dest $dest.bak"
|
|
765
|
+
RESULTS+=("$name"$'\t'"blocked"$'\t'"skipped")
|
|
766
|
+
FAILURES=$((FAILURES + 1))
|
|
767
|
+
continue
|
|
768
|
+
elif [ -e "$dest" ]; then
|
|
769
|
+
if [ ! -d "$dest/.git" ]; then
|
|
770
|
+
ui_fail "$name exists but is not a git checkout — left untouched."
|
|
771
|
+
ui_next "move it aside, then re-run: mv $dest $dest.bak"
|
|
772
|
+
RESULTS+=("$name blocked skipped")
|
|
773
|
+
FAILURES=$((FAILURES + 1))
|
|
774
|
+
continue
|
|
775
|
+
fi
|
|
776
|
+
origin_url="$(git -C "$dest" remote get-url origin 2>/dev/null || true)"
|
|
777
|
+
if ! origin_matches "$slug" "$origin_url"; then
|
|
778
|
+
ui_fail "$name has origin '$origin_url', expected MetaMask/$slug — left untouched."
|
|
779
|
+
ui_next "point this run elsewhere: mm-harness setup-base --dir <other-base>"
|
|
780
|
+
RESULTS+=("$name wrong-remote skipped")
|
|
781
|
+
FAILURES=$((FAILURES + 1))
|
|
782
|
+
continue
|
|
783
|
+
fi
|
|
784
|
+
ui_info "$name already exists — fetching (no reset, no checkout change)."
|
|
785
|
+
if git -C "$dest" fetch --prune origin >&2; then
|
|
786
|
+
clone_state="existing"
|
|
787
|
+
else
|
|
788
|
+
ui_warn "$name: fetch failed — continuing with the local state."
|
|
789
|
+
ui_next "git -C $dest fetch --prune origin"
|
|
790
|
+
clone_state="existing (stale)"
|
|
791
|
+
fi
|
|
792
|
+
else
|
|
793
|
+
ui_info "Cloning MetaMask/$slug into $name"
|
|
794
|
+
if clone_repo "$slug" "$dest"; then
|
|
795
|
+
clone_state="cloned"
|
|
796
|
+
else
|
|
797
|
+
ui_fail "could not clone MetaMask/$slug over SSH or HTTPS."
|
|
798
|
+
ui_next "ssh -T git@github.com # verify your SSH key"
|
|
799
|
+
ui_next "gh auth login # or authenticate for HTTPS"
|
|
800
|
+
RESULTS+=("$name failed skipped")
|
|
801
|
+
FAILURES=$((FAILURES + 1))
|
|
802
|
+
continue
|
|
803
|
+
fi
|
|
804
|
+
fi
|
|
805
|
+
|
|
806
|
+
ui_info "$name: installing dependencies"
|
|
807
|
+
if install_deps "$dest"; then
|
|
808
|
+
install_state="installed"
|
|
809
|
+
ui_ok "$name ready"
|
|
810
|
+
else
|
|
811
|
+
install_state="failed"
|
|
812
|
+
FAILURES=$((FAILURES + 1))
|
|
813
|
+
ui_fail "$name: dependency install failed."
|
|
814
|
+
# A wrong Node version is by far the most common cause, and these repos pin
|
|
815
|
+
# the version they want in .nvmrc. Note the version your package manager
|
|
816
|
+
# runs is what counts: a version manager can hand yarn a different Node than
|
|
817
|
+
# this shell resolves, so the pin is named but the current version is not.
|
|
818
|
+
if [ -f "$dest/.nvmrc" ]; then
|
|
819
|
+
pinned="$(tr -d '[:space:]' < "$dest/.nvmrc")"
|
|
820
|
+
ui_next "$name pins Node $pinned (.nvmrc) — switch to it, then re-run"
|
|
821
|
+
ui_next "nvm use $pinned # or: asdf set nodejs ${pinned#v}"
|
|
822
|
+
fi
|
|
823
|
+
ui_next "cd $dest && yarn install # re-run to see the full error"
|
|
824
|
+
fi
|
|
825
|
+
|
|
826
|
+
RESULTS+=("$name $clone_state $install_state")
|
|
827
|
+
done
|
|
828
|
+
|
|
829
|
+
# --- step: summary ------------------------------------------------------------
|
|
830
|
+
step "Summary"
|
|
831
|
+
printf ' %-28s %-16s %s\n' 'Directory' 'Clone' 'Install' >&2
|
|
832
|
+
for row in "${RESULTS[@]}"; do
|
|
833
|
+
IFS=$'\t' read -r r_name r_clone r_install <<< "$row"
|
|
834
|
+
printf ' %-28s %-16s %s\n' "$r_name" "$r_clone" "$r_install" >&2
|
|
835
|
+
done
|
|
836
|
+
say ""
|
|
837
|
+
say " Base: $BASE_DIR"
|
|
838
|
+
|
|
839
|
+
EXIT_CODE="$EXIT_OK"
|
|
840
|
+
if [ "$FAILURES" -gt 0 ]; then
|
|
841
|
+
EXIT_CODE="$EXIT_RUNTIME"
|
|
842
|
+
ui_warn "$FAILURES of ${#PLAN_DIRS[@]} directories need attention (see the Next: lines above)."
|
|
843
|
+
else
|
|
844
|
+
ui_ok "All ${#PLAN_DIRS[@]} checkouts are cloned and installed."
|
|
845
|
+
fi
|
|
846
|
+
|
|
847
|
+
# Preferences persist only after a clean run, so a half-broken setup is never
|
|
848
|
+
# adopted as the new default.
|
|
849
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
850
|
+
config_save
|
|
851
|
+
ui_info "Saved preferences to $CONFIG_FILE (--show-config to inspect, --reset-config to clear)."
|
|
852
|
+
fi
|
|
853
|
+
|
|
854
|
+
{
|
|
855
|
+
printf '\n'
|
|
856
|
+
printf 'This script does clones and dependency installs only. It does NOT install\n'
|
|
857
|
+
printf 'platform toolchains, create simulators, write .env files, or run builds.\n'
|
|
858
|
+
printf '\n'
|
|
859
|
+
printf 'Next, check whether this machine can build and run what you just cloned:\n'
|
|
860
|
+
printf '\n'
|
|
861
|
+
printf ' mm-harness doctor\n'
|
|
862
|
+
} >&2
|
|
863
|
+
|
|
864
|
+
finish "$EXIT_CODE"
|