@osfactory/har 0.49.1 → 0.50.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.
@@ -27,6 +27,119 @@ pt_log() {
27
27
  fi
28
28
  }
29
29
 
30
+ # ── Node package manager helpers ──────────────────────────────────────────────
31
+ # Mirrored verbatim in harness.env and provision-toolchain.sh so a sourcing
32
+ # script and the provisioning subprocess resolve the same tool.
33
+
34
+ # Package managers HAR can drive, in fallback preference order.
35
+ HAR_NODE_PACKAGE_MANAGERS="npm bun pnpm yarn"
36
+
37
+ # Package manager the repo declares: an explicit HARNESS_NODE_PACKAGE_MANAGER
38
+ # wins, then package.json "packageManager", then the lockfile. Empty when the
39
+ # repo says nothing.
40
+ har_node_declared_package_manager() {
41
+ local dir="${1:-.}"
42
+
43
+ if [ -n "${HARNESS_NODE_PACKAGE_MANAGER:-}" ]; then
44
+ echo "$HARNESS_NODE_PACKAGE_MANAGER"
45
+ return
46
+ fi
47
+ if [ -f "$dir/package.json" ]; then
48
+ local field
49
+ field="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' "$dir/package.json" | head -1)"
50
+ if [ -n "$field" ]; then
51
+ echo "$field"
52
+ return
53
+ fi
54
+ fi
55
+ if [ -f "$dir/bun.lock" ] || [ -f "$dir/bun.lockb" ]; then echo bun; return; fi
56
+ if [ -f "$dir/pnpm-lock.yaml" ]; then echo pnpm; return; fi
57
+ if [ -f "$dir/yarn.lock" ]; then echo yarn; return; fi
58
+ if [ -f "$dir/package-lock.json" ]; then echo npm; return; fi
59
+ echo ""
60
+ }
61
+
62
+ # Package manager to actually run. A manager the repo declares but this machine
63
+ # lacks falls back to one that is installed, so a repo pinned to npm still
64
+ # provisions on a bun-only machine (and the reverse).
65
+ har_node_package_manager() {
66
+ local dir="${1:-.}"
67
+ local declared
68
+ declared="$(har_node_declared_package_manager "$dir")"
69
+
70
+ if [ -n "$declared" ] && command -v "$declared" >/dev/null 2>&1; then
71
+ echo "$declared"
72
+ return
73
+ fi
74
+
75
+ local candidate
76
+ for candidate in $HAR_NODE_PACKAGE_MANAGERS; do
77
+ if command -v "$candidate" >/dev/null 2>&1; then
78
+ echo "$candidate"
79
+ return
80
+ fi
81
+ done
82
+
83
+ echo "${declared:-npm}"
84
+ }
85
+
86
+ # Lockfile a manager writes, so a substitute install can clean up after itself.
87
+ har_node_lockfile() {
88
+ case "$1" in
89
+ bun) echo "bun.lock" ;;
90
+ npm) echo "package-lock.json" ;;
91
+ pnpm) echo "pnpm-lock.yaml" ;;
92
+ yarn) echo "yarn.lock" ;;
93
+ *) echo "" ;;
94
+ esac
95
+ }
96
+
97
+ # Install dependencies in a directory. When a substitute manager stands in for
98
+ # the one the repo declares, any lockfile it creates is removed afterwards:
99
+ # provisioning must not migrate the repo to a different package manager.
100
+ # (bun writes bun.lock even under --no-save, so the flags alone are not enough.)
101
+ har_node_install() {
102
+ local dir="$1"
103
+ local manager="$2"
104
+ local declared="${3:-}"
105
+ local substituting=false
106
+ local lockfile=""
107
+ local had_lockfile=false
108
+
109
+ if [ -n "$declared" ] && [ "$declared" != "$manager" ]; then
110
+ substituting=true
111
+ lockfile="$(har_node_lockfile "$manager")"
112
+ if [ -n "$lockfile" ] && [ -e "$dir/$lockfile" ]; then
113
+ had_lockfile=true
114
+ fi
115
+ fi
116
+
117
+ local code=0
118
+ (cd "$dir" && "$manager" install --silent) || code=$?
119
+
120
+ if [ "$substituting" = true ] && [ "$had_lockfile" = false ] && [ -n "$lockfile" ]; then
121
+ rm -f "$dir/$lockfile"
122
+ [ "$manager" = "bun" ] && rm -f "$dir/bun.lockb"
123
+ fi
124
+ return $code
125
+ }
126
+
127
+ # Package runner (npx equivalent) for one-off CLIs such as pm2, including the
128
+ # flag that keeps it non-interactive. Takes a manager name, or none to resolve
129
+ # from the environment and PATH.
130
+ har_pkg_exec() {
131
+ case "${1:-${HARNESS_NODE_PACKAGE_MANAGER:-}}" in
132
+ bun) echo "bunx"; return ;;
133
+ pnpm) echo "pnpm dlx"; return ;;
134
+ yarn) echo "yarn dlx"; return ;;
135
+ npm) echo "npx --yes"; return ;;
136
+ esac
137
+ if command -v npx >/dev/null 2>&1; then echo "npx --yes"; return; fi
138
+ if command -v bunx >/dev/null 2>&1; then echo "bunx"; return; fi
139
+ if command -v pnpm >/dev/null 2>&1; then echo "pnpm dlx"; return; fi
140
+ echo "npx --yes"
141
+ }
142
+
30
143
  append_env() {
31
144
  local key="$1"
32
145
  local value="$2"
@@ -81,32 +194,37 @@ run_install_cmd() {
81
194
 
82
195
  provision_node() {
83
196
  local dir="$1"
84
- local npm_bin="npm"
85
197
  local node_bin="node"
198
+ local pkg_manager declared_manager
199
+ declared_manager="$(har_node_declared_package_manager "$dir")"
200
+ pkg_manager="$(har_node_package_manager "$dir")"
201
+
202
+ if [ -n "$declared_manager" ] && [ "$declared_manager" != "$pkg_manager" ]; then
203
+ pt_log "Repo declares ${declared_manager}, which is not on PATH — installing with ${pkg_manager} and leaving the lockfile untouched."
204
+ fi
86
205
 
87
206
  if ! run_install_cmd "$dir"; then
88
- pt_log "Installing Node dependencies..."
89
- if [ -f "$dir/pnpm-lock.yaml" ] && command -v pnpm >/dev/null 2>&1; then
90
- (cd "$dir" && pnpm install --silent)
91
- npm_bin="pnpm"
92
- elif [ -f "$dir/yarn.lock" ] && command -v yarn >/dev/null 2>&1; then
93
- (cd "$dir" && yarn install --silent)
94
- npm_bin="yarn"
95
- else
96
- (cd "$dir" && npm install --silent)
97
- fi
207
+ pt_log "Installing Node dependencies with ${pkg_manager}..."
208
+ har_node_install "$dir" "$pkg_manager" "$declared_manager"
98
209
  fi
99
210
 
100
211
  if command -v node >/dev/null 2>&1; then
101
212
  node_bin="$(command -v node)"
213
+ elif [ "$pkg_manager" = "bun" ]; then
214
+ # bun-only machine: bun runs the `node -e` snippets verify.sh relies on.
215
+ node_bin="$(command -v bun)"
102
216
  fi
103
- if [ "$npm_bin" = "npm" ] && command -v npm >/dev/null 2>&1; then
104
- npm_bin="$(command -v npm)"
217
+
218
+ local npm_bin="$pkg_manager"
219
+ if command -v "$pkg_manager" >/dev/null 2>&1; then
220
+ npm_bin="$(command -v "$pkg_manager")"
105
221
  fi
106
222
 
107
223
  append_env "HARNESS_ECOSYSTEM" "node"
108
224
  append_env "NODE_BIN" "$node_bin"
109
225
  append_env "NPM_BIN" "$npm_bin"
226
+ append_env "HARNESS_NODE_PACKAGE_MANAGER" "$pkg_manager"
227
+ append_env "HARNESS_PKG_EXEC" "$(har_pkg_exec "$pkg_manager")"
110
228
  append_path_prefix "$dir/node_modules/.bin"
111
229
  }
112
230
 
@@ -252,8 +370,11 @@ provision_monorepo_root() {
252
370
  [ -n "$HAR_WORKTREE_DIR" ] || return 0
253
371
  [ -f "$HAR_WORKTREE_DIR/package.json" ] || return 0
254
372
  [ -d "$HAR_WORKTREE_DIR/node_modules" ] && return 0
255
- pt_log "Installing monorepo root dependencies in $HAR_WORKTREE_DIR..."
256
- (cd "$HAR_WORKTREE_DIR" && npm install --silent)
373
+ local pkg_manager
374
+ pkg_manager="$(har_node_package_manager "$HAR_WORKTREE_DIR")"
375
+ pt_log "Installing monorepo root dependencies in $HAR_WORKTREE_DIR with ${pkg_manager}..."
376
+ har_node_install "$HAR_WORKTREE_DIR" "$pkg_manager" \
377
+ "$(har_node_declared_package_manager "$HAR_WORKTREE_DIR")"
257
378
  }
258
379
 
259
380
  provision_ecosystem() {
@@ -165,7 +165,7 @@ fi
165
165
  SHARED_ECOSYSTEM="$SCRIPT_DIR/ecosystem.shared.config.cjs"
166
166
  if [ -f "$SHARED_ECOSYSTEM" ]; then
167
167
  log "Starting shared app services from ecosystem.shared.config.cjs..."
168
- (cd "$REPO_ROOT" && npx --yes pm2 startOrReload "$SHARED_ECOSYSTEM" >/dev/null)
168
+ (cd "$REPO_ROOT" && $(har_pkg_exec) pm2 startOrReload "$SHARED_ECOSYSTEM" >/dev/null)
169
169
  log "Shared app services running (pm2 ls | grep har-${HARNESS_PROJECT_NAME}-shared-)."
170
170
  fi
171
171
 
@@ -41,7 +41,7 @@ fi
41
41
  [ -n "$WORKTREE_PATH" ] || WORKTREE_PATH="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${AGENT_ID}"
42
42
 
43
43
  PM2_REGEX="$(har_pm2_delete_regex "$AGENT_ID")"
44
- npx --yes pm2 delete "$PM2_REGEX" 2>/dev/null || true
44
+ $(har_pkg_exec) pm2 delete "$PM2_REGEX" 2>/dev/null || true
45
45
  echo "✓ Stopped PM2 processes"
46
46
 
47
47
  if har_infra_enabled db; then
@@ -164,7 +164,7 @@ har_check_foreign_pm2() {
164
164
  local agent_id="$1"
165
165
  local pm2_raw
166
166
  har_harness_uses_pm2 || return 0
167
- pm2_raw="$(npx --yes pm2 jlist 2>/dev/null || true)"
167
+ pm2_raw="$($(har_pkg_exec) pm2 jlist 2>/dev/null || true)"
168
168
  [ -n "$pm2_raw" ] || return 0
169
169
  set +e
170
170
  echo "$pm2_raw" | node -e "
@@ -23,6 +23,10 @@ export HARNESS_AGENT_SLOT_MAX=3
23
23
  export HARNESS_ECOSYSTEM="auto"
24
24
  # Optional install override (eval in work dir after env file is created)
25
25
  # export HARNESS_INSTALL_CMD="make deps"
26
+ # Node package manager: auto-detected from package.json "packageManager" and the
27
+ # lockfile (bun.lock → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm),
28
+ # falling back to whichever is installed. Set to pin one: npm | bun | pnpm | yarn
29
+ # export HARNESS_NODE_PACKAGE_MANAGER="bun"
26
30
 
27
31
  # Shared infrastructure — space-separated docker compose service names from
28
32
  # docker-compose.agent.yml, started ONCE by setup-infra.sh and shared by all
@@ -38,6 +42,119 @@ har_infra_enabled() {
38
42
  esac
39
43
  }
40
44
 
45
+ # ── Node package manager helpers ──────────────────────────────────────────────
46
+ # Mirrored verbatim in harness.env and provision-toolchain.sh so a sourcing
47
+ # script and the provisioning subprocess resolve the same tool.
48
+
49
+ # Package managers HAR can drive, in fallback preference order.
50
+ HAR_NODE_PACKAGE_MANAGERS="npm bun pnpm yarn"
51
+
52
+ # Package manager the repo declares: an explicit HARNESS_NODE_PACKAGE_MANAGER
53
+ # wins, then package.json "packageManager", then the lockfile. Empty when the
54
+ # repo says nothing.
55
+ har_node_declared_package_manager() {
56
+ local dir="${1:-.}"
57
+
58
+ if [ -n "${HARNESS_NODE_PACKAGE_MANAGER:-}" ]; then
59
+ echo "$HARNESS_NODE_PACKAGE_MANAGER"
60
+ return
61
+ fi
62
+ if [ -f "$dir/package.json" ]; then
63
+ local field
64
+ field="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' "$dir/package.json" | head -1)"
65
+ if [ -n "$field" ]; then
66
+ echo "$field"
67
+ return
68
+ fi
69
+ fi
70
+ if [ -f "$dir/bun.lock" ] || [ -f "$dir/bun.lockb" ]; then echo bun; return; fi
71
+ if [ -f "$dir/pnpm-lock.yaml" ]; then echo pnpm; return; fi
72
+ if [ -f "$dir/yarn.lock" ]; then echo yarn; return; fi
73
+ if [ -f "$dir/package-lock.json" ]; then echo npm; return; fi
74
+ echo ""
75
+ }
76
+
77
+ # Package manager to actually run. A manager the repo declares but this machine
78
+ # lacks falls back to one that is installed, so a repo pinned to npm still
79
+ # provisions on a bun-only machine (and the reverse).
80
+ har_node_package_manager() {
81
+ local dir="${1:-.}"
82
+ local declared
83
+ declared="$(har_node_declared_package_manager "$dir")"
84
+
85
+ if [ -n "$declared" ] && command -v "$declared" >/dev/null 2>&1; then
86
+ echo "$declared"
87
+ return
88
+ fi
89
+
90
+ local candidate
91
+ for candidate in $HAR_NODE_PACKAGE_MANAGERS; do
92
+ if command -v "$candidate" >/dev/null 2>&1; then
93
+ echo "$candidate"
94
+ return
95
+ fi
96
+ done
97
+
98
+ echo "${declared:-npm}"
99
+ }
100
+
101
+ # Lockfile a manager writes, so a substitute install can clean up after itself.
102
+ har_node_lockfile() {
103
+ case "$1" in
104
+ bun) echo "bun.lock" ;;
105
+ npm) echo "package-lock.json" ;;
106
+ pnpm) echo "pnpm-lock.yaml" ;;
107
+ yarn) echo "yarn.lock" ;;
108
+ *) echo "" ;;
109
+ esac
110
+ }
111
+
112
+ # Install dependencies in a directory. When a substitute manager stands in for
113
+ # the one the repo declares, any lockfile it creates is removed afterwards:
114
+ # provisioning must not migrate the repo to a different package manager.
115
+ # (bun writes bun.lock even under --no-save, so the flags alone are not enough.)
116
+ har_node_install() {
117
+ local dir="$1"
118
+ local manager="$2"
119
+ local declared="${3:-}"
120
+ local substituting=false
121
+ local lockfile=""
122
+ local had_lockfile=false
123
+
124
+ if [ -n "$declared" ] && [ "$declared" != "$manager" ]; then
125
+ substituting=true
126
+ lockfile="$(har_node_lockfile "$manager")"
127
+ if [ -n "$lockfile" ] && [ -e "$dir/$lockfile" ]; then
128
+ had_lockfile=true
129
+ fi
130
+ fi
131
+
132
+ local code=0
133
+ (cd "$dir" && "$manager" install --silent) || code=$?
134
+
135
+ if [ "$substituting" = true ] && [ "$had_lockfile" = false ] && [ -n "$lockfile" ]; then
136
+ rm -f "$dir/$lockfile"
137
+ [ "$manager" = "bun" ] && rm -f "$dir/bun.lockb"
138
+ fi
139
+ return $code
140
+ }
141
+
142
+ # Package runner (npx equivalent) for one-off CLIs such as pm2, including the
143
+ # flag that keeps it non-interactive. Takes a manager name, or none to resolve
144
+ # from the environment and PATH.
145
+ har_pkg_exec() {
146
+ case "${1:-${HARNESS_NODE_PACKAGE_MANAGER:-}}" in
147
+ bun) echo "bunx"; return ;;
148
+ pnpm) echo "pnpm dlx"; return ;;
149
+ yarn) echo "yarn dlx"; return ;;
150
+ npm) echo "npx --yes"; return ;;
151
+ esac
152
+ if command -v npx >/dev/null 2>&1; then echo "npx --yes"; return; fi
153
+ if command -v bunx >/dev/null 2>&1; then echo "bunx"; return; fi
154
+ if command -v pnpm >/dev/null 2>&1; then echo "pnpm dlx"; return; fi
155
+ echo "npx --yes"
156
+ }
157
+
41
158
  # Postgres client — uses host tools when installed, otherwise docker exec into
42
159
  # the shared db container. Usage: har_pg psql -d postgres -c "SELECT 1"
43
160
  har_pg() {
@@ -187,8 +187,9 @@ elif [ -f "$SCRIPT_DIR/provision-toolchain.sh" ]; then
187
187
  HAR_AGENT_ID="$AGENT_ID" \
188
188
  "$SCRIPT_DIR/provision-toolchain.sh"
189
189
  elif [ -f "$WORK_DIR/package.json" ] && [ ! -d "$WORK_DIR/node_modules" ]; then
190
- log "Installing dependencies in $WORK_DIR..."
191
- (cd "$WORK_DIR" && npm install --silent)
190
+ PKG_MANAGER="$(har_node_package_manager "$WORK_DIR")"
191
+ log "Installing dependencies in $WORK_DIR with ${PKG_MANAGER}..."
192
+ har_node_install "$WORK_DIR" "$PKG_MANAGER" "$(har_node_declared_package_manager "$WORK_DIR")"
192
193
  fi
193
194
 
194
195
  SLOT_AGENT_ID="$AGENT_ID" \
@@ -27,6 +27,119 @@ pt_log() {
27
27
  fi
28
28
  }
29
29
 
30
+ # ── Node package manager helpers ──────────────────────────────────────────────
31
+ # Mirrored verbatim in harness.env and provision-toolchain.sh so a sourcing
32
+ # script and the provisioning subprocess resolve the same tool.
33
+
34
+ # Package managers HAR can drive, in fallback preference order.
35
+ HAR_NODE_PACKAGE_MANAGERS="npm bun pnpm yarn"
36
+
37
+ # Package manager the repo declares: an explicit HARNESS_NODE_PACKAGE_MANAGER
38
+ # wins, then package.json "packageManager", then the lockfile. Empty when the
39
+ # repo says nothing.
40
+ har_node_declared_package_manager() {
41
+ local dir="${1:-.}"
42
+
43
+ if [ -n "${HARNESS_NODE_PACKAGE_MANAGER:-}" ]; then
44
+ echo "$HARNESS_NODE_PACKAGE_MANAGER"
45
+ return
46
+ fi
47
+ if [ -f "$dir/package.json" ]; then
48
+ local field
49
+ field="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' "$dir/package.json" | head -1)"
50
+ if [ -n "$field" ]; then
51
+ echo "$field"
52
+ return
53
+ fi
54
+ fi
55
+ if [ -f "$dir/bun.lock" ] || [ -f "$dir/bun.lockb" ]; then echo bun; return; fi
56
+ if [ -f "$dir/pnpm-lock.yaml" ]; then echo pnpm; return; fi
57
+ if [ -f "$dir/yarn.lock" ]; then echo yarn; return; fi
58
+ if [ -f "$dir/package-lock.json" ]; then echo npm; return; fi
59
+ echo ""
60
+ }
61
+
62
+ # Package manager to actually run. A manager the repo declares but this machine
63
+ # lacks falls back to one that is installed, so a repo pinned to npm still
64
+ # provisions on a bun-only machine (and the reverse).
65
+ har_node_package_manager() {
66
+ local dir="${1:-.}"
67
+ local declared
68
+ declared="$(har_node_declared_package_manager "$dir")"
69
+
70
+ if [ -n "$declared" ] && command -v "$declared" >/dev/null 2>&1; then
71
+ echo "$declared"
72
+ return
73
+ fi
74
+
75
+ local candidate
76
+ for candidate in $HAR_NODE_PACKAGE_MANAGERS; do
77
+ if command -v "$candidate" >/dev/null 2>&1; then
78
+ echo "$candidate"
79
+ return
80
+ fi
81
+ done
82
+
83
+ echo "${declared:-npm}"
84
+ }
85
+
86
+ # Lockfile a manager writes, so a substitute install can clean up after itself.
87
+ har_node_lockfile() {
88
+ case "$1" in
89
+ bun) echo "bun.lock" ;;
90
+ npm) echo "package-lock.json" ;;
91
+ pnpm) echo "pnpm-lock.yaml" ;;
92
+ yarn) echo "yarn.lock" ;;
93
+ *) echo "" ;;
94
+ esac
95
+ }
96
+
97
+ # Install dependencies in a directory. When a substitute manager stands in for
98
+ # the one the repo declares, any lockfile it creates is removed afterwards:
99
+ # provisioning must not migrate the repo to a different package manager.
100
+ # (bun writes bun.lock even under --no-save, so the flags alone are not enough.)
101
+ har_node_install() {
102
+ local dir="$1"
103
+ local manager="$2"
104
+ local declared="${3:-}"
105
+ local substituting=false
106
+ local lockfile=""
107
+ local had_lockfile=false
108
+
109
+ if [ -n "$declared" ] && [ "$declared" != "$manager" ]; then
110
+ substituting=true
111
+ lockfile="$(har_node_lockfile "$manager")"
112
+ if [ -n "$lockfile" ] && [ -e "$dir/$lockfile" ]; then
113
+ had_lockfile=true
114
+ fi
115
+ fi
116
+
117
+ local code=0
118
+ (cd "$dir" && "$manager" install --silent) || code=$?
119
+
120
+ if [ "$substituting" = true ] && [ "$had_lockfile" = false ] && [ -n "$lockfile" ]; then
121
+ rm -f "$dir/$lockfile"
122
+ [ "$manager" = "bun" ] && rm -f "$dir/bun.lockb"
123
+ fi
124
+ return $code
125
+ }
126
+
127
+ # Package runner (npx equivalent) for one-off CLIs such as pm2, including the
128
+ # flag that keeps it non-interactive. Takes a manager name, or none to resolve
129
+ # from the environment and PATH.
130
+ har_pkg_exec() {
131
+ case "${1:-${HARNESS_NODE_PACKAGE_MANAGER:-}}" in
132
+ bun) echo "bunx"; return ;;
133
+ pnpm) echo "pnpm dlx"; return ;;
134
+ yarn) echo "yarn dlx"; return ;;
135
+ npm) echo "npx --yes"; return ;;
136
+ esac
137
+ if command -v npx >/dev/null 2>&1; then echo "npx --yes"; return; fi
138
+ if command -v bunx >/dev/null 2>&1; then echo "bunx"; return; fi
139
+ if command -v pnpm >/dev/null 2>&1; then echo "pnpm dlx"; return; fi
140
+ echo "npx --yes"
141
+ }
142
+
30
143
  append_env() {
31
144
  local key="$1"
32
145
  local value="$2"
@@ -81,32 +194,37 @@ run_install_cmd() {
81
194
 
82
195
  provision_node() {
83
196
  local dir="$1"
84
- local npm_bin="npm"
85
197
  local node_bin="node"
198
+ local pkg_manager declared_manager
199
+ declared_manager="$(har_node_declared_package_manager "$dir")"
200
+ pkg_manager="$(har_node_package_manager "$dir")"
201
+
202
+ if [ -n "$declared_manager" ] && [ "$declared_manager" != "$pkg_manager" ]; then
203
+ pt_log "Repo declares ${declared_manager}, which is not on PATH — installing with ${pkg_manager} and leaving the lockfile untouched."
204
+ fi
86
205
 
87
206
  if ! run_install_cmd "$dir"; then
88
- pt_log "Installing Node dependencies..."
89
- if [ -f "$dir/pnpm-lock.yaml" ] && command -v pnpm >/dev/null 2>&1; then
90
- (cd "$dir" && pnpm install --silent)
91
- npm_bin="pnpm"
92
- elif [ -f "$dir/yarn.lock" ] && command -v yarn >/dev/null 2>&1; then
93
- (cd "$dir" && yarn install --silent)
94
- npm_bin="yarn"
95
- else
96
- (cd "$dir" && npm install --silent)
97
- fi
207
+ pt_log "Installing Node dependencies with ${pkg_manager}..."
208
+ har_node_install "$dir" "$pkg_manager" "$declared_manager"
98
209
  fi
99
210
 
100
211
  if command -v node >/dev/null 2>&1; then
101
212
  node_bin="$(command -v node)"
213
+ elif [ "$pkg_manager" = "bun" ]; then
214
+ # bun-only machine: bun runs the `node -e` snippets verify.sh relies on.
215
+ node_bin="$(command -v bun)"
102
216
  fi
103
- if [ "$npm_bin" = "npm" ] && command -v npm >/dev/null 2>&1; then
104
- npm_bin="$(command -v npm)"
217
+
218
+ local npm_bin="$pkg_manager"
219
+ if command -v "$pkg_manager" >/dev/null 2>&1; then
220
+ npm_bin="$(command -v "$pkg_manager")"
105
221
  fi
106
222
 
107
223
  append_env "HARNESS_ECOSYSTEM" "node"
108
224
  append_env "NODE_BIN" "$node_bin"
109
225
  append_env "NPM_BIN" "$npm_bin"
226
+ append_env "HARNESS_NODE_PACKAGE_MANAGER" "$pkg_manager"
227
+ append_env "HARNESS_PKG_EXEC" "$(har_pkg_exec "$pkg_manager")"
110
228
  append_path_prefix "$dir/node_modules/.bin"
111
229
  }
112
230
 
@@ -252,8 +370,11 @@ provision_monorepo_root() {
252
370
  [ -n "$HAR_WORKTREE_DIR" ] || return 0
253
371
  [ -f "$HAR_WORKTREE_DIR/package.json" ] || return 0
254
372
  [ -d "$HAR_WORKTREE_DIR/node_modules" ] && return 0
255
- pt_log "Installing monorepo root dependencies in $HAR_WORKTREE_DIR..."
256
- (cd "$HAR_WORKTREE_DIR" && npm install --silent)
373
+ local pkg_manager
374
+ pkg_manager="$(har_node_package_manager "$HAR_WORKTREE_DIR")"
375
+ pt_log "Installing monorepo root dependencies in $HAR_WORKTREE_DIR with ${pkg_manager}..."
376
+ har_node_install "$HAR_WORKTREE_DIR" "$pkg_manager" \
377
+ "$(har_node_declared_package_manager "$HAR_WORKTREE_DIR")"
257
378
  }
258
379
 
259
380
  provision_ecosystem() {
@@ -165,7 +165,7 @@ fi
165
165
  SHARED_ECOSYSTEM="$SCRIPT_DIR/ecosystem.shared.config.cjs"
166
166
  if [ -f "$SHARED_ECOSYSTEM" ]; then
167
167
  log "Starting shared app services from ecosystem.shared.config.cjs..."
168
- (cd "$REPO_ROOT" && npx --yes pm2 startOrReload "$SHARED_ECOSYSTEM" >/dev/null)
168
+ (cd "$REPO_ROOT" && $(har_pkg_exec) pm2 startOrReload "$SHARED_ECOSYSTEM" >/dev/null)
169
169
  log "Shared app services running (pm2 ls | grep har-${HARNESS_PROJECT_NAME}-shared-)."
170
170
  fi
171
171
 
@@ -10,6 +10,10 @@ export HARNESS_AGENT_SLOT_MAX=3
10
10
  # Toolchain provisioning — launch writes Xcode/simulator paths into .env.agent.<id>
11
11
  export HARNESS_ECOSYSTEM="ios"
12
12
  # export HARNESS_INSTALL_CMD="xcodebuild -resolvePackageDependencies ..."
13
+ # Node package manager: auto-detected from package.json "packageManager" and the
14
+ # lockfile (bun.lock → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm),
15
+ # falling back to whichever is installed. Set to pin one: npm | bun | pnpm | yarn
16
+ # export HARNESS_NODE_PACKAGE_MANAGER="bun"
13
17
 
14
18
  # ── Xcode project ─────────────────────────────────────────────────────────────
15
19
  # Set one of HARNESS_XCODE_WORKSPACE or HARNESS_XCODE_PROJECT (workspace wins).
@@ -70,6 +74,119 @@ har_infra_enabled() {
70
74
  esac
71
75
  }
72
76
 
77
+ # ── Node package manager helpers ──────────────────────────────────────────────
78
+ # Mirrored verbatim in harness.env and provision-toolchain.sh so a sourcing
79
+ # script and the provisioning subprocess resolve the same tool.
80
+
81
+ # Package managers HAR can drive, in fallback preference order.
82
+ HAR_NODE_PACKAGE_MANAGERS="npm bun pnpm yarn"
83
+
84
+ # Package manager the repo declares: an explicit HARNESS_NODE_PACKAGE_MANAGER
85
+ # wins, then package.json "packageManager", then the lockfile. Empty when the
86
+ # repo says nothing.
87
+ har_node_declared_package_manager() {
88
+ local dir="${1:-.}"
89
+
90
+ if [ -n "${HARNESS_NODE_PACKAGE_MANAGER:-}" ]; then
91
+ echo "$HARNESS_NODE_PACKAGE_MANAGER"
92
+ return
93
+ fi
94
+ if [ -f "$dir/package.json" ]; then
95
+ local field
96
+ field="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' "$dir/package.json" | head -1)"
97
+ if [ -n "$field" ]; then
98
+ echo "$field"
99
+ return
100
+ fi
101
+ fi
102
+ if [ -f "$dir/bun.lock" ] || [ -f "$dir/bun.lockb" ]; then echo bun; return; fi
103
+ if [ -f "$dir/pnpm-lock.yaml" ]; then echo pnpm; return; fi
104
+ if [ -f "$dir/yarn.lock" ]; then echo yarn; return; fi
105
+ if [ -f "$dir/package-lock.json" ]; then echo npm; return; fi
106
+ echo ""
107
+ }
108
+
109
+ # Package manager to actually run. A manager the repo declares but this machine
110
+ # lacks falls back to one that is installed, so a repo pinned to npm still
111
+ # provisions on a bun-only machine (and the reverse).
112
+ har_node_package_manager() {
113
+ local dir="${1:-.}"
114
+ local declared
115
+ declared="$(har_node_declared_package_manager "$dir")"
116
+
117
+ if [ -n "$declared" ] && command -v "$declared" >/dev/null 2>&1; then
118
+ echo "$declared"
119
+ return
120
+ fi
121
+
122
+ local candidate
123
+ for candidate in $HAR_NODE_PACKAGE_MANAGERS; do
124
+ if command -v "$candidate" >/dev/null 2>&1; then
125
+ echo "$candidate"
126
+ return
127
+ fi
128
+ done
129
+
130
+ echo "${declared:-npm}"
131
+ }
132
+
133
+ # Lockfile a manager writes, so a substitute install can clean up after itself.
134
+ har_node_lockfile() {
135
+ case "$1" in
136
+ bun) echo "bun.lock" ;;
137
+ npm) echo "package-lock.json" ;;
138
+ pnpm) echo "pnpm-lock.yaml" ;;
139
+ yarn) echo "yarn.lock" ;;
140
+ *) echo "" ;;
141
+ esac
142
+ }
143
+
144
+ # Install dependencies in a directory. When a substitute manager stands in for
145
+ # the one the repo declares, any lockfile it creates is removed afterwards:
146
+ # provisioning must not migrate the repo to a different package manager.
147
+ # (bun writes bun.lock even under --no-save, so the flags alone are not enough.)
148
+ har_node_install() {
149
+ local dir="$1"
150
+ local manager="$2"
151
+ local declared="${3:-}"
152
+ local substituting=false
153
+ local lockfile=""
154
+ local had_lockfile=false
155
+
156
+ if [ -n "$declared" ] && [ "$declared" != "$manager" ]; then
157
+ substituting=true
158
+ lockfile="$(har_node_lockfile "$manager")"
159
+ if [ -n "$lockfile" ] && [ -e "$dir/$lockfile" ]; then
160
+ had_lockfile=true
161
+ fi
162
+ fi
163
+
164
+ local code=0
165
+ (cd "$dir" && "$manager" install --silent) || code=$?
166
+
167
+ if [ "$substituting" = true ] && [ "$had_lockfile" = false ] && [ -n "$lockfile" ]; then
168
+ rm -f "$dir/$lockfile"
169
+ [ "$manager" = "bun" ] && rm -f "$dir/bun.lockb"
170
+ fi
171
+ return $code
172
+ }
173
+
174
+ # Package runner (npx equivalent) for one-off CLIs such as pm2, including the
175
+ # flag that keeps it non-interactive. Takes a manager name, or none to resolve
176
+ # from the environment and PATH.
177
+ har_pkg_exec() {
178
+ case "${1:-${HARNESS_NODE_PACKAGE_MANAGER:-}}" in
179
+ bun) echo "bunx"; return ;;
180
+ pnpm) echo "pnpm dlx"; return ;;
181
+ yarn) echo "yarn dlx"; return ;;
182
+ npm) echo "npx --yes"; return ;;
183
+ esac
184
+ if command -v npx >/dev/null 2>&1; then echo "npx --yes"; return; fi
185
+ if command -v bunx >/dev/null 2>&1; then echo "bunx"; return; fi
186
+ if command -v pnpm >/dev/null 2>&1; then echo "pnpm dlx"; return; fi
187
+ echo "npx --yes"
188
+ }
189
+
73
190
  # Optional "agent usable" smoke beyond build/test, such as a simulator flow or
74
191
  # backend-auth check. Enable only when this project needs it.
75
192
  # export HARNESS_READINESS_CMD="./.har/readiness.sh"