@osfactory/har 0.61.2 → 0.62.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.
@@ -228,52 +228,291 @@ provision_node() {
228
228
  append_path_prefix "$dir/node_modules/.bin"
229
229
  }
230
230
 
231
+ # ── Python interpreter helpers ────────────────────────────────────────────────
232
+ # Resolve the project Python (requires-python, .python-version, uv) instead of
233
+ # blindly using PATH python3.
234
+
235
+ har_python_version_from_bin() {
236
+ local bin="$1"
237
+ "$bin" -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' 2>/dev/null \
238
+ | tr -d '[:space:]'
239
+ }
240
+
241
+ har_python_version_to_sortable() {
242
+ local v="${1#python}"
243
+ v="${v#v}"
244
+ local major=0 minor=0 patch=0
245
+ IFS='.' read -r major minor patch _ <<< "$v"
246
+ major=${major:-0}
247
+ minor=${minor:-0}
248
+ patch=${patch:-0}
249
+ printf '%03d%03d%03d' "$major" "$minor" "$patch"
250
+ }
251
+
252
+ har_python_version_ge() {
253
+ local a b
254
+ a="$(har_python_version_to_sortable "$1")"
255
+ b="$(har_python_version_to_sortable "$2")"
256
+ [ "$a" -ge "$b" ]
257
+ }
258
+
259
+ har_python_requires_minimum() {
260
+ local spec="$1"
261
+ local ver=""
262
+
263
+ [ -n "$spec" ] || return 1
264
+
265
+ ver="$(printf '%s' "$spec" | sed -n \
266
+ -e 's/.*>=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
267
+ -e 's/.*~=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
268
+ -e 's/.*==\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
269
+ | head -1)"
270
+
271
+ if [ -z "$ver" ]; then
272
+ ver="$(printf '%s' "$spec" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p')"
273
+ fi
274
+
275
+ [ -n "$ver" ] && echo "$ver"
276
+ }
277
+
278
+ har_python_read_dot_version() {
279
+ local dir="$1"
280
+ [ -f "$dir/.python-version" ] || return 1
281
+ head -1 "$dir/.python-version" | tr -d '[:space:]'
282
+ }
283
+
284
+ har_python_read_requires_spec() {
285
+ local dir="$1"
286
+ local spec=""
287
+
288
+ if [ -f "$dir/pyproject.toml" ]; then
289
+ spec="$(sed -n 's/^[[:space:]]*requires-python[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
290
+ "$dir/pyproject.toml" | head -1)"
291
+ fi
292
+ if [ -z "$spec" ] && [ -f "$dir/setup.cfg" ]; then
293
+ spec="$(sed -n '/^\[options\]/,/^\[/ s/^python_requires[[:space:]]*=[[:space:]]*\(.*\)/\1/p' \
294
+ "$dir/setup.cfg" | head -1 | tr -d ' "')"
295
+ fi
296
+ [ -n "$spec" ] && echo "$spec"
297
+ }
298
+
299
+ har_python_project_minimum() {
300
+ local dir="$1"
301
+ local req_min="" dot_ver="" higher=""
302
+
303
+ req_min="$(har_python_requires_minimum "$(har_python_read_requires_spec "$dir" || true)" || true)"
304
+ dot_ver="$(har_python_read_dot_version "$dir" || true)"
305
+ if [ -n "$dot_ver" ]; then
306
+ dot_ver="$(printf '%s' "$dot_ver" | sed 's/^v//; s/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/')"
307
+ fi
308
+
309
+ if [ -n "$req_min" ] && [ -n "$dot_ver" ]; then
310
+ if har_python_version_ge "$dot_ver" "$req_min"; then
311
+ higher="$dot_ver"
312
+ else
313
+ higher="$req_min"
314
+ fi
315
+ elif [ -n "$req_min" ]; then
316
+ higher="$req_min"
317
+ elif [ -n "$dot_ver" ]; then
318
+ higher="$dot_ver"
319
+ fi
320
+
321
+ [ -n "$higher" ] && echo "$higher"
322
+ }
323
+
324
+ har_python_is_uv_project() {
325
+ local dir="$1"
326
+ [ -f "$dir/uv.lock" ] && return 0
327
+ [ -f "$dir/pyproject.toml" ] && grep -q '^\[tool\.uv\]' "$dir/pyproject.toml" 2>/dev/null
328
+ }
329
+
330
+ har_python_uv_find() {
331
+ local dir="$1"
332
+ local request="${2:-}"
333
+
334
+ command -v uv >/dev/null 2>&1 || return 1
335
+ if [ -n "$request" ]; then
336
+ (cd "$dir" && uv python find "$request" 2>/dev/null)
337
+ else
338
+ (cd "$dir" && uv python find 2>/dev/null)
339
+ fi
340
+ }
341
+
342
+ har_python_resolve_interpreter() {
343
+ local dir="$1"
344
+ local minimum="${2:-}"
345
+ local found=""
346
+
347
+ if har_python_is_uv_project "$dir"; then
348
+ found="$(har_python_uv_find "$dir" "$minimum" || true)"
349
+ if [ -n "$found" ] && [ -x "$found" ]; then
350
+ echo "$found"
351
+ return 0
352
+ fi
353
+ fi
354
+
355
+ if [ -f "$dir/.python-version" ]; then
356
+ found="$(har_python_uv_find "$dir" "$(har_python_read_dot_version "$dir")" || true)"
357
+ if [ -n "$found" ] && [ -x "$found" ]; then
358
+ echo "$found"
359
+ return 0
360
+ fi
361
+ if command -v pyenv >/dev/null 2>&1; then
362
+ found="$(cd "$dir" && pyenv which python 2>/dev/null || true)"
363
+ if [ -n "$found" ] && [ -x "$found" ]; then
364
+ echo "$found"
365
+ return 0
366
+ fi
367
+ fi
368
+ fi
369
+
370
+ if [ -n "$minimum" ]; then
371
+ found="$(har_python_uv_find "$dir" "$minimum" || true)"
372
+ if [ -n "$found" ] && [ -x "$found" ]; then
373
+ echo "$found"
374
+ return 0
375
+ fi
376
+ fi
377
+
378
+ if command -v python3 >/dev/null 2>&1; then
379
+ local sys_py sys_ver=""
380
+ sys_py="$(command -v python3)"
381
+ if [ -z "$minimum" ]; then
382
+ echo "$sys_py"
383
+ return 0
384
+ fi
385
+ sys_ver="$(har_python_version_from_bin "$sys_py")"
386
+ if [ -n "$sys_ver" ] && har_python_version_ge "$sys_ver" "$minimum"; then
387
+ echo "$sys_py"
388
+ return 0
389
+ fi
390
+ echo "$sys_py"
391
+ return 0
392
+ fi
393
+
394
+ return 1
395
+ }
396
+
397
+ har_python_warn_if_below_minimum() {
398
+ local bin="$1"
399
+ local minimum="$2"
400
+ local actual=""
401
+
402
+ [ -n "$minimum" ] || return 0
403
+ actual="$(har_python_version_from_bin "$bin")"
404
+ [ -n "$actual" ] || return 0
405
+ if ! har_python_version_ge "$actual" "$minimum"; then
406
+ pt_log "WARNING: resolved Python ${actual} is older than required ${minimum} (requires-python / .python-version)"
407
+ fi
408
+ }
409
+
410
+ har_python_create_venv() {
411
+ local dir="$1"
412
+ local venv_dir="$2"
413
+ local interpreter="$3"
414
+ local venv_rel="${venv_dir#"$dir"/}"
415
+
416
+ if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
417
+ pt_log "Creating Python venv with uv at ${venv_rel}..."
418
+ if (cd "$dir" && uv venv --python "$interpreter" "$venv_dir" 2>/dev/null) \
419
+ || (cd "$dir" && uv venv "$venv_dir" 2>/dev/null); then
420
+ return 0
421
+ fi
422
+ pt_log "uv venv failed — falling back to ${interpreter} -m venv"
423
+ fi
424
+
425
+ pt_log "Creating Python venv at ${venv_rel}..."
426
+ "$interpreter" -m venv "$venv_dir"
427
+ }
428
+
429
+ har_python_install_deps() {
430
+ local dir="$1"
431
+ local venv_dir="$2"
432
+
433
+ if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
434
+ pt_log "Syncing Python dependencies with uv..."
435
+ if (cd "$dir" && UV_PROJECT_ENVIRONMENT="$venv_dir" uv sync --quiet 2>/dev/null); then
436
+ return 0
437
+ fi
438
+ if (cd "$dir" && uv sync --quiet 2>/dev/null); then
439
+ return 0
440
+ fi
441
+ pt_log "uv sync failed — falling back to pip"
442
+ return 1
443
+ fi
444
+ return 1
445
+ }
446
+
231
447
  provision_python() {
232
448
  local dir="$1"
233
449
  local venv_rel="${HARNESS_PYTHON_VENV_DIR:-.har/venv}"
234
450
  local venv_dir="$dir/$venv_rel"
235
- local python_bin="python3"
451
+ local python_bin=""
452
+ local interpreter=""
453
+ local project_min=""
454
+
455
+ project_min="$(har_python_project_minimum "$dir" || true)"
456
+ interpreter="$(har_python_resolve_interpreter "$dir" "$project_min" || true)"
236
457
 
237
- if ! command -v python3 >/dev/null 2>&1; then
238
- pt_log "python3 not found on PATH — skipping venv provisioning"
458
+ if [ -z "$interpreter" ] || ! command -v "$interpreter" >/dev/null 2>&1; then
459
+ pt_log "No suitable Python interpreter found — skipping venv provisioning"
239
460
  append_env "HARNESS_ECOSYSTEM" "python"
240
461
  append_env "PYTHON_BIN" "${PYTHON_BIN:-python3}"
241
462
  return
242
463
  fi
243
464
 
465
+ if [ -n "$project_min" ]; then
466
+ har_python_warn_if_below_minimum "$interpreter" "$project_min"
467
+ fi
468
+
469
+ if [ -d "$venv_dir" ] && [ -x "$venv_dir/bin/python" ] && [ -n "$project_min" ]; then
470
+ local venv_ver=""
471
+ venv_ver="$(har_python_version_from_bin "$venv_dir/bin/python")"
472
+ if [ -n "$venv_ver" ] && ! har_python_version_ge "$venv_ver" "$project_min"; then
473
+ pt_log "Existing venv Python ${venv_ver} is older than required ${project_min} — recreating..."
474
+ rm -rf "$venv_dir"
475
+ fi
476
+ fi
477
+
244
478
  if [ ! -d "$venv_dir" ]; then
245
- pt_log "Creating Python venv at $venv_rel..."
246
- if ! python3 -m venv "$venv_dir"; then
479
+ if ! har_python_create_venv "$dir" "$venv_dir" "$interpreter"; then
247
480
  rm -rf "$venv_dir"
248
- pt_log "python3 -m venv failed (on Debian/Ubuntu install python3-venv) — using system python3"
481
+ pt_log "venv creation failed (on Debian/Ubuntu install python3-venv) — using resolved interpreter"
249
482
  append_env "HARNESS_ECOSYSTEM" "python"
250
- append_env "PYTHON_BIN" "$(command -v python3)"
483
+ append_env "PYTHON_BIN" "$interpreter"
251
484
  return
252
485
  fi
253
486
  fi
254
487
 
255
488
  if [ ! -x "$venv_dir/bin/python" ]; then
256
- pt_log "Python venv at $venv_rel is missing or broken — using system python3"
489
+ pt_log "Python venv at $venv_rel is missing or broken — using resolved interpreter"
257
490
  append_env "HARNESS_ECOSYSTEM" "python"
258
- append_env "PYTHON_BIN" "$(command -v python3)"
491
+ append_env "PYTHON_BIN" "$interpreter"
259
492
  return
260
493
  fi
261
494
 
262
495
  python_bin="$venv_dir/bin/python"
496
+ if [ -n "$project_min" ]; then
497
+ har_python_warn_if_below_minimum "$python_bin" "$project_min"
498
+ fi
499
+
263
500
  # shellcheck disable=SC1091
264
501
  source "$venv_dir/bin/activate"
265
502
 
266
503
  if ! run_install_cmd "$dir"; then
267
- pt_log "Installing Python dependencies..."
268
- if [ -f "$dir/pyproject.toml" ]; then
269
- (cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
270
- elif [ -f "$dir/requirements.txt" ]; then
271
- (cd "$dir" && pip install -q -r requirements.txt)
272
- elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
273
- (cd "$dir" && pip install -q -e .)
274
- elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
275
- (cd "$dir" && pipenv install --dev)
276
- python_bin="$(cd "$dir" && pipenv --py)"
504
+ if ! har_python_install_deps "$dir" "$venv_dir"; then
505
+ pt_log "Installing Python dependencies..."
506
+ if [ -f "$dir/pyproject.toml" ]; then
507
+ (cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
508
+ elif [ -f "$dir/requirements.txt" ]; then
509
+ (cd "$dir" && pip install -q -r requirements.txt)
510
+ elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
511
+ (cd "$dir" && pip install -q -e .)
512
+ elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
513
+ (cd "$dir" && pipenv install --dev)
514
+ python_bin="$(cd "$dir" && pipenv --py)"
515
+ fi
277
516
  fi
278
517
  fi
279
518
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Verification pipeline for iOS mobile app repos — build and test with xcodebuild.
3
- # Outputs JSON to stdout, human-readable progress to stderr.
3
+ # Outputs JSON to stdout (machine contract), human-readable progress to stderr.
4
+ # Passing steps omit `output`. `har env verify` streams progress only.
4
5
  #
5
6
  # Usage: ./.har/verify.sh <agent-id> [--full]
6
7
  #
@@ -92,7 +93,7 @@ run_step() {
92
93
  end=$(now_ms)
93
94
  elapsed=$(( end - start ))
94
95
 
95
- local pass_bool step_output_escaped
96
+ local pass_bool
96
97
  if [ "$exit_code" = "0" ]; then
97
98
  echo "✓ (${elapsed}ms)" >&2
98
99
  pass_bool="true"
@@ -103,14 +104,7 @@ run_step() {
103
104
  OVERALL_PASS=false
104
105
  fi
105
106
 
106
- step_output_escaped=$(escape_step_output "$output")
107
-
108
- RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
109
- const fs = require('fs');
110
- let arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
111
- arr.push({name:'$name',pass:$pass_bool,ms:$elapsed,output:$step_output_escaped});
112
- process.stdout.write(JSON.stringify(arr));
113
- " 2>/dev/null || echo "$RESULTS_JSON")
107
+ record_step_result "$name" "$pass_bool" "$elapsed" "$output"
114
108
 
115
109
  if [ "$pass_bool" = "false" ] && [ -z "$FULL" ]; then
116
110
  return 1
@@ -228,52 +228,291 @@ provision_node() {
228
228
  append_path_prefix "$dir/node_modules/.bin"
229
229
  }
230
230
 
231
+ # ── Python interpreter helpers ────────────────────────────────────────────────
232
+ # Resolve the project Python (requires-python, .python-version, uv) instead of
233
+ # blindly using PATH python3.
234
+
235
+ har_python_version_from_bin() {
236
+ local bin="$1"
237
+ "$bin" -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' 2>/dev/null \
238
+ | tr -d '[:space:]'
239
+ }
240
+
241
+ har_python_version_to_sortable() {
242
+ local v="${1#python}"
243
+ v="${v#v}"
244
+ local major=0 minor=0 patch=0
245
+ IFS='.' read -r major minor patch _ <<< "$v"
246
+ major=${major:-0}
247
+ minor=${minor:-0}
248
+ patch=${patch:-0}
249
+ printf '%03d%03d%03d' "$major" "$minor" "$patch"
250
+ }
251
+
252
+ har_python_version_ge() {
253
+ local a b
254
+ a="$(har_python_version_to_sortable "$1")"
255
+ b="$(har_python_version_to_sortable "$2")"
256
+ [ "$a" -ge "$b" ]
257
+ }
258
+
259
+ har_python_requires_minimum() {
260
+ local spec="$1"
261
+ local ver=""
262
+
263
+ [ -n "$spec" ] || return 1
264
+
265
+ ver="$(printf '%s' "$spec" | sed -n \
266
+ -e 's/.*>=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
267
+ -e 's/.*~=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
268
+ -e 's/.*==\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
269
+ | head -1)"
270
+
271
+ if [ -z "$ver" ]; then
272
+ ver="$(printf '%s' "$spec" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p')"
273
+ fi
274
+
275
+ [ -n "$ver" ] && echo "$ver"
276
+ }
277
+
278
+ har_python_read_dot_version() {
279
+ local dir="$1"
280
+ [ -f "$dir/.python-version" ] || return 1
281
+ head -1 "$dir/.python-version" | tr -d '[:space:]'
282
+ }
283
+
284
+ har_python_read_requires_spec() {
285
+ local dir="$1"
286
+ local spec=""
287
+
288
+ if [ -f "$dir/pyproject.toml" ]; then
289
+ spec="$(sed -n 's/^[[:space:]]*requires-python[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
290
+ "$dir/pyproject.toml" | head -1)"
291
+ fi
292
+ if [ -z "$spec" ] && [ -f "$dir/setup.cfg" ]; then
293
+ spec="$(sed -n '/^\[options\]/,/^\[/ s/^python_requires[[:space:]]*=[[:space:]]*\(.*\)/\1/p' \
294
+ "$dir/setup.cfg" | head -1 | tr -d ' "')"
295
+ fi
296
+ [ -n "$spec" ] && echo "$spec"
297
+ }
298
+
299
+ har_python_project_minimum() {
300
+ local dir="$1"
301
+ local req_min="" dot_ver="" higher=""
302
+
303
+ req_min="$(har_python_requires_minimum "$(har_python_read_requires_spec "$dir" || true)" || true)"
304
+ dot_ver="$(har_python_read_dot_version "$dir" || true)"
305
+ if [ -n "$dot_ver" ]; then
306
+ dot_ver="$(printf '%s' "$dot_ver" | sed 's/^v//; s/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/')"
307
+ fi
308
+
309
+ if [ -n "$req_min" ] && [ -n "$dot_ver" ]; then
310
+ if har_python_version_ge "$dot_ver" "$req_min"; then
311
+ higher="$dot_ver"
312
+ else
313
+ higher="$req_min"
314
+ fi
315
+ elif [ -n "$req_min" ]; then
316
+ higher="$req_min"
317
+ elif [ -n "$dot_ver" ]; then
318
+ higher="$dot_ver"
319
+ fi
320
+
321
+ [ -n "$higher" ] && echo "$higher"
322
+ }
323
+
324
+ har_python_is_uv_project() {
325
+ local dir="$1"
326
+ [ -f "$dir/uv.lock" ] && return 0
327
+ [ -f "$dir/pyproject.toml" ] && grep -q '^\[tool\.uv\]' "$dir/pyproject.toml" 2>/dev/null
328
+ }
329
+
330
+ har_python_uv_find() {
331
+ local dir="$1"
332
+ local request="${2:-}"
333
+
334
+ command -v uv >/dev/null 2>&1 || return 1
335
+ if [ -n "$request" ]; then
336
+ (cd "$dir" && uv python find "$request" 2>/dev/null)
337
+ else
338
+ (cd "$dir" && uv python find 2>/dev/null)
339
+ fi
340
+ }
341
+
342
+ har_python_resolve_interpreter() {
343
+ local dir="$1"
344
+ local minimum="${2:-}"
345
+ local found=""
346
+
347
+ if har_python_is_uv_project "$dir"; then
348
+ found="$(har_python_uv_find "$dir" "$minimum" || true)"
349
+ if [ -n "$found" ] && [ -x "$found" ]; then
350
+ echo "$found"
351
+ return 0
352
+ fi
353
+ fi
354
+
355
+ if [ -f "$dir/.python-version" ]; then
356
+ found="$(har_python_uv_find "$dir" "$(har_python_read_dot_version "$dir")" || true)"
357
+ if [ -n "$found" ] && [ -x "$found" ]; then
358
+ echo "$found"
359
+ return 0
360
+ fi
361
+ if command -v pyenv >/dev/null 2>&1; then
362
+ found="$(cd "$dir" && pyenv which python 2>/dev/null || true)"
363
+ if [ -n "$found" ] && [ -x "$found" ]; then
364
+ echo "$found"
365
+ return 0
366
+ fi
367
+ fi
368
+ fi
369
+
370
+ if [ -n "$minimum" ]; then
371
+ found="$(har_python_uv_find "$dir" "$minimum" || true)"
372
+ if [ -n "$found" ] && [ -x "$found" ]; then
373
+ echo "$found"
374
+ return 0
375
+ fi
376
+ fi
377
+
378
+ if command -v python3 >/dev/null 2>&1; then
379
+ local sys_py sys_ver=""
380
+ sys_py="$(command -v python3)"
381
+ if [ -z "$minimum" ]; then
382
+ echo "$sys_py"
383
+ return 0
384
+ fi
385
+ sys_ver="$(har_python_version_from_bin "$sys_py")"
386
+ if [ -n "$sys_ver" ] && har_python_version_ge "$sys_ver" "$minimum"; then
387
+ echo "$sys_py"
388
+ return 0
389
+ fi
390
+ echo "$sys_py"
391
+ return 0
392
+ fi
393
+
394
+ return 1
395
+ }
396
+
397
+ har_python_warn_if_below_minimum() {
398
+ local bin="$1"
399
+ local minimum="$2"
400
+ local actual=""
401
+
402
+ [ -n "$minimum" ] || return 0
403
+ actual="$(har_python_version_from_bin "$bin")"
404
+ [ -n "$actual" ] || return 0
405
+ if ! har_python_version_ge "$actual" "$minimum"; then
406
+ pt_log "WARNING: resolved Python ${actual} is older than required ${minimum} (requires-python / .python-version)"
407
+ fi
408
+ }
409
+
410
+ har_python_create_venv() {
411
+ local dir="$1"
412
+ local venv_dir="$2"
413
+ local interpreter="$3"
414
+ local venv_rel="${venv_dir#"$dir"/}"
415
+
416
+ if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
417
+ pt_log "Creating Python venv with uv at ${venv_rel}..."
418
+ if (cd "$dir" && uv venv --python "$interpreter" "$venv_dir" 2>/dev/null) \
419
+ || (cd "$dir" && uv venv "$venv_dir" 2>/dev/null); then
420
+ return 0
421
+ fi
422
+ pt_log "uv venv failed — falling back to ${interpreter} -m venv"
423
+ fi
424
+
425
+ pt_log "Creating Python venv at ${venv_rel}..."
426
+ "$interpreter" -m venv "$venv_dir"
427
+ }
428
+
429
+ har_python_install_deps() {
430
+ local dir="$1"
431
+ local venv_dir="$2"
432
+
433
+ if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
434
+ pt_log "Syncing Python dependencies with uv..."
435
+ if (cd "$dir" && UV_PROJECT_ENVIRONMENT="$venv_dir" uv sync --quiet 2>/dev/null); then
436
+ return 0
437
+ fi
438
+ if (cd "$dir" && uv sync --quiet 2>/dev/null); then
439
+ return 0
440
+ fi
441
+ pt_log "uv sync failed — falling back to pip"
442
+ return 1
443
+ fi
444
+ return 1
445
+ }
446
+
231
447
  provision_python() {
232
448
  local dir="$1"
233
449
  local venv_rel="${HARNESS_PYTHON_VENV_DIR:-.har/venv}"
234
450
  local venv_dir="$dir/$venv_rel"
235
- local python_bin="python3"
451
+ local python_bin=""
452
+ local interpreter=""
453
+ local project_min=""
454
+
455
+ project_min="$(har_python_project_minimum "$dir" || true)"
456
+ interpreter="$(har_python_resolve_interpreter "$dir" "$project_min" || true)"
236
457
 
237
- if ! command -v python3 >/dev/null 2>&1; then
238
- pt_log "python3 not found on PATH — skipping venv provisioning"
458
+ if [ -z "$interpreter" ] || ! command -v "$interpreter" >/dev/null 2>&1; then
459
+ pt_log "No suitable Python interpreter found — skipping venv provisioning"
239
460
  append_env "HARNESS_ECOSYSTEM" "python"
240
461
  append_env "PYTHON_BIN" "${PYTHON_BIN:-python3}"
241
462
  return
242
463
  fi
243
464
 
465
+ if [ -n "$project_min" ]; then
466
+ har_python_warn_if_below_minimum "$interpreter" "$project_min"
467
+ fi
468
+
469
+ if [ -d "$venv_dir" ] && [ -x "$venv_dir/bin/python" ] && [ -n "$project_min" ]; then
470
+ local venv_ver=""
471
+ venv_ver="$(har_python_version_from_bin "$venv_dir/bin/python")"
472
+ if [ -n "$venv_ver" ] && ! har_python_version_ge "$venv_ver" "$project_min"; then
473
+ pt_log "Existing venv Python ${venv_ver} is older than required ${project_min} — recreating..."
474
+ rm -rf "$venv_dir"
475
+ fi
476
+ fi
477
+
244
478
  if [ ! -d "$venv_dir" ]; then
245
- pt_log "Creating Python venv at $venv_rel..."
246
- if ! python3 -m venv "$venv_dir"; then
479
+ if ! har_python_create_venv "$dir" "$venv_dir" "$interpreter"; then
247
480
  rm -rf "$venv_dir"
248
- pt_log "python3 -m venv failed (on Debian/Ubuntu install python3-venv) — using system python3"
481
+ pt_log "venv creation failed (on Debian/Ubuntu install python3-venv) — using resolved interpreter"
249
482
  append_env "HARNESS_ECOSYSTEM" "python"
250
- append_env "PYTHON_BIN" "$(command -v python3)"
483
+ append_env "PYTHON_BIN" "$interpreter"
251
484
  return
252
485
  fi
253
486
  fi
254
487
 
255
488
  if [ ! -x "$venv_dir/bin/python" ]; then
256
- pt_log "Python venv at $venv_rel is missing or broken — using system python3"
489
+ pt_log "Python venv at $venv_rel is missing or broken — using resolved interpreter"
257
490
  append_env "HARNESS_ECOSYSTEM" "python"
258
- append_env "PYTHON_BIN" "$(command -v python3)"
491
+ append_env "PYTHON_BIN" "$interpreter"
259
492
  return
260
493
  fi
261
494
 
262
495
  python_bin="$venv_dir/bin/python"
496
+ if [ -n "$project_min" ]; then
497
+ har_python_warn_if_below_minimum "$python_bin" "$project_min"
498
+ fi
499
+
263
500
  # shellcheck disable=SC1091
264
501
  source "$venv_dir/bin/activate"
265
502
 
266
503
  if ! run_install_cmd "$dir"; then
267
- pt_log "Installing Python dependencies..."
268
- if [ -f "$dir/pyproject.toml" ]; then
269
- (cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
270
- elif [ -f "$dir/requirements.txt" ]; then
271
- (cd "$dir" && pip install -q -r requirements.txt)
272
- elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
273
- (cd "$dir" && pip install -q -e .)
274
- elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
275
- (cd "$dir" && pipenv install --dev)
276
- python_bin="$(cd "$dir" && pipenv --py)"
504
+ if ! har_python_install_deps "$dir" "$venv_dir"; then
505
+ pt_log "Installing Python dependencies..."
506
+ if [ -f "$dir/pyproject.toml" ]; then
507
+ (cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
508
+ elif [ -f "$dir/requirements.txt" ]; then
509
+ (cd "$dir" && pip install -q -r requirements.txt)
510
+ elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
511
+ (cd "$dir" && pip install -q -e .)
512
+ elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
513
+ (cd "$dir" && pipenv install --dev)
514
+ python_bin="$(cd "$dir" && pipenv --py)"
515
+ fi
277
516
  fi
278
517
  fi
279
518
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.61.2",
3
+ "version": "0.62.0",
4
4
  "description": "Open harness for reproducible multi-agent coding workflows",
5
5
  "keywords": [
6
6
  "har",