hub-runner 0.2.6 → 0.2.7

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/README.md CHANGED
@@ -12,7 +12,7 @@ hub-runner start
12
12
 
13
13
  | Command | What it does |
14
14
  |---|---|
15
- | `hub-runner start [--take-over]` | Starts the runner under launchd (`~/Library/LaunchAgents/dev.hub.runner.plist`). It starts again at every login and is not restarted after a crash: run `start` again. `--take-over` claims the Mac slot from another Mac. |
15
+ | `hub-runner start [--take-over]` | Starts the runner under launchd (`~/Library/LaunchAgents/dev.hub.runner.plist`). It starts again at every login and is not restarted after a crash: run `start` again. `--take-over` claims the Mac slot from another Mac on that start only: a later start at login claims without it. |
16
16
  | `hub-runner stop` | Stops it. |
17
17
  | `hub-runner restart` | Lets the open turns finish, then starts it again. |
18
18
  | `hub-runner status` | The runner's state, pid, version, bots, and whether launchd has it loaded. Exits 1 when it isn't running. |
@@ -33,5 +33,5 @@ npm i -g hub-runner@latest --min-release-age=0 && hub-runner restart
33
33
 
34
34
  - Node 24 or later, and `pnpm` on your PATH when you run `start`: the runner uses both to set the bots up.
35
35
  - The hub is `https://hosted-hub.playground-vercel.tools`; set `HUB_URL` when you run `login` for another one.
36
- - The HubHosted app only shows the runner's state. Quitting, signing out of or replacing the app doesn't touch the runner.
36
+ - The HubHosted app runs a runner of its own for each workspace with a Mac bot, in `~/.hub/local/<scope>` under the launchd job `dev.hub.runner.<scope>`; this CLI acts on one of those when `HUB_RUNNER_DIR` and `HUB_RUNNER_LABEL` name it. An npm install keeps `~/.hub/local` and `dev.hub.runner`. Quitting, signing out of or replacing the app doesn't stop a runner.
37
37
  - If the runner doesn't start, `hub-runner status` and `hub-runner logs` say why.
package/check.mjs CHANGED
@@ -196,18 +196,19 @@ async function stopTree(pgid, tree, { name, timeoutMs, warn }) {
196
196
  killTree(pgid, tree);
197
197
  return untilTreeGone(pgid, tree, KILL_GRACE_MS);
198
198
  }
199
- function ownsLeader(record, rows, signature) {
199
+ var signature = (name, port) => ` dev --agent ${name} --port ${port} `;
200
+ function ownsLeader(record, rows, signature2) {
200
201
  if (!(record.pid > 1) || record.pgid !== record.pid) return false;
201
202
  const row = rows.find((r) => r.pid === record.pid);
202
203
  if (!row || row.pgid !== record.pgid) return false;
203
- if (!` ${row.command} `.includes(signature)) return false;
204
+ if (!` ${row.command} `.includes(signature2)) return false;
204
205
  const member = record.tree?.find((m) => m.pid === record.pid);
205
206
  if (member) return member.startedAt === row.startedAt;
206
207
  const recorded = Date.parse(record.startedAt);
207
208
  return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
208
209
  }
209
- function ourTree(record, rows, signature) {
210
- const leader = ownsLeader(record, rows, signature);
210
+ function ourTree(record, rows, signature2) {
211
+ const leader = ownsLeader(record, rows, signature2);
211
212
  return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
212
213
  }
213
214
 
@@ -255,7 +256,6 @@ function createSupervisor({
255
256
  const entries = /* @__PURE__ */ new Map();
256
257
  let closed = false;
257
258
  let starting = Promise.resolve();
258
- const signature = (name, port) => ` dev --agent ${name} --port ${port} `;
259
259
  const argvFor = (name, port) => [...command, "dev", "--agent", name, "--port", String(port), "--no-ui", "--no-default-extensions", "--resume"];
260
260
  const envFor = (agent) => typeof env === "function" ? env(agent) : env;
261
261
  const fingerprintFor = (agent, spawnEnv = envFor(agent)) => fingerprint ? fingerprint({ agent, env: spawnEnv, argv: argvFor(agent.name, agent.port) }) : null;
package/lib/launchd.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  // The runner's launchd job: a plist in the user's LaunchAgents, loaded in the GUI domain, so it starts at login and
2
2
  // outlives every app. `RunAtLoad` without `KeepAlive`: a crash waits for `hub-runner start`.
3
3
  import { execFileSync, spawnSync } from 'node:child_process';
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
- import { LABEL, appHome, credentialsFile, logFile, mainFile, plistFile, runnerDir, version } from './paths.mjs';
6
+ import { LABEL, appHome, credentialsFile, logFile, mainFile, plistFile, runnerDir, takeOverFile, version } from './paths.mjs';
7
7
  import { deviceName } from './device.mjs';
8
8
 
9
9
  const xml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -41,9 +41,9 @@ function program(home) {
41
41
  return { node: process.execPath, main: mainFile(), pnpm, path: process.env.PATH, build: version() };
42
42
  }
43
43
 
44
- export function plist({ takeOver }) {
44
+ export function plist() {
45
45
  const run = program(appHome());
46
- const env = { HUB_PNPM: run.pnpm, PATH: run.path, HUB_RUNNER_DIR: runnerDir(), HUB_RUNNER_BUILD: run.build, HUB_DEVICE_NAME: deviceName(), ...(takeOver ? { HUB_RUNNER_TAKE_OVER: '1' } : {}) };
46
+ const env = { HUB_PNPM: run.pnpm, PATH: run.path, HUB_RUNNER_DIR: runnerDir(), HUB_RUNNER_BUILD: run.build, HUB_DEVICE_NAME: deviceName() };
47
47
  return `<?xml version="1.0" encoding="UTF-8"?>
48
48
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
49
49
  <plist version="1.0">
@@ -78,7 +78,10 @@ export function start({ takeOver = false } = {}) {
78
78
  if (!existsSync(credentialsFile())) throw new Error('not signed in: run `hub-runner login` first');
79
79
  mkdirSync(runnerDir(), { recursive: true });
80
80
  mkdirSync(dirname(plistFile()), { recursive: true });
81
- writeFileSync(plistFile(), plist({ takeOver }));
81
+ writeFileSync(plistFile(), plist());
82
+ // A takeover is for the start it's asked on, never a later load of the plist (a login): the runner reads and deletes this.
83
+ if (takeOver) writeFileSync(takeOverFile(), '');
84
+ else rmSync(takeOverFile(), { force: true });
82
85
  if (loaded()) bootout();
83
86
  launchctl(['bootstrap', domain(), plistFile()]);
84
87
  }
package/lib/paths.mjs CHANGED
@@ -2,11 +2,14 @@ import { readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
 
5
- export const LABEL = 'dev.hub.runner';
5
+ // The Hub app runs one runner per workspace, each under its own label (RunnerInstall.swift); an npm install keeps the one.
6
+ export const LABEL = process.env.HUB_RUNNER_LABEL || 'dev.hub.runner';
6
7
  export const DEFAULT_HUB = 'https://hosted-hub.playground-vercel.tools';
7
8
  export const runnerDir = () => process.env.HUB_RUNNER_DIR || join(homedir(), '.hub', 'local');
8
9
  export const credentialsFile = () => join(runnerDir(), 'credentials.json');
9
10
  export const logFile = () => join(runnerDir(), 'runner.log');
11
+ // `start --take-over` asks the next runner to start to take the claim over; it deletes the file (hosted-hub/runner/main.ts).
12
+ export const takeOverFile = () => join(runnerDir(), 'take-over');
10
13
  export const plistFile = () => join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
11
14
  export const mainFile = () => new URL('../main.mjs', import.meta.url).pathname;
12
15
  export const version = () => JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
package/main.mjs CHANGED
@@ -1580,27 +1580,27 @@ var require_adapter = __commonJS({
1580
1580
  var require_proper_lockfile = __commonJS({
1581
1581
  "../../hosted-hub/node_modules/proper-lockfile/index.js"(exports, module) {
1582
1582
  "use strict";
1583
- var lockfile2 = require_lockfile();
1583
+ var lockfile3 = require_lockfile();
1584
1584
  var { toPromise, toSync, toSyncOptions } = require_adapter();
1585
1585
  async function lock(file, options) {
1586
- const release2 = await toPromise(lockfile2.lock)(file, options);
1586
+ const release2 = await toPromise(lockfile3.lock)(file, options);
1587
1587
  return toPromise(release2);
1588
1588
  }
1589
1589
  function lockSync(file, options) {
1590
- const release2 = toSync(lockfile2.lock)(file, toSyncOptions(options));
1590
+ const release2 = toSync(lockfile3.lock)(file, toSyncOptions(options));
1591
1591
  return toSync(release2);
1592
1592
  }
1593
1593
  function unlock(file, options) {
1594
- return toPromise(lockfile2.unlock)(file, options);
1594
+ return toPromise(lockfile3.unlock)(file, options);
1595
1595
  }
1596
1596
  function unlockSync(file, options) {
1597
- return toSync(lockfile2.unlock)(file, toSyncOptions(options));
1597
+ return toSync(lockfile3.unlock)(file, toSyncOptions(options));
1598
1598
  }
1599
1599
  function check(file, options) {
1600
- return toPromise(lockfile2.check)(file, options);
1600
+ return toPromise(lockfile3.check)(file, options);
1601
1601
  }
1602
1602
  function checkSync(file, options) {
1603
- return toSync(lockfile2.check)(file, toSyncOptions(options));
1603
+ return toSync(lockfile3.check)(file, toSyncOptions(options));
1604
1604
  }
1605
1605
  module.exports = lock;
1606
1606
  module.exports.lock = lock;
@@ -1613,11 +1613,11 @@ var require_proper_lockfile = __commonJS({
1613
1613
  });
1614
1614
 
1615
1615
  // ../../hosted-hub/runner/main.ts
1616
- var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
1616
+ var import_proper_lockfile2 = __toESM(require_proper_lockfile(), 1);
1617
1617
  import { homedir as homedir4 } from "node:os";
1618
1618
  import { randomUUID } from "node:crypto";
1619
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, realpathSync as realpathSync4, writeFileSync as writeFileSync7 } from "node:fs";
1620
- import { join as join16 } from "node:path";
1619
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync11, realpathSync as realpathSync4, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
1620
+ import { join as join17 } from "node:path";
1621
1621
 
1622
1622
  // ../../hosted-hub/lib/clean-env.mjs
1623
1623
  var INHERITED_ENV = /^(EVE_|WORKFLOW_)/;
@@ -1861,18 +1861,19 @@ async function stopTree(pgid, tree, { name, timeoutMs, warn }) {
1861
1861
  killTree(pgid, tree);
1862
1862
  return untilTreeGone(pgid, tree, KILL_GRACE_MS);
1863
1863
  }
1864
- function ownsLeader(record, rows, signature) {
1864
+ var signature = (name, port) => ` dev --agent ${name} --port ${port} `;
1865
+ function ownsLeader(record, rows, signature2) {
1865
1866
  if (!(record.pid > 1) || record.pgid !== record.pid) return false;
1866
1867
  const row = rows.find((r) => r.pid === record.pid);
1867
1868
  if (!row || row.pgid !== record.pgid) return false;
1868
- if (!` ${row.command} `.includes(signature)) return false;
1869
+ if (!` ${row.command} `.includes(signature2)) return false;
1869
1870
  const member = record.tree?.find((m) => m.pid === record.pid);
1870
1871
  if (member) return member.startedAt === row.startedAt;
1871
1872
  const recorded = Date.parse(record.startedAt);
1872
1873
  return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
1873
1874
  }
1874
- function ourTree(record, rows, signature) {
1875
- const leader = ownsLeader(record, rows, signature);
1875
+ function ourTree(record, rows, signature2) {
1876
+ const leader = ownsLeader(record, rows, signature2);
1876
1877
  return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
1877
1878
  }
1878
1879
 
@@ -1920,7 +1921,6 @@ function createSupervisor({
1920
1921
  const entries = /* @__PURE__ */ new Map();
1921
1922
  let closed = false;
1922
1923
  let starting = Promise.resolve();
1923
- const signature = (name, port) => ` dev --agent ${name} --port ${port} `;
1924
1924
  const argvFor = (name, port) => [...command, "dev", "--agent", name, "--port", String(port), "--no-ui", "--no-default-extensions", "--resume"];
1925
1925
  const envFor = (agent) => typeof env === "function" ? env(agent) : env;
1926
1926
  const fingerprintFor = (agent, spawnEnv = envFor(agent)) => fingerprint ? fingerprint({ agent, env: spawnEnv, argv: argvFor(agent.name, agent.port) }) : null;
@@ -4552,8 +4552,79 @@ async function stopWithin(stop, ms, signal) {
4552
4552
  ]);
4553
4553
  }
4554
4554
 
4555
+ // ../../hosted-hub/runner/ports.ts
4556
+ var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
4557
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync10 } from "node:fs";
4558
+ import { join as join16 } from "node:path";
4559
+ var listening = async (port) => (await listeners(port)).length > 0;
4560
+ async function ownedPorts(stateDir, root) {
4561
+ let records;
4562
+ try {
4563
+ records = JSON.parse(readFileSync10(join16(stateDir, "processes.json"), "utf8"));
4564
+ } catch {
4565
+ return /* @__PURE__ */ new Map();
4566
+ }
4567
+ const owned = /* @__PURE__ */ new Map();
4568
+ const rows = table();
4569
+ for (const r of Array.isArray(records) ? records : []) {
4570
+ if (typeof r?.agent !== "string" || typeof r?.port !== "number" || r.workspace && r.workspace !== root) continue;
4571
+ const { tree } = ourTree(r, rows, signature(r.agent, r.port));
4572
+ const holders = await listeners(r.port).catch(() => []);
4573
+ if (holders.length && holders.every((pid) => tree.some((m) => m.pid === pid))) owned.set(r.agent, r.port);
4574
+ }
4575
+ return owned;
4576
+ }
4577
+ function portClaims(dir2, warn) {
4578
+ const held = /* @__PURE__ */ new Map();
4579
+ mkdirSync7(dir2, { recursive: true });
4580
+ const release2 = async (port) => {
4581
+ const r = held.get(port);
4582
+ held.delete(port);
4583
+ await r?.().catch(() => {
4584
+ });
4585
+ };
4586
+ return {
4587
+ claim: async (port) => {
4588
+ if (held.has(port)) return true;
4589
+ const path = join16(dir2, `${port}.lock`);
4590
+ const done = await import_proper_lockfile.default.lock(path, {
4591
+ lockfilePath: path,
4592
+ realpath: false,
4593
+ stale: 1e4,
4594
+ onCompromised: (e) => {
4595
+ held.delete(port);
4596
+ warn(`port ${port}: claim lost, ${e.message}`);
4597
+ }
4598
+ }).catch((error) => {
4599
+ if (error.code !== "ELOCKED") throw error;
4600
+ return null;
4601
+ });
4602
+ if (done) held.set(port, done);
4603
+ return !!done;
4604
+ },
4605
+ release: release2,
4606
+ releaseAll: () => Promise.all([...held.keys()].map(release2))
4607
+ };
4608
+ }
4609
+ async function pickPorts(start, roster, taken, portBase, owned, portBusy, claims) {
4610
+ const picked = [];
4611
+ for (const name of start) {
4612
+ const own = owned.get(name);
4613
+ let port = own ?? portBase + roster.indexOf(name);
4614
+ for (; ; port++) {
4615
+ if (taken.has(port) || !await claims.claim(port)) continue;
4616
+ if (port === own || !await portBusy(port)) break;
4617
+ await claims.release(port);
4618
+ }
4619
+ taken.add(port);
4620
+ picked.push({ name, port });
4621
+ }
4622
+ return picked;
4623
+ }
4624
+
4555
4625
  // ../../hosted-hub/runner/hub-runner.ts
4556
- var realBots = { stage: stageLocal, supervise: createSupervisor };
4626
+ var supervise = (o) => Object.assign(createSupervisor(o), { owned: () => ownedPorts(o.stateDir, o.root) });
4627
+ var realBots = { stage: stageLocal, supervise };
4557
4628
  var HeldElsewhere = class extends Error {
4558
4629
  holder;
4559
4630
  constructor(holder) {
@@ -4581,6 +4652,7 @@ function botEnv(env, config, bot, token) {
4581
4652
  var log = (...args) => console.error((/* @__PURE__ */ new Date()).toISOString(), "[hub-runner]", ...args);
4582
4653
  async function run3(config, bots = realBots) {
4583
4654
  const shutdown = new AbortController();
4655
+ const portBusy = config.portBusy ?? listening, claims = portClaims(config.portsDir, (line) => log("ports", line));
4584
4656
  const net = (input, init = {}) => fetch(input, { ...init, signal: init.signal ? AbortSignal.any([shutdown.signal, init.signal]) : shutdown.signal });
4585
4657
  let token = "";
4586
4658
  let supersede;
@@ -4609,7 +4681,7 @@ async function run3(config, bots = realBots) {
4609
4681
  if (!res.ok) throw new Error(`${init?.method ?? "GET"} ${path}: HTTP ${res.status} ${(await res.text()).slice(0, 200)}`);
4610
4682
  return res.json();
4611
4683
  };
4612
- const claim = await hub("/api/local-runner/claim", { method: "POST", body: JSON.stringify({ device: config.device, deviceName: config.deviceName, takeOver: config.takeOver, attachments: true }) });
4684
+ const claim = await hub("/api/local-runner/claim", { method: "POST", body: JSON.stringify({ device: config.device, deviceName: config.deviceName, takeOver: config.takeOver, explicit: true, takeOverFrom: config.takeOverFrom, attachments: true }) });
4613
4685
  if (claim.status === 409) throw new HeldElsewhere((await claim.json()).holder);
4614
4686
  if (!claim.ok) throw new Error(`claim: HTTP ${claim.status} ${(await claim.text()).slice(0, 200)}`);
4615
4687
  const claimed = await claim.json();
@@ -4706,13 +4778,16 @@ async function run3(config, bots = realBots) {
4706
4778
  }
4707
4779
  await tell(null);
4708
4780
  const wanted = work.bots;
4709
- const taken = new Set([...running.values()].map((a) => a.port));
4710
- const fresh = wanted.filter((name) => !running.has(name)).map((name) => {
4711
- let port = config.portBase + work.bots.indexOf(name);
4712
- while (taken.has(port)) port++;
4713
- taken.add(port);
4714
- return { name, port };
4715
- });
4781
+ const owned = adopted ? /* @__PURE__ */ new Map() : await supervisor.owned();
4782
+ const fresh = await pickPorts(
4783
+ wanted.filter((n) => !running.has(n)),
4784
+ work.bots,
4785
+ new Set([...running.values()].map((a) => a.port)),
4786
+ config.portBase,
4787
+ owned,
4788
+ portBusy,
4789
+ claims
4790
+ );
4716
4791
  shutdown.signal.throwIfAborted();
4717
4792
  for (const agent of fresh) running.set(agent.name, agent);
4718
4793
  if (!adopted) {
@@ -4721,7 +4796,7 @@ async function run3(config, bots = realBots) {
4721
4796
  } else for (const agent of fresh) supervisor.start(agent);
4722
4797
  for (const [name, agent] of running) if (!wanted.includes(name)) {
4723
4798
  running.delete(name);
4724
- void supervisor.stop(agent);
4799
+ void supervisor.stop(agent).then(() => claims.release(agent.port));
4725
4800
  }
4726
4801
  report2();
4727
4802
  await Promise.all(fresh.map(async (agent) => {
@@ -4975,6 +5050,7 @@ async function run3(config, bots = realBots) {
4975
5050
  model.stop();
4976
5051
  clearTimeout(retry);
4977
5052
  await supervisor.stopAll();
5053
+ await claims.releaseAll();
4978
5054
  await Promise.allSettled([feed, syncing]);
4979
5055
  };
4980
5056
  void superseded.then(() => stopRunner(false));
@@ -4989,24 +5065,30 @@ async function run3(config, bots = realBots) {
4989
5065
  var need = (name) => process.env[name] || (() => {
4990
5066
  throw new Error(`${name} is required`);
4991
5067
  })();
4992
- var configured = process.env.HUB_RUNNER_DIR || join16(homedir4(), ".hub", "local");
4993
- mkdirSync7(configured, { recursive: true });
5068
+ var configured = process.env.HUB_RUNNER_DIR || join17(homedir4(), ".hub", "local");
5069
+ mkdirSync8(configured, { recursive: true });
4994
5070
  var dir = realpathSync4(configured);
4995
- mkdirSync7(join16(dir, "status"), { recursive: true });
4996
- statusFile(join16(dir, "status", `${process.pid}.json`), { pid: process.pid, state: "starting", build: process.env.HUB_RUNNER_BUILD ?? "" });
5071
+ mkdirSync8(join17(dir, "status"), { recursive: true });
5072
+ statusFile(join17(dir, "status", `${process.pid}.json`), { pid: process.pid, state: "starting", build: process.env.HUB_RUNNER_BUILD ?? "" });
4997
5073
  process.on("exit", exitStatus);
4998
5074
  var drainAsked = new Promise((resolve6) => process.on("SIGUSR2", () => resolve6()));
4999
- var release = await import_proper_lockfile.default.lock(dir, { lockfilePath: join16(dir, "runner.lock"), stale: 1e4, realpath: false }).catch((error) => {
5075
+ var release = await import_proper_lockfile2.default.lock(dir, { lockfilePath: join17(dir, "runner.lock"), stale: 1e4, realpath: false }).catch((error) => {
5000
5076
  if (error.code !== "ELOCKED") throw error;
5001
5077
  status("busy-here");
5002
5078
  process.exit(5);
5003
5079
  });
5004
- var deviceFile = join16(dir, "device-id");
5080
+ var deviceFile = join17(dir, "device-id");
5005
5081
  if (!existsSync11(deviceFile)) writeFileSync7(deviceFile, randomUUID());
5006
- var device = readFileSync10(deviceFile, "utf8").trim();
5007
- var credentials = process.env.HUB_URL || process.env.HUB_SESSION ? { hubUrl: need("HUB_URL"), session: need("HUB_SESSION"), bypass: process.env.HUB_PROTECTION_BYPASS || void 0 } : JSON.parse(readFileSync10(join16(dir, "credentials.json"), "utf8"));
5082
+ var device = readFileSync11(deviceFile, "utf8").trim();
5083
+ var credentials = process.env.HUB_URL || process.env.HUB_SESSION ? { hubUrl: need("HUB_URL"), session: need("HUB_SESSION"), bypass: process.env.HUB_PROTECTION_BYPASS || void 0 } : JSON.parse(readFileSync11(join17(dir, "credentials.json"), "utf8"));
5008
5084
  if ("workspace" in credentials && credentials.workspace) status("starting", { workspace: credentials.workspace });
5009
5085
  var deviceName = process.env.HUB_DEVICE_NAME || device;
5086
+ var takeOverFile = join17(dir, "take-over");
5087
+ var takeOverFromFile = join17(dir, "take-over-from");
5088
+ var takeOver = existsSync11(takeOverFile);
5089
+ var takeOverFrom = existsSync11(takeOverFromFile) ? readFileSync11(takeOverFromFile, "utf8").trim() || void 0 : void 0;
5090
+ rmSync5(takeOverFile, { force: true });
5091
+ rmSync5(takeOverFromFile, { force: true });
5010
5092
  var started = await run3({
5011
5093
  hubUrl: credentials.hubUrl.replace(/\/$/, ""),
5012
5094
  session: credentials.session,
@@ -5016,9 +5098,12 @@ var started = await run3({
5016
5098
  pnpm: need("HUB_PNPM"),
5017
5099
  // Two runners on one machine (the e2e) need their own ports; two Macs never share any.
5018
5100
  portBase: Number(process.env.HUB_RUNNER_PORT_BASE) || 5400,
5101
+ // Shared by every workspace's runner on this Mac, whatever its dir: each claims its bots' ports there.
5102
+ portsDir: process.env.HUB_RUNNER_PORTS_DIR || join17(homedir4(), ".hub", "ports"),
5019
5103
  device,
5020
5104
  deviceName,
5021
- takeOver: process.env.HUB_RUNNER_TAKE_OVER === "1"
5105
+ takeOver,
5106
+ takeOverFrom
5022
5107
  }).catch(async (error) => {
5023
5108
  if (!(error instanceof HeldElsewhere)) throw error;
5024
5109
  status("held-elsewhere", { holder: error.holder });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hub-runner",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Runs a hub workspace's Mac bots on this Mac under launchd, with no app needed.",
6
6
  "type": "module",