@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.
@@ -758,6 +758,29 @@ escape_step_output() {
758
758
  printf '%s' "$1" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const s=d.trim().split('\n').slice(0,50).join('\n');process.stdout.write(JSON.stringify(s))})" 2>/dev/null || echo '""'
759
759
  }
760
760
 
761
+ # Append one verify.sh step to RESULTS_JSON (caller global).
762
+ # Passing steps omit output — it already appeared as ✓ on stderr.
763
+ record_step_result() {
764
+ local name="$1"
765
+ local pass_bool="$2"
766
+ local elapsed="$3"
767
+ local output="${4:-}"
768
+ local step_json
769
+ if [ "$pass_bool" = "true" ]; then
770
+ step_json=$(NAME="$name" MS="$elapsed" node -e 'process.stdout.write(JSON.stringify({name:process.env.NAME,pass:true,ms:Number(process.env.MS)}))' 2>/dev/null) || return 0
771
+ else
772
+ local escaped
773
+ escaped=$(escape_step_output "$output")
774
+ step_json=$(NAME="$name" MS="$elapsed" node -e "process.stdout.write(JSON.stringify({name:process.env.NAME,pass:false,ms:Number(process.env.MS),output:$escaped}))" 2>/dev/null) || return 0
775
+ fi
776
+ RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
777
+ const fs = require('fs');
778
+ const arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
779
+ arr.push($step_json);
780
+ process.stdout.write(JSON.stringify(arr));
781
+ " 2>/dev/null || echo "$RESULTS_JSON")
782
+ }
783
+
761
784
  # Emit "<id>\t<command>" for each registered stage listed in stages.json
762
785
  # verificationStages (used by verify --full). Ids without a matching registered
763
786
  # stage are inline verify.sh steps and skipped here; lifecycle stages
@@ -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 CLI/library repos.
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
  #
@@ -70,7 +71,7 @@ run_step() {
70
71
  end=$(now_ms)
71
72
  elapsed=$(( end - start ))
72
73
 
73
- local pass_bool step_output_escaped
74
+ local pass_bool
74
75
  if [ "$exit_code" = "0" ]; then
75
76
  echo "✓ (${elapsed}ms)" >&2
76
77
  pass_bool="true"
@@ -81,14 +82,7 @@ run_step() {
81
82
  OVERALL_PASS=false
82
83
  fi
83
84
 
84
- step_output_escaped=$(escape_step_output "$output")
85
-
86
- RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
87
- const fs = require('fs');
88
- let arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
89
- arr.push({name:'$name',pass:$pass_bool,ms:$elapsed,output:$step_output_escaped});
90
- process.stdout.write(JSON.stringify(arr));
91
- " 2>/dev/null || echo "$RESULTS_JSON")
85
+ record_step_result "$name" "$pass_bool" "$elapsed" "$output"
92
86
 
93
87
  if [ "$pass_bool" = "false" ] && [ -z "$FULL" ]; then
94
88
  return 1
@@ -405,6 +405,29 @@ escape_step_output() {
405
405
  printf '%s' "$1" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const s=d.trim().split('\n').slice(0,50).join('\n');process.stdout.write(JSON.stringify(s))})" 2>/dev/null || echo '""'
406
406
  }
407
407
 
408
+ # Append one verify.sh step to RESULTS_JSON (caller global).
409
+ # Passing steps omit output — it already appeared as ✓ on stderr.
410
+ record_step_result() {
411
+ local name="$1"
412
+ local pass_bool="$2"
413
+ local elapsed="$3"
414
+ local output="${4:-}"
415
+ local step_json
416
+ if [ "$pass_bool" = "true" ]; then
417
+ step_json=$(NAME="$name" MS="$elapsed" node -e 'process.stdout.write(JSON.stringify({name:process.env.NAME,pass:true,ms:Number(process.env.MS)}))' 2>/dev/null) || return 0
418
+ else
419
+ local escaped
420
+ escaped=$(escape_step_output "$output")
421
+ step_json=$(NAME="$name" MS="$elapsed" node -e "process.stdout.write(JSON.stringify({name:process.env.NAME,pass:false,ms:Number(process.env.MS),output:$escaped}))" 2>/dev/null) || return 0
422
+ fi
423
+ RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
424
+ const fs = require('fs');
425
+ const arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
426
+ arr.push($step_json);
427
+ process.stdout.write(JSON.stringify(arr));
428
+ " 2>/dev/null || echo "$RESULTS_JSON")
429
+ }
430
+
408
431
  # Emit "<id>\t<command>" for each registered stage listed in stages.json
409
432
  # verificationStages (used by verify --full). Ids without a matching registered
410
433
  # stage are inline verify.sh steps and skipped here; lifecycle stages