@osfactory/har 0.49.2 → 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.
package/dist/index.js CHANGED
@@ -29790,10 +29790,32 @@ var init_slot_launch_guard_occupied = __esm({
29790
29790
  }
29791
29791
  });
29792
29792
 
29793
+ // src/utils/package-runner.ts
29794
+ function commandExists(command2) {
29795
+ const result = (0, import_child_process6.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
29796
+ encoding: "utf8"
29797
+ });
29798
+ return result.status === 0 && Boolean(result.stdout?.trim());
29799
+ }
29800
+ function packageRunner() {
29801
+ if (cached !== void 0) return cached;
29802
+ const available = RUNNERS.find((runner) => commandExists(runner.split(" ")[0]));
29803
+ cached = available ?? RUNNERS[0];
29804
+ return cached;
29805
+ }
29806
+ var import_child_process6, RUNNERS, cached;
29807
+ var init_package_runner = __esm({
29808
+ "src/utils/package-runner.ts"() {
29809
+ "use strict";
29810
+ import_child_process6 = require("child_process");
29811
+ RUNNERS = ["npx --yes", "bunx", "pnpm dlx", "yarn dlx"];
29812
+ }
29813
+ });
29814
+
29793
29815
  // src/core/slot-preflight.ts
29794
29816
  function listPm2Processes() {
29795
29817
  try {
29796
- const raw = (0, import_child_process6.execSync)("npx --yes pm2 jlist", {
29818
+ const raw = (0, import_child_process7.execSync)(`${packageRunner()} pm2 jlist`, {
29797
29819
  encoding: "utf8",
29798
29820
  stdio: ["pipe", "pipe", "ignore"],
29799
29821
  timeout: 3e3
@@ -29874,7 +29896,7 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
29874
29896
  remediation: "Stop the other harness session (./.har/teardown.sh in that repo) or use a different agent slot.",
29875
29897
  details: { processes: foreign.processes }
29876
29898
  });
29877
- remediations.push("Inspect with: npx pm2 jlist | grep agent-" + agentId);
29899
+ remediations.push(`Inspect with: ${packageRunner()} pm2 jlist | grep agent-${agentId}`);
29878
29900
  }
29879
29901
  if (usesPm2 && pm2Procs) {
29880
29902
  const slotPrefix = `har-${projectName}-agent-${agentId}-`;
@@ -30008,7 +30030,7 @@ function formatPreflightReport(agentId, readiness) {
30008
30030
  }
30009
30031
  return lines2.join("\n");
30010
30032
  }
30011
- var path16, import_child_process6;
30033
+ var path16, import_child_process7;
30012
30034
  var init_slot_preflight = __esm({
30013
30035
  "src/core/slot-preflight.ts"() {
30014
30036
  "use strict";
@@ -30019,14 +30041,15 @@ var init_slot_preflight = __esm({
30019
30041
  init_slot_launch_guard_occupied();
30020
30042
  init_slot_ports();
30021
30043
  init_slot_registry();
30022
- import_child_process6 = require("child_process");
30044
+ import_child_process7 = require("child_process");
30045
+ init_package_runner();
30023
30046
  }
30024
30047
  });
30025
30048
 
30026
30049
  // src/core/slot-status.ts
30027
30050
  function readGitRemote(repoPath) {
30028
30051
  try {
30029
- const remote = (0, import_child_process7.execSync)("git remote get-url origin", {
30052
+ const remote = (0, import_child_process8.execSync)("git remote get-url origin", {
30030
30053
  cwd: repoPath,
30031
30054
  encoding: "utf8",
30032
30055
  stdio: ["pipe", "pipe", "ignore"]
@@ -30038,7 +30061,7 @@ function readGitRemote(repoPath) {
30038
30061
  }
30039
30062
  function runGit2(cwd, args) {
30040
30063
  try {
30041
- return (0, import_child_process7.execSync)(`git ${args}`, {
30064
+ return (0, import_child_process8.execSync)(`git ${args}`, {
30042
30065
  cwd,
30043
30066
  encoding: "utf8",
30044
30067
  stdio: ["pipe", "pipe", "ignore"]
@@ -30114,7 +30137,7 @@ function lastBuildPass(run2) {
30114
30137
  }
30115
30138
  function listPm2Processes2() {
30116
30139
  try {
30117
- const raw = (0, import_child_process7.execSync)("npx --yes pm2 jlist", {
30140
+ const raw = (0, import_child_process8.execSync)(`${packageRunner()} pm2 jlist`, {
30118
30141
  encoding: "utf8",
30119
30142
  stdio: ["pipe", "pipe", "ignore"],
30120
30143
  timeout: 3e3
@@ -30217,14 +30240,14 @@ function collectEnvironmentStatus(repoPath) {
30217
30240
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
30218
30241
  };
30219
30242
  }
30220
- var fs16, os6, path17, import_child_process7, BYPASS_WARNING_MS;
30243
+ var fs16, os6, path17, import_child_process8, BYPASS_WARNING_MS;
30221
30244
  var init_slot_status = __esm({
30222
30245
  "src/core/slot-status.ts"() {
30223
30246
  "use strict";
30224
30247
  fs16 = __toESM(require("fs"));
30225
30248
  os6 = __toESM(require("os"));
30226
30249
  path17 = __toESM(require("path"));
30227
- import_child_process7 = require("child_process");
30250
+ import_child_process8 = require("child_process");
30228
30251
  init_env();
30229
30252
  init_manifest();
30230
30253
  init_stages();
@@ -30232,6 +30255,7 @@ var init_slot_status = __esm({
30232
30255
  init_runs();
30233
30256
  init_slot_registry();
30234
30257
  init_slot_preflight();
30258
+ init_package_runner();
30235
30259
  BYPASS_WARNING_MS = 15 * 60 * 1e3;
30236
30260
  }
30237
30261
  });
@@ -53490,8 +53514,8 @@ exec "${otelHookCommand}" run \\
53490
53514
  }
53491
53515
  return wrapperPath;
53492
53516
  }
53493
- function commandExists2(command2) {
53494
- const result = (0, import_child_process10.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
53517
+ function commandExists3(command2) {
53518
+ const result = (0, import_child_process11.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
53495
53519
  encoding: "utf8"
53496
53520
  });
53497
53521
  return result.status === 0 && Boolean(result.stdout?.trim());
@@ -53501,25 +53525,37 @@ function resolveOtelHookCommand(hooksHome) {
53501
53525
  if (fs41.existsSync(local)) return local;
53502
53526
  return null;
53503
53527
  }
53528
+ function resolveHooksInstaller(hooksHome) {
53529
+ const candidates = [
53530
+ {
53531
+ command: "npm",
53532
+ args: ["install", "--prefix", hooksHome, "--no-fund", "--no-audit", OTEL_HOOKS_PACKAGE]
53533
+ },
53534
+ { command: "bun", args: ["add", "--cwd", hooksHome, OTEL_HOOKS_PACKAGE] },
53535
+ { command: "pnpm", args: ["add", "--dir", hooksHome, OTEL_HOOKS_PACKAGE] }
53536
+ ];
53537
+ return candidates.find((candidate) => commandExists3(candidate.command)) ?? null;
53538
+ }
53504
53539
  function tryInstallPackage(hooksHome) {
53505
53540
  fs41.mkdirSync(hooksHome, { recursive: true });
53506
- if (!commandExists2("npm")) {
53541
+ const installer = resolveHooksInstaller(hooksHome);
53542
+ if (!installer) {
53507
53543
  return {
53508
53544
  ok: false,
53509
- detail: "npm not found; install Node.js to use @osfactory/otel-hook"
53545
+ detail: "no package manager found; install Node.js or bun to use @osfactory/otel-hook"
53510
53546
  };
53511
53547
  }
53512
- const result = (0, import_child_process10.spawnSync)(
53513
- "npm",
53514
- ["install", "--prefix", hooksHome, "--no-fund", "--no-audit", OTEL_HOOKS_PACKAGE],
53515
- { encoding: "utf8", timeout: 18e4 }
53516
- );
53548
+ const described = `${installer.command} ${installer.args.join(" ")}`;
53549
+ const result = (0, import_child_process11.spawnSync)(installer.command, installer.args, {
53550
+ encoding: "utf8",
53551
+ timeout: 18e4
53552
+ });
53517
53553
  if (result.status === 0 && fs41.existsSync(localOtelHookBin(hooksHome))) {
53518
- return { ok: true, detail: `npm install --prefix ${hooksHome} ${OTEL_HOOKS_PACKAGE}` };
53554
+ return { ok: true, detail: described };
53519
53555
  }
53520
53556
  return {
53521
53557
  ok: false,
53522
- detail: `npm install ${OTEL_HOOKS_PACKAGE} failed: ${(result.stderr || result.stdout || "").trim().slice(0, 400)}`
53558
+ detail: `${described} failed: ${(result.stderr || result.stdout || "").trim().slice(0, 400)}`
53523
53559
  };
53524
53560
  }
53525
53561
  function uninstallLegacyPythonHooks(hooksHome) {
@@ -53530,9 +53566,9 @@ function uninstallLegacyPythonHooks(hooksHome) {
53530
53566
  ["pip3", ["uninstall", "-y", "opentelemetry-hooks"]]
53531
53567
  ];
53532
53568
  for (const [cmd, args] of attempts) {
53533
- if (!commandExists2(cmd)) continue;
53569
+ if (!commandExists3(cmd)) continue;
53534
53570
  try {
53535
- (0, import_child_process10.spawnSync)(cmd, args, { encoding: "utf8", timeout: 6e4 });
53571
+ (0, import_child_process11.spawnSync)(cmd, args, { encoding: "utf8", timeout: 6e4 });
53536
53572
  } catch {
53537
53573
  }
53538
53574
  }
@@ -53564,7 +53600,7 @@ function normalizeLegacyProviderFlagsInCommand(command2) {
53564
53600
  }
53565
53601
  function runSetupProvider(providerId, otelHookCommand, wrapperPath) {
53566
53602
  const hookCommand = `${wrapperPath} --provider ${providerId}`;
53567
- const result = (0, import_child_process10.spawnSync)(
53603
+ const result = (0, import_child_process11.spawnSync)(
53568
53604
  otelHookCommand,
53569
53605
  [
53570
53606
  "setup",
@@ -53805,11 +53841,11 @@ function ensureOtelHooks(options) {
53805
53841
  function disableOtelHooksExport() {
53806
53842
  return writeOtelHooksConfig(buildOtelHooksConfig({ enabled: false }));
53807
53843
  }
53808
- var import_child_process10, fs41, os15, path47, OTEL_HOOKS_PACKAGE, OTEL_HOOKS_PROVIDERS, OTEL_HOOKS_AGENTS, LEGACY_CURSOR_ONLY_HOOK_EVENTS;
53844
+ var import_child_process11, fs41, os15, path47, OTEL_HOOKS_PACKAGE, OTEL_HOOKS_PROVIDERS, OTEL_HOOKS_AGENTS, LEGACY_CURSOR_ONLY_HOOK_EVENTS;
53809
53845
  var init_otel_hooks = __esm({
53810
53846
  "src/core/otel-hooks.ts"() {
53811
53847
  "use strict";
53812
- import_child_process10 = require("child_process");
53848
+ import_child_process11 = require("child_process");
53813
53849
  fs41 = __toESM(require("fs"));
53814
53850
  os15 = __toESM(require("os"));
53815
53851
  path47 = __toESM(require("path"));
@@ -70178,13 +70214,13 @@ var path44 = __toESM(require("path"));
70178
70214
  var readline5 = __toESM(require("readline"));
70179
70215
 
70180
70216
  // src/utils/clipboard.ts
70181
- var import_child_process8 = require("child_process");
70182
- function commandExists(command2) {
70183
- const probe = process.platform === "win32" ? (0, import_child_process8.spawnSync)("where", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }) : (0, import_child_process8.spawnSync)("which", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
70217
+ var import_child_process9 = require("child_process");
70218
+ function commandExists2(command2) {
70219
+ const probe = process.platform === "win32" ? (0, import_child_process9.spawnSync)("where", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }) : (0, import_child_process9.spawnSync)("which", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
70184
70220
  return probe.status === 0;
70185
70221
  }
70186
70222
  function tryPipeCommand(command2, args, text) {
70187
- const result = (0, import_child_process8.spawnSync)(command2, args, {
70223
+ const result = (0, import_child_process9.spawnSync)(command2, args, {
70188
70224
  input: text,
70189
70225
  encoding: "utf8",
70190
70226
  stdio: ["pipe", "ignore", "ignore"]
@@ -70199,12 +70235,12 @@ function writeOsc52Clipboard(text, stream = process.stderr) {
70199
70235
  return true;
70200
70236
  }
70201
70237
  function copyViaPlatformTools(text) {
70202
- if (process.platform === "darwin" && commandExists("pbcopy")) {
70238
+ if (process.platform === "darwin" && commandExists2("pbcopy")) {
70203
70239
  if (tryPipeCommand("pbcopy", [], text)) return { ok: true, method: "pbcopy" };
70204
70240
  }
70205
70241
  if (process.platform === "win32") {
70206
- if (commandExists("powershell")) {
70207
- const result = (0, import_child_process8.spawnSync)(
70242
+ if (commandExists2("powershell")) {
70243
+ const result = (0, import_child_process9.spawnSync)(
70208
70244
  "powershell",
70209
70245
  [
70210
70246
  "-NoProfile",
@@ -70220,18 +70256,18 @@ function copyViaPlatformTools(text) {
70220
70256
  );
70221
70257
  if (result.status === 0 && !result.error) return { ok: true, method: "powershell" };
70222
70258
  }
70223
- if (commandExists("clip") && tryPipeCommand("clip", [], text)) {
70259
+ if (commandExists2("clip") && tryPipeCommand("clip", [], text)) {
70224
70260
  return { ok: true, method: "clip" };
70225
70261
  }
70226
70262
  }
70227
70263
  if (process.platform === "linux" || process.platform === "freebsd" || process.platform === "openbsd") {
70228
- if (commandExists("wl-copy") && tryPipeCommand("wl-copy", [], text)) {
70264
+ if (commandExists2("wl-copy") && tryPipeCommand("wl-copy", [], text)) {
70229
70265
  return { ok: true, method: "wl-copy" };
70230
70266
  }
70231
- if (commandExists("xclip") && tryPipeCommand("xclip", ["-selection", "clipboard"], text)) {
70267
+ if (commandExists2("xclip") && tryPipeCommand("xclip", ["-selection", "clipboard"], text)) {
70232
70268
  return { ok: true, method: "xclip" };
70233
70269
  }
70234
- if (commandExists("xsel") && tryPipeCommand("xsel", ["--clipboard", "--input"], text)) {
70270
+ if (commandExists2("xsel") && tryPipeCommand("xsel", ["--clipboard", "--input"], text)) {
70235
70271
  return { ok: true, method: "xsel" };
70236
70272
  }
70237
70273
  }
@@ -70450,7 +70486,7 @@ init_validation();
70450
70486
  init_control_config();
70451
70487
 
70452
70488
  // src/core/control-lifecycle.ts
70453
- var import_child_process9 = require("child_process");
70489
+ var import_child_process10 = require("child_process");
70454
70490
  var path46 = __toESM(require("path"));
70455
70491
 
70456
70492
  // src/core/control-image.ts
@@ -70503,7 +70539,7 @@ function buildDockerRunArgs(options) {
70503
70539
  return args;
70504
70540
  }
70505
70541
  function runDocker(args) {
70506
- const result = (0, import_child_process9.spawnSync)("docker", args, {
70542
+ const result = (0, import_child_process10.spawnSync)("docker", args, {
70507
70543
  stdio: "inherit",
70508
70544
  env: process.env
70509
70545
  });
@@ -70513,7 +70549,7 @@ function runDocker(args) {
70513
70549
  return result.status ?? 1;
70514
70550
  }
70515
70551
  function controlContainerExists(name = CONTROL_CONTAINER_NAME) {
70516
- const result = (0, import_child_process9.spawnSync)("docker", ["ps", "-aq", "-f", `name=^${name}$`], {
70552
+ const result = (0, import_child_process10.spawnSync)("docker", ["ps", "-aq", "-f", `name=^${name}$`], {
70517
70553
  encoding: "utf8",
70518
70554
  stdio: ["pipe", "pipe", "ignore"]
70519
70555
  });
@@ -72353,13 +72389,13 @@ init_portal_credentials();
72353
72389
  // src/core/portal-login.ts
72354
72390
  var http = __toESM(require("http"));
72355
72391
  var import_crypto = require("crypto");
72356
- var import_child_process11 = require("child_process");
72392
+ var import_child_process12 = require("child_process");
72357
72393
  var CALLBACK_PATH = "/callback";
72358
72394
  var LOGIN_TIMEOUT_MS = 3 * 6e4;
72359
72395
  function openBrowser(url) {
72360
72396
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
72361
72397
  try {
72362
- const child = (0, import_child_process11.spawn)(cmd, args, { stdio: "ignore", detached: true });
72398
+ const child = (0, import_child_process12.spawn)(cmd, args, { stdio: "ignore", detached: true });
72363
72399
  child.on("error", () => {
72364
72400
  });
72365
72401
  child.unref();
@@ -73519,7 +73555,7 @@ __export(util_exports, {
73519
73555
  assertNever: () => assertNever,
73520
73556
  assertNotEqual: () => assertNotEqual,
73521
73557
  assignProp: () => assignProp,
73522
- cached: () => cached,
73558
+ cached: () => cached2,
73523
73559
  captureStackTrace: () => captureStackTrace,
73524
73560
  cleanEnum: () => cleanEnum,
73525
73561
  cleanRegex: () => cleanRegex,
@@ -73584,7 +73620,7 @@ function jsonStringifyReplacer(_2, value) {
73584
73620
  return value.toString();
73585
73621
  return value;
73586
73622
  }
73587
- function cached(getter) {
73623
+ function cached2(getter) {
73588
73624
  const set = false;
73589
73625
  return {
73590
73626
  get value() {
@@ -73673,7 +73709,7 @@ var captureStackTrace = Error.captureStackTrace ? Error.captureStackTrace : (...
73673
73709
  function isObject(data) {
73674
73710
  return typeof data === "object" && data !== null && !Array.isArray(data);
73675
73711
  }
73676
- var allowsEval = cached(() => {
73712
+ var allowsEval = cached2(() => {
73677
73713
  if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) {
73678
73714
  return false;
73679
73715
  }
@@ -75168,7 +75204,7 @@ function handleOptionalObjectResult(result, final, key, input) {
75168
75204
  }
75169
75205
  var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
75170
75206
  $ZodType.init(inst, def);
75171
- const _normalized = cached(() => {
75207
+ const _normalized = cached2(() => {
75172
75208
  const keys = Object.keys(def.shape);
75173
75209
  for (const k of keys) {
75174
75210
  if (!(def.shape[k] instanceof $ZodType)) {
@@ -75403,7 +75439,7 @@ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnio
75403
75439
  }
75404
75440
  return propValues;
75405
75441
  });
75406
- const disc = cached(() => {
75442
+ const disc = cached2(() => {
75407
75443
  const opts = def.options;
75408
75444
  const map = /* @__PURE__ */ new Map();
75409
75445
  for (const o of opts) {
@@ -69,6 +69,11 @@ how this project is built and tested. Use toolchain variables from `.env.agent.<
69
69
  The stock verify section is keyed by `HARNESS_ECOSYSTEM`; it is a starting point,
70
70
  not the repo's final contract. Replace conventions that do not match this project.
71
71
 
72
+ `${NPM_BIN}` may resolve to bun, pnpm, or yarn as well as npm, so keep Node steps
73
+ package-manager agnostic: always `${NPM_BIN:-npm} run <script>` (never bare
74
+ `${NPM_BIN} test`, which runs bun's own test runner), and never npm-only flags
75
+ such as `--prefix` — use a subshell like `(cd docs && ${NPM_BIN:-npm} run build)`.
76
+
72
77
  **Tier contract:**
73
78
 
74
79
  | Mode | Command | Intent |
@@ -25,7 +25,7 @@ Prefer targeted edits over full rewrites. Key files to review:
25
25
  Keep this accurate — it is the harness index. Update whenever scripts, stages, or workflow change.
26
26
 
27
27
  ### `.har/harness.env`, `verify.sh`, `provision-toolchain.sh`, `ecosystem.agent.template.cjs`, `CLAUDE.agent.md`
28
- Align commands and instructions with the current stack. Verify steps must use toolchain paths from `.env.agent.<id>` (`PYTHON_BIN`, `NPM_BIN`, `XCODEBUILD_BIN`, …) — never hardcoded venv or interpreter paths. Replace stock ecosystem conventions that do not match the repository; do not leave npm/pytest/go/cargo/maven/gradle examples in place by accident.
28
+ Align commands and instructions with the current stack. Verify steps must use toolchain paths from `.env.agent.<id>` (`PYTHON_BIN`, `NPM_BIN`, `XCODEBUILD_BIN`, …) — never hardcoded venv or interpreter paths. `NPM_BIN` may be bun, pnpm, or yarn, so Node steps must use `${NPM_BIN:-npm} run <script>` and avoid npm-only flags such as `--prefix`. Replace stock ecosystem conventions that do not match the repository; do not leave npm/pytest/go/cargo/maven/gradle examples in place by accident.
29
29
 
30
30
  ### `.har/env.template`, `setup-infra.sh`, `docker-compose.agent.yml`
31
31
  Update only if infra changed.
@@ -28,7 +28,7 @@ case "$COMMAND" in
28
28
  ENV_FILE="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT" || true)"
29
29
  WORKTREE_DIR="$(existing_slot_worktree "$AGENT_ID")"
30
30
  REG_FILE="$(slot_registry_file "$AGENT_ID")"
31
- PM2_RAW=$(npx --yes pm2 jlist 2>/dev/null || true)
31
+ PM2_RAW=$($(har_pkg_exec) pm2 jlist 2>/dev/null || true)
32
32
  PM2_FOUND=false
33
33
  PM2_FOREIGN=false
34
34
  PM2_LEGACY=false
@@ -123,22 +123,23 @@ process.stdin.on('end', () => {
123
123
  logs)
124
124
  SERVICE="${3:-}"
125
125
  if [ -n "$SERVICE" ]; then
126
- npx pm2 logs "${PM2_SLOT_PREFIX}-${SERVICE}" --lines 100
126
+ $(har_pkg_exec) pm2 logs "${PM2_SLOT_PREFIX}-${SERVICE}" --lines 100
127
127
  else
128
- npx pm2 logs --name "${PM2_SLOT_PREFIX}" --lines 100
128
+ $(har_pkg_exec) pm2 logs --name "${PM2_SLOT_PREFIX}" --lines 100
129
129
  fi
130
130
  ;;
131
131
 
132
132
  restart)
133
133
  SERVICE="${3:-}"
134
+ PKG_EXEC="$(har_pkg_exec)"
134
135
  if [ -n "$SERVICE" ]; then
135
- npx pm2 restart "${PM2_SLOT_PREFIX}-${SERVICE}"
136
+ $(har_pkg_exec) pm2 restart "${PM2_SLOT_PREFIX}-${SERVICE}"
136
137
  else
137
- npx pm2 jlist 2>/dev/null | node -e "
138
+ $(har_pkg_exec) pm2 jlist 2>/dev/null | node -e "
138
139
  const prefix = '${PM2_SLOT_PREFIX}-';
139
140
  const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
140
141
  const names = d.filter(x => x.name && x.name.startsWith(prefix)).map(x => x.name);
141
- names.forEach(n => require('child_process').execSync('npx pm2 restart ' + n, {stdio:'inherit'}));
142
+ names.forEach(n => require('child_process').execSync('${PKG_EXEC} pm2 restart ' + n, {stdio:'inherit'}));
142
143
  if (names.length === 0) console.log('No processes found for ${PM2_SLOT_PREFIX}');
143
144
  " 2>/dev/null || true
144
145
  fi
@@ -184,7 +184,7 @@ har_check_foreign_pm2() {
184
184
  local agent_id="$1"
185
185
  local pm2_raw
186
186
  har_harness_uses_pm2 || return 0
187
- pm2_raw="$(npx --yes pm2 jlist 2>/dev/null || true)"
187
+ pm2_raw="$($(har_pkg_exec) pm2 jlist 2>/dev/null || true)"
188
188
  [ -n "$pm2_raw" ] || return 0
189
189
  set +e
190
190
  echo "$pm2_raw" | node -e "
@@ -51,6 +51,10 @@ export HARNESS_AGENT_SLOT_MAX=5
51
51
  # Ecosystem: auto (detect from manifests) | node | python | go | rust | java | ruby | none
52
52
  export HARNESS_ECOSYSTEM="auto"
53
53
  # export HARNESS_INSTALL_CMD="make deps"
54
+ # Node package manager: auto-detected from package.json "packageManager" and the
55
+ # lockfile (bun.lock → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm),
56
+ # falling back to whichever is installed. Set to pin one: npm | bun | pnpm | yarn
57
+ # export HARNESS_NODE_PACKAGE_MANAGER="bun"
54
58
 
55
59
  # Shared infrastructure — space-separated docker compose service names from
56
60
  # docker-compose.agent.yml. Started ONCE by setup-infra.sh and shared by all
@@ -66,6 +70,119 @@ har_infra_enabled() {
66
70
  esac
67
71
  }
68
72
 
73
+ # ── Node package manager helpers ──────────────────────────────────────────────
74
+ # Mirrored verbatim in harness.env and provision-toolchain.sh so a sourcing
75
+ # script and the provisioning subprocess resolve the same tool.
76
+
77
+ # Package managers HAR can drive, in fallback preference order.
78
+ HAR_NODE_PACKAGE_MANAGERS="npm bun pnpm yarn"
79
+
80
+ # Package manager the repo declares: an explicit HARNESS_NODE_PACKAGE_MANAGER
81
+ # wins, then package.json "packageManager", then the lockfile. Empty when the
82
+ # repo says nothing.
83
+ har_node_declared_package_manager() {
84
+ local dir="${1:-.}"
85
+
86
+ if [ -n "${HARNESS_NODE_PACKAGE_MANAGER:-}" ]; then
87
+ echo "$HARNESS_NODE_PACKAGE_MANAGER"
88
+ return
89
+ fi
90
+ if [ -f "$dir/package.json" ]; then
91
+ local field
92
+ field="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' "$dir/package.json" | head -1)"
93
+ if [ -n "$field" ]; then
94
+ echo "$field"
95
+ return
96
+ fi
97
+ fi
98
+ if [ -f "$dir/bun.lock" ] || [ -f "$dir/bun.lockb" ]; then echo bun; return; fi
99
+ if [ -f "$dir/pnpm-lock.yaml" ]; then echo pnpm; return; fi
100
+ if [ -f "$dir/yarn.lock" ]; then echo yarn; return; fi
101
+ if [ -f "$dir/package-lock.json" ]; then echo npm; return; fi
102
+ echo ""
103
+ }
104
+
105
+ # Package manager to actually run. A manager the repo declares but this machine
106
+ # lacks falls back to one that is installed, so a repo pinned to npm still
107
+ # provisions on a bun-only machine (and the reverse).
108
+ har_node_package_manager() {
109
+ local dir="${1:-.}"
110
+ local declared
111
+ declared="$(har_node_declared_package_manager "$dir")"
112
+
113
+ if [ -n "$declared" ] && command -v "$declared" >/dev/null 2>&1; then
114
+ echo "$declared"
115
+ return
116
+ fi
117
+
118
+ local candidate
119
+ for candidate in $HAR_NODE_PACKAGE_MANAGERS; do
120
+ if command -v "$candidate" >/dev/null 2>&1; then
121
+ echo "$candidate"
122
+ return
123
+ fi
124
+ done
125
+
126
+ echo "${declared:-npm}"
127
+ }
128
+
129
+ # Lockfile a manager writes, so a substitute install can clean up after itself.
130
+ har_node_lockfile() {
131
+ case "$1" in
132
+ bun) echo "bun.lock" ;;
133
+ npm) echo "package-lock.json" ;;
134
+ pnpm) echo "pnpm-lock.yaml" ;;
135
+ yarn) echo "yarn.lock" ;;
136
+ *) echo "" ;;
137
+ esac
138
+ }
139
+
140
+ # Install dependencies in a directory. When a substitute manager stands in for
141
+ # the one the repo declares, any lockfile it creates is removed afterwards:
142
+ # provisioning must not migrate the repo to a different package manager.
143
+ # (bun writes bun.lock even under --no-save, so the flags alone are not enough.)
144
+ har_node_install() {
145
+ local dir="$1"
146
+ local manager="$2"
147
+ local declared="${3:-}"
148
+ local substituting=false
149
+ local lockfile=""
150
+ local had_lockfile=false
151
+
152
+ if [ -n "$declared" ] && [ "$declared" != "$manager" ]; then
153
+ substituting=true
154
+ lockfile="$(har_node_lockfile "$manager")"
155
+ if [ -n "$lockfile" ] && [ -e "$dir/$lockfile" ]; then
156
+ had_lockfile=true
157
+ fi
158
+ fi
159
+
160
+ local code=0
161
+ (cd "$dir" && "$manager" install --silent) || code=$?
162
+
163
+ if [ "$substituting" = true ] && [ "$had_lockfile" = false ] && [ -n "$lockfile" ]; then
164
+ rm -f "$dir/$lockfile"
165
+ [ "$manager" = "bun" ] && rm -f "$dir/bun.lockb"
166
+ fi
167
+ return $code
168
+ }
169
+
170
+ # Package runner (npx equivalent) for one-off CLIs such as pm2, including the
171
+ # flag that keeps it non-interactive. Takes a manager name, or none to resolve
172
+ # from the environment and PATH.
173
+ har_pkg_exec() {
174
+ case "${1:-${HARNESS_NODE_PACKAGE_MANAGER:-}}" in
175
+ bun) echo "bunx"; return ;;
176
+ pnpm) echo "pnpm dlx"; return ;;
177
+ yarn) echo "yarn dlx"; return ;;
178
+ npm) echo "npx --yes"; return ;;
179
+ esac
180
+ if command -v npx >/dev/null 2>&1; then echo "npx --yes"; return; fi
181
+ if command -v bunx >/dev/null 2>&1; then echo "bunx"; return; fi
182
+ if command -v pnpm >/dev/null 2>&1; then echo "pnpm dlx"; return; fi
183
+ echo "npx --yes"
184
+ }
185
+
69
186
  # Postgres client — uses host tools when installed, otherwise docker exec into
70
187
  # the shared db container. Usage: har_pg psql -d postgres -c "SELECT 1"
71
188
  # har_pg createdb -T template_x agent_1
@@ -249,13 +249,13 @@ DEBUG_PORT="$DEBUG_PORT" \
249
249
 
250
250
  PM2_REGEX="$(har_pm2_delete_regex "$AGENT_ID")"
251
251
  # Stop existing processes for this agent (project-scoped — never touch other harnesses).
252
- npx --yes pm2 delete "$PM2_REGEX" 2>/dev/null || true
252
+ $(har_pkg_exec) pm2 delete "$PM2_REGEX" 2>/dev/null || true
253
253
 
254
254
  # Start PM2 processes
255
255
  log "Starting PM2 processes..."
256
256
  cd "$WORK_DIR"
257
- npx pm2 start "$ECOSYSTEM_FILE"
258
- npx pm2 save --force >/dev/null 2>&1 || true
257
+ $(har_pkg_exec) pm2 start "$ECOSYSTEM_FILE"
258
+ $(har_pkg_exec) pm2 save --force >/dev/null 2>&1 || true
259
259
 
260
260
  # Health check
261
261
  if [ -n "${HARNESS_HEALTH_CHECK_PATH:-}" ]; then