@deeeed/metamask-harness 0.17.4 → 0.17.5

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +6 -103
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -55
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -58
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -107
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -43
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -0,0 +1,28 @@
1
+ # Compatibility overlays (historical-commit replay)
2
+
3
+ NOT dead code. These overlays exist for replay/eval runs against OLD product
4
+ commits — e.g. re-running a historical task on a fixed baseline to validate
5
+ prompt or harness changes. A "zero current references" scan will always come
6
+ up empty here by design: the feature's purpose is non-current checkouts.
7
+
8
+ - Purpose: make historical product checkouts bootable/bridgeable under the
9
+ current local toolchain so Recipe v1 evals can replay against them.
10
+ - When it applies: mobile checkouts predating React Native 0.81 polyfill fixes.
11
+ - How it is applied: MANUALLY (no automatic apply logic in this repo).
12
+ The operator or the `/recipe-harness` skill applies the patch reversibly
13
+ before a historical rebuild (`git apply <patch>` in the product checkout,
14
+ `git apply -R` to remove) and records the overlay path in validation
15
+ evidence. `adapters/mobile/inject.sh` does NOT apply these.
16
+
17
+ Kept in the external runner repository, not Farmslot root scripts, so a
18
+ `/recipe-harness` skill can apply the same reversible overlay before a
19
+ historical rebuild and record the overlay path in validation evidence.
20
+
21
+ Listed in adapters/manifest.json (kind: compat-overlay) so the doctor
22
+ verifies the surface and this directory is never flagged as unused again.
23
+
24
+ ## Patches
25
+
26
+ - `rn81-message-event-source.patch`: adds a read-only `MessageEvent.prototype.source`
27
+ getter for React Native 0.81 historical checkouts whose bundled polyfill omits
28
+ it while CDP websocket clients expect browser-compatible `MessageEvent` shape.
@@ -0,0 +1,42 @@
1
+ compat-overlay: rn81-message-event-source (historical-checkout replay support)
2
+
3
+ What: patches metamask-mobile shim.js to add a read-only
4
+ MessageEvent.prototype.source getter.
5
+ Why: the harness CDP bridge expects browser-compatible MessageEvent shape;
6
+ pre-RN-0.81 checkouts ship a polyfill that omits .source, so the bridge
7
+ cannot attach when replaying tasks on those historical commits.
8
+ When: only when replaying/evaluating against mobile checkouts older than the
9
+ RN 0.81 upgrade (pre-RN-0.81 checkouts). Never needed on current main.
10
+ How: applied MANUALLY in the historical product checkout —
11
+ git apply <this file> (and `git apply -R` to remove)
12
+ No automatic apply logic exists in this repo. See ../README.md.
13
+
14
+ diff --git a/shim.js b/shim.js
15
+ --- a/shim.js
16
+ +++ b/shim.js
17
+ @@
18
+ if (typeof global.MessageEvent === 'undefined') {
19
+ global.MessageEvent =
20
+ require('react-native/src/private/webapis/html/events/MessageEvent').default;
21
+ }
22
+
23
+ +// Historical runner compatibility: React Native 0.81's MessageEvent polyfill
24
+ +// can omit the browser `source` getter expected by CDP websocket clients under
25
+ +// newer Xcode/Hermes toolchains. Keep the getter read-only and null, matching
26
+ +// browser MessageEvent semantics for non-window messages.
27
+ +if (
28
+ + global.MessageEvent?.prototype &&
29
+ + !Object.getOwnPropertyDescriptor(global.MessageEvent.prototype, 'source')
30
+ +) {
31
+ + Object.defineProperty(global.MessageEvent.prototype, 'source', {
32
+ + configurable: true,
33
+ + enumerable: true,
34
+ + get() {
35
+ + return null;
36
+ + },
37
+ + });
38
+ +}
39
+ +
40
+ class AbortError extends Error {
41
+ constructor(message) {
42
+ super(message);
@@ -77,6 +77,45 @@ default_metro_workers() {
77
77
  # shellcheck disable=SC1091
78
78
  . "$SCRIPT_DIR/lib/tmux-viewer.sh"
79
79
 
80
+ # Bridgeless RN gates console→Metro forwarding on console._isPolyfilled, which is
81
+ # false with the Hermes native console — device logs (incl. DevLogger) never reach
82
+ # metro.log. The forwarder attaches over CDP and appends them back. Kill switch:
83
+ # METAMASK_RECIPE_CONSOLE_FORWARD=0.
84
+ start_console_forwarder() {
85
+ local forwarder="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
86
+ local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
87
+ [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
88
+ { [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
89
+ # Only one debugger can own a React Native page. Reap any forwarder already
90
+ # attached to this Metro port, even if it was launched from another checkout or
91
+ # package install path and therefore does not own this runtime dir's pid file.
92
+ local pid cmd
93
+ while read -r pid cmd; do
94
+ case "$cmd" in
95
+ *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
96
+ kill "$pid" 2>/dev/null || true
97
+ ;;
98
+ esac
99
+ done < <(ps -axo pid=,command= 2>/dev/null || true)
100
+ if [ -f "$fwd_pid_file" ]; then
101
+ local old_fwd
102
+ old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
103
+ # A crash can leave a stale pid file and the OS can recycle the pid for an
104
+ # unrelated process — only kill when the command line is really ours.
105
+ if [ -n "$old_fwd" ]; then
106
+ case "$(ps -p "$old_fwd" -o command= 2>/dev/null)" in
107
+ *console-forwarder.cjs*) kill "$old_fwd" 2>/dev/null ;;
108
+ esac
109
+ fi
110
+ rm -f "$fwd_pid_file"
111
+ fi
112
+ nohup node "$forwarder" --port "$PORT" --out "$LOG_FILE" \
113
+ </dev/null >> "$LOG_DIR/console-forwarder.err" 2>&1 &
114
+ echo "$!" > "$fwd_pid_file"
115
+ disown 2>/dev/null || true
116
+ printf 'Device console → metro.log (CDP forwarder pid %s)\n' "$(cat "$fwd_pid_file")" >&2
117
+ }
118
+
80
119
  write_metro_runner() {
81
120
  local runner="$1"
82
121
  local clear_flag=""
@@ -140,6 +179,7 @@ if metro_ready; then
140
179
  rm -f "$PID_FILE"
141
180
  elif metro_listener_valid; then
142
181
  printf 'Metro already running and valid on port %s\n' "$PORT" >&2
182
+ start_console_forwarder
143
183
  exit 0
144
184
  else
145
185
  stop_metro_listener || exit 1
@@ -229,6 +269,7 @@ if [ "$READY" = true ]; then
229
269
  first_ready_pid="${ready_pids%% *}"
230
270
  [ -z "$first_ready_pid" ] || printf '%s\n' "$first_ready_pid" > "$PID_FILE"
231
271
  printf 'Metro ready on port %s\n' "$PORT" >&2
272
+ start_console_forwarder
232
273
  printf ' Next: mm-harness logs (tail Metro)\n' >&2
233
274
  exit 0
234
275
  fi
@@ -261,19 +261,76 @@ check_file "package.json"
261
261
  check_file "app/dev-tools/AgenticService/AgenticService.ts"
262
262
 
263
263
  if ! grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null; then
264
- checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"fail\"}")
264
+ checks+=("{\"name\":\"NavigationService patch\",\"status\":\"fail\"}")
265
265
  status="fail"
266
266
  else
267
- checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"pass\"}")
267
+ checks+=("{\"name\":\"NavigationService patch\",\"status\":\"pass\"}")
268
268
  fi
269
269
 
270
270
  if ! grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null; then
271
- checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"fail\"}")
271
+ checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"fail\"}")
272
272
  status="fail"
273
273
  else
274
- checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"pass\"}")
274
+ checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"pass\"}")
275
275
  fi
276
276
 
277
+ # Drift detection (read-only): compare the in-repo AgenticService/HUD against
278
+ # the resolved runner overlay, not a runner-owned copy. The runner is the only
279
+ # MetaMask-aware source of truth for Mobile injection.
280
+ overlay_drift_check="$(
281
+ HARNESS_DIR_FOR_DRIFT="$HARNESS_DIR" TARGET_FOR_DRIFT="$TARGET" node <<'NODE'
282
+ const fs = require('fs');
283
+ const path = require('path');
284
+ const name = 'agentic overlay matches runner (HUD freshness)';
285
+ const harnessDir = process.env.HARNESS_DIR_FOR_DRIFT;
286
+ const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'dev-tools', 'AgenticService');
287
+ function readTrimmed(file) {
288
+ try { return fs.readFileSync(file, 'utf8').trim(); } catch { return ''; }
289
+ }
290
+ let runnerDir = readTrimmed(path.join(harnessDir, 'runner', '.runner-source'));
291
+ if (!runnerDir) {
292
+ try {
293
+ const manifest = JSON.parse(fs.readFileSync(path.join(harnessDir, 'manifest.json'), 'utf8'));
294
+ runnerDir = manifest && manifest.source && manifest.source.runnerDir || '';
295
+ } catch {}
296
+ }
297
+ if (!runnerDir) {
298
+ process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
299
+ process.exit(0);
300
+ }
301
+ const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'dev-tools', 'AgenticService');
302
+ let checked = 0;
303
+ const drifted = [];
304
+ const missing = [];
305
+ try {
306
+ for (const entry of fs.readdirSync(overlayDir)) {
307
+ if (!entry.endsWith('.patch')) continue;
308
+ if (/\.test\./u.test(entry)) continue;
309
+ const base = entry.slice(0, -'.patch'.length);
310
+ const targetFile = path.join(targetDir, base);
311
+ if (!fs.existsSync(targetFile)) { missing.push(base); continue; }
312
+ checked += 1;
313
+ if (fs.readFileSync(path.join(overlayDir, entry), 'utf8') !== fs.readFileSync(targetFile, 'utf8')) {
314
+ drifted.push(base);
315
+ }
316
+ }
317
+ } catch (error) {
318
+ process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner overlay compare skipped: ' + String((error && error.message) || error) }));
319
+ process.exit(0);
320
+ }
321
+ if (drifted.length || missing.length) {
322
+ const parts = [];
323
+ if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
324
+ if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
325
+ process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
326
+ } else if (checked === 0) {
327
+ process.stdout.write(JSON.stringify({ name, status: 'pass' }));
328
+ } else {
329
+ process.stdout.write(JSON.stringify({ name, status: 'pass' }));
330
+ }
331
+ NODE
332
+ )"
333
+ checks+=("$overlay_drift_check")
277
334
  run_with_timeout() {
278
335
  local log_path="$1"
279
336
  local timeout_s="$2"
@@ -24,10 +24,10 @@ REQUIRE_PLATFORM=""
24
24
 
25
25
  while [ "$#" -gt 0 ]; do
26
26
  case "$1" in
27
- --target) [ "$#" -ge 2 ] || { echo "Missing value for --target" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
28
- --port) [ "$#" -ge 2 ] || { echo "Missing value for --port" >&2; exit 2; }; PORT="$2"; shift 2 ;;
29
- --max-polls) [ "$#" -ge 2 ] || { echo "Missing value for --max-polls" >&2; exit 2; }; MAX_POLLS="$2"; shift 2 ;;
30
- --platform) [ "$#" -ge 2 ] || { echo "Missing value for --platform" >&2; exit 2; }; REQUIRE_PLATFORM="$2"; shift 2 ;;
27
+ --target) TARGET="$2"; shift 2 ;;
28
+ --port) PORT="$2"; shift 2 ;;
29
+ --max-polls) MAX_POLLS="$2"; shift 2 ;;
30
+ --platform) REQUIRE_PLATFORM="$2"; shift 2 ;;
31
31
  -h|--help)
32
32
  printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>] [--platform ios|android]\n'
33
33
  exit 0
@@ -36,12 +36,7 @@ while [ "$#" -gt 0 ]; do
36
36
  esac
37
37
  done
38
38
 
39
- case "$MAX_POLLS" in
40
- ''|*[!0-9]*) echo "wait-for-bridge: --max-polls must be an integer of at least 2." >&2; exit 2 ;;
41
- esac
42
- [ "$MAX_POLLS" -ge 2 ] || { echo "wait-for-bridge: --max-polls must be at least 2 for stable readiness." >&2; exit 2; }
43
-
44
- TARGET="$(cd "$TARGET" && pwd -P)"
39
+ TARGET="$(cd "$TARGET" && pwd)"
45
40
 
46
41
  # The matcher reads the required platform from the environment so the value survives
47
42
  # into the node subprocess that runs the shared matcher against the status output.
@@ -133,24 +128,15 @@ try {
133
128
  ' "$MATCH_LIB" "$STATUS_LOG" 2>/dev/null || printf 'none'
134
129
  }
135
130
 
136
- READY_STREAK=0
137
131
  for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
138
132
  if bridge_has_route; then
139
- READY_STREAK=$((READY_STREAK + 1))
140
- # The bridge can appear briefly while React Native is still completing its
141
- # startup navigation/reload. Require two consecutive healthy snapshots so a
142
- # following command never inherits that transient gap.
143
- if [ "$READY_STREAK" -ge 2 ]; then
144
- printf 'Mobile bridge ready\n' >&2
145
- exit 0
146
- fi
147
- else
148
- READY_STREAK=0
133
+ printf 'Mobile bridge ready\n' >&2
134
+ exit 0
149
135
  fi
150
136
  if [ "$ATTEMPT" = "1" ] || [ $((ATTEMPT % 5)) -eq 0 ]; then
151
137
  printf 'Waiting for Mobile bridge (%s/%s): %s\n' "$ATTEMPT" "$MAX_POLLS" "$(bridge_wait_reason)" >&2
152
138
  fi
153
- [ "$ATTEMPT" -ge "$MAX_POLLS" ] || sleep 2
139
+ sleep 2
154
140
  done
155
141
 
156
142
  cat "$STATUS_LOG" >&2 || true
@@ -0,0 +1,49 @@
1
+ // harness-source-fingerprint.mjs — deterministic fingerprint of the runner files
2
+ // that get injected into a slot's harness overlay.
3
+ //
4
+ // inject.mjs stamps this into the slot manifest at install time; the launch-time
5
+ // freshness guard recomputes it and re-injects when they differ, so a slot can
6
+ // never silently run a stale injected launcher after the runner changes. This is
7
+ // what makes the runner the single source of truth: edit a runner adapter file and
8
+ // the next launch refreshes every slot from it.
9
+ //
10
+ // stat-only (size + mtime) so it is cheap enough to run on every launch; no file
11
+ // contents are read and no Date.now()/random is used (both would break determinism).
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import crypto from 'node:crypto';
15
+
16
+ // Trees whose contents are copied into <slot>/harness by inject.mjs. Any change
17
+ // here means injected slots are stale.
18
+ const SOURCE_DIRS = ['adapters/extension', 'adapters/shared', 'library'];
19
+
20
+ export function harnessSourceFingerprint(runnerDir) {
21
+ const entries = [];
22
+ for (const rel of SOURCE_DIRS) {
23
+ collect(path.join(runnerDir, rel), runnerDir, entries);
24
+ }
25
+ entries.sort();
26
+ return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
27
+ }
28
+
29
+ function collect(dir, root, out) {
30
+ let dirents;
31
+ try {
32
+ dirents = fs.readdirSync(dir, { withFileTypes: true });
33
+ } catch {
34
+ return; // absent tree contributes nothing
35
+ }
36
+ for (const dirent of dirents) {
37
+ // Skip hidden noise (.DS_Store, editor swap files) so the fingerprint tracks
38
+ // only real source and does not thrash re-injects.
39
+ if (dirent.name.startsWith('.')) continue;
40
+ const full = path.join(dir, dirent.name);
41
+ if (dirent.isDirectory()) {
42
+ collect(full, root, out);
43
+ continue;
44
+ }
45
+ if (!dirent.isFile()) continue;
46
+ const stat = fs.statSync(full);
47
+ out.push(`${path.relative(root, full)}|${stat.size}|${Math.floor(stat.mtimeMs)}`);
48
+ }
49
+ }
@@ -4,11 +4,7 @@ set -euo pipefail
4
4
  TARGET=""
5
5
  while [[ $# -gt 0 ]]; do
6
6
  case "$1" in
7
- --target)
8
- [[ $# -ge 2 ]] || { echo "Usage: install-repo-deps.sh --target <checkout>" >&2; exit 2; }
9
- TARGET="$2"
10
- shift 2
11
- ;;
7
+ --target) TARGET="$2"; shift 2 ;;
12
8
  -h|--help)
13
9
  echo "Usage: install-repo-deps.sh --target <checkout>"
14
10
  exit 0