@osfactory/har 0.61.3 → 0.62.1

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/dist/index.js CHANGED
@@ -33,6 +33,31 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  mod
34
34
  ));
35
35
 
36
+ // src/core/package-version.ts
37
+ function getHarPackageVersion() {
38
+ if (process.env.HAR_PACKAGE_VERSION) {
39
+ return process.env.HAR_PACKAGE_VERSION;
40
+ }
41
+ const candidates = [
42
+ path.resolve(__dirname, "..", "package.json"),
43
+ path.resolve(__dirname, "..", "..", "package.json")
44
+ ];
45
+ for (const packageJsonPath of candidates) {
46
+ if (!fs.existsSync(packageJsonPath)) continue;
47
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
48
+ return pkg.version;
49
+ }
50
+ throw new Error("Could not resolve @osfactory/har package.json for version lookup");
51
+ }
52
+ var fs, path;
53
+ var init_package_version = __esm({
54
+ "src/core/package-version.ts"() {
55
+ "use strict";
56
+ fs = __toESM(require("fs"));
57
+ path = __toESM(require("path"));
58
+ }
59
+ });
60
+
36
61
  // src/core/telemetry-config.ts
37
62
  function getPreferencePath() {
38
63
  if (process.env.HAR_TELEMETRY_CONFIG_PATH) {
@@ -28752,7 +28777,7 @@ var init_control_config = __esm({
28752
28777
  "use strict";
28753
28778
  init_portal_credentials();
28754
28779
  DEFAULT_CONTROL_API_URL = "http://localhost:3847";
28755
- DEFAULT_PORTAL_URL = "https://har.kerno.io";
28780
+ DEFAULT_PORTAL_URL = "https://app.harhq.com";
28756
28781
  }
28757
28782
  });
28758
28783
 
@@ -32881,6 +32906,7 @@ async function buildPortalPayload(repoPath, controlApiUrl, since, full = false)
32881
32906
  }
32882
32907
  const syncBody = {
32883
32908
  path: repoPath,
32909
+ cliVersion: getHarPackageVersion(),
32884
32910
  ...status.gitRemote ? { gitRemote: status.gitRemote } : {},
32885
32911
  ...manifest ? { manifest } : {},
32886
32912
  ...stagesRegistry ? { stagesRegistry } : {},
@@ -33303,6 +33329,7 @@ var init_control_sync = __esm({
33303
33329
  init_work_units();
33304
33330
  init_cloud_executor();
33305
33331
  init_telemetry_config();
33332
+ init_package_version();
33306
33333
  init_logging();
33307
33334
  init_usage_harvest();
33308
33335
  init_telemetry_env();
@@ -66492,26 +66519,8 @@ function isYargsInstance(y) {
66492
66519
  var Yargs = YargsFactory(esm_default);
66493
66520
  var yargs_default = Yargs;
66494
66521
 
66495
- // src/core/package-version.ts
66496
- var fs = __toESM(require("fs"));
66497
- var path = __toESM(require("path"));
66498
- function getHarPackageVersion() {
66499
- if (process.env.HAR_PACKAGE_VERSION) {
66500
- return process.env.HAR_PACKAGE_VERSION;
66501
- }
66502
- const candidates = [
66503
- path.resolve(__dirname, "..", "package.json"),
66504
- path.resolve(__dirname, "..", "..", "package.json")
66505
- ];
66506
- for (const packageJsonPath of candidates) {
66507
- if (!fs.existsSync(packageJsonPath)) continue;
66508
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
66509
- return pkg.version;
66510
- }
66511
- throw new Error("Could not resolve @osfactory/har package.json for version lookup");
66512
- }
66513
-
66514
66522
  // src/cli/index.ts
66523
+ init_package_version();
66515
66524
  init_telemetry_config();
66516
66525
  init_sync_context();
66517
66526
 
@@ -72206,6 +72215,7 @@ var import_child_process13 = require("child_process");
72206
72215
  var path52 = __toESM(require("path"));
72207
72216
 
72208
72217
  // src/core/control-image.ts
72218
+ init_package_version();
72209
72219
  var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
72210
72220
  function getControlImageName() {
72211
72221
  return process.env.HAR_CONTROL_IMAGE ?? DEFAULT_CONTROL_IMAGE;
@@ -82504,6 +82514,7 @@ var StdioServerTransport = class {
82504
82514
  };
82505
82515
 
82506
82516
  // src/mcp/server.ts
82517
+ init_package_version();
82507
82518
  init_telemetry_config();
82508
82519
  init_runs();
82509
82520
  init_work_units();
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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.3",
3
+ "version": "0.62.1",
4
4
  "description": "Open harness for reproducible multi-agent coding workflows",
5
5
  "keywords": [
6
6
  "har",