@bivy/bivy 0.1.0 → 0.1.1-staging.21

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.
@@ -117,6 +117,23 @@ export class ControlPlaneTaskPoller {
117
117
  poke() {
118
118
  void this.tick();
119
119
  }
120
+ /**
121
+ * Replace the routing labels this live poller serves.
122
+ *
123
+ * Node names are editable while the daemon is running, and targeted queue
124
+ * labels are derived from that name (`bivy/<name>`). Keeping the startup-time
125
+ * labels forever leaves work routed to a renamed node pending until the daemon
126
+ * restarts. Update in place so already-running queue jobs are not disturbed,
127
+ * then poll immediately for work addressed to the new name.
128
+ */
129
+ setLabels(labels) {
130
+ const next = Array.from(new Set(labels.map((label) => label.trim()).filter(Boolean)));
131
+ if (!next.length || (next.length === this.cfg.labels.length && next.every((label, i) => label === this.cfg.labels[i])))
132
+ return;
133
+ this.cfg.labels = next;
134
+ console.log(`[control-plane-tasks] now watching hosted queue for labels [${next.join(", ")}]`);
135
+ this.poke();
136
+ }
120
137
  stop() {
121
138
  if (this.timer)
122
139
  clearInterval(this.timer);
package/dist/git-auth.js CHANGED
@@ -88,7 +88,13 @@ function writeIfChanged(file, content, mode) {
88
88
  export function ensureCredentialHelper() {
89
89
  fs.mkdirSync(credDir(), { recursive: true, mode: 0o700 });
90
90
  writeIfChanged(workerPath(), WORKER_SOURCE, 0o700);
91
- const shim = `#!/usr/bin/env sh\nexec ${shDq(process.execPath)} ${shDq(workerPath())} "$@"\n`;
91
+ // Git can invoke a credential helper after its caller's checkout has been
92
+ // removed (for example while an old daemon drains across an npm update, or
93
+ // while a broken clone is being rebuilt). Node resolves its cwd before it
94
+ // evaluates the worker and aborts with uv_cwd when that inherited directory
95
+ // has been unlinked. Move to a stable directory in the shell first; the worker
96
+ // resolves endpoint.json from import.meta.url and never relies on cwd.
97
+ const shim = `#!/usr/bin/env sh\ncd / || exit 0\nexec ${shDq(process.execPath)} ${shDq(workerPath())} "$@"\n`;
92
98
  writeIfChanged(shimPath(), shim, 0o700);
93
99
  return shimPath();
94
100
  }
@@ -32,7 +32,7 @@ export function parseGitHubRemote(input) {
32
32
  /** Infer owner/repo from a workspace's origin remote, if it is a GitHub checkout. */
33
33
  export async function inferGitHubRepoFromWorkspace(workspace) {
34
34
  try {
35
- const { stdout } = await exec("git", ["-C", workspace, "remote", "get-url", "origin"]);
35
+ const { stdout } = await exec("git", ["-C", workspace, "remote", "get-url", "origin"], { cwd: workspace });
36
36
  return parseGitHubRemote(stdout);
37
37
  }
38
38
  catch {
@@ -65,7 +65,7 @@ export async function resolveGitHubToken(env = process.env) {
65
65
  */
66
66
  export async function fetchOrigin(repoDir) {
67
67
  try {
68
- await exec("git", ["-C", repoDir, "fetch", "origin", "--prune"], { timeout: 120_000 });
68
+ await exec("git", ["-C", repoDir, "fetch", "origin", "--prune"], { cwd: repoDir, timeout: 120_000 });
69
69
  }
70
70
  catch {
71
71
  // offline / no origin / no rights — branch off the refs we already have.
@@ -80,7 +80,7 @@ export async function fetchOrigin(repoDir) {
80
80
  export async function resolveDefaultBaseRef(repoDir) {
81
81
  // origin/HEAD points at the remote's default branch when it's been recorded.
82
82
  try {
83
- const { stdout } = await exec("git", ["-C", repoDir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
83
+ const { stdout } = await exec("git", ["-C", repoDir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd: repoDir });
84
84
  const ref = stdout.trim();
85
85
  if (ref)
86
86
  return ref; // e.g. "origin/main"
@@ -89,8 +89,8 @@ export async function resolveDefaultBaseRef(repoDir) {
89
89
  // origin/HEAD is often unset on a fresh clone — record it, then retry.
90
90
  }
91
91
  try {
92
- await exec("git", ["-C", repoDir, "remote", "set-head", "origin", "--auto"]);
93
- const { stdout } = await exec("git", ["-C", repoDir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
92
+ await exec("git", ["-C", repoDir, "remote", "set-head", "origin", "--auto"], { cwd: repoDir });
93
+ const { stdout } = await exec("git", ["-C", repoDir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd: repoDir });
94
94
  const ref = stdout.trim();
95
95
  if (ref)
96
96
  return ref;
@@ -112,7 +112,7 @@ export async function resolveDefaultBaseRef(repoDir) {
112
112
  export async function resolveBranchBaseRef(repoDir, branch) {
113
113
  const ref = `origin/${branch}`;
114
114
  try {
115
- await exec("git", ["-C", repoDir, "rev-parse", "--verify", "--quiet", ref]);
115
+ await exec("git", ["-C", repoDir, "rev-parse", "--verify", "--quiet", ref], { cwd: repoDir });
116
116
  return ref;
117
117
  }
118
118
  catch {
@@ -131,7 +131,7 @@ export async function isReusableCheckout(dest) {
131
131
  if (!fs.existsSync(path.join(dest, ".git")))
132
132
  return false;
133
133
  try {
134
- await exec("git", ["-C", dest, "rev-parse", "--show-toplevel"]);
134
+ await exec("git", ["-C", dest, "rev-parse", "--show-toplevel"], { cwd: dest });
135
135
  return true;
136
136
  }
137
137
  catch {
@@ -177,15 +177,15 @@ export async function cloneOrUpdateRepo(opts) {
177
177
  try {
178
178
  // Rewrite origin to the clean URL. This also MIGRATES any pre-existing
179
179
  // clone whose remote still carries an embedded token from before this fix.
180
- await exec("git", ["-C", dest, "remote", "set-url", "origin", url]);
181
- await configureRepoCredentialHelper((a) => exec("git", a), dest);
182
- await exec("git", ["-C", dest, ...cc, "fetch", "--all", "--prune"], { timeout: 120_000, env });
180
+ await exec("git", ["-C", dest, "remote", "set-url", "origin", url], { cwd: dest });
181
+ await configureRepoCredentialHelper((a) => exec("git", a, { cwd: dest }), dest);
182
+ await exec("git", ["-C", dest, ...cc, "fetch", "--all", "--prune"], { cwd: dest, timeout: 120_000, env });
183
183
  // Re-point origin/HEAD at the remote's CURRENT default branch. Without
184
184
  // this a cached checkout keeps whatever default it recorded at first
185
185
  // clone, so if the repo's default later changed (e.g. main → master) a new
186
186
  // session would branch off the stale remote default. --auto is best-effort
187
187
  // and only rewrites the local origin/HEAD pointer, never a branch.
188
- await exec("git", ["-C", dest, "remote", "set-head", "origin", "--auto"]);
188
+ await exec("git", ["-C", dest, "remote", "set-head", "origin", "--auto"], { cwd: dest });
189
189
  }
190
190
  catch {
191
191
  // offline / fetch failed — reuse the existing checkout as-is
@@ -200,9 +200,15 @@ export async function cloneOrUpdateRepo(opts) {
200
200
  // session" fail with "destination exists" or "Not a git repository".
201
201
  if (fs.existsSync(dest))
202
202
  fs.rmSync(dest, { recursive: true, force: true });
203
- await exec("git", [...cc, "clone", url, dest], { timeout: 600_000, env });
203
+ // Always give child_process a real cwd. The daemon may still be serving while
204
+ // an npm-global update atomically replaces its install directory; inheriting
205
+ // that now-unlinked cwd makes git abort before it can even process `clone`
206
+ // ("Unable to read current working directory"). The repos root is durable
207
+ // across package updates and was created immediately above.
208
+ await exec("git", [...cc, "clone", url, dest], { cwd: opts.root, timeout: 600_000, env });
204
209
  // Persist the helper config so agent-run git in this clone authenticates too.
205
- await configureRepoCredentialHelper((a) => exec("git", a), dest);
210
+ // These follow-up processes need an explicit cwd for the same reason.
211
+ await configureRepoCredentialHelper((a) => exec("git", a, { cwd: dest }), dest);
206
212
  hardenGitConfigPerms(dest);
207
213
  return dest;
208
214
  }
package/dist/server.js CHANGED
@@ -86,7 +86,7 @@ const repoRoot = path.resolve(__dirname, "..");
86
86
  // running from source. Packaged/release builds may override them (BIVY_ASSET_ROOT,
87
87
  // BIVY_DATA_DIR) so it can write outside the read-only app bundle.
88
88
  const assetRoot = process.env.BIVY_ASSET_ROOT ?? repoRoot;
89
- const appDir = process.env.BIVY_DATA_DIR ?? path.join(repoRoot, ".bivy");
89
+ const appDir = path.resolve(process.env.BIVY_DATA_DIR ?? path.join(repoRoot, ".bivy"));
90
90
  // Load persisted env from cli.json into process.env so config written there —
91
91
  // e.g. connecting a GitHub App (BIVY_GITHUB_APP_ID / BIVY_GITHUB_HOSTED_TASKS /
92
92
  // BIVY_NODE_LABEL) — takes effect on the NEXT restart even if the service
@@ -115,6 +115,13 @@ initSharedDepCache(appDir);
115
115
  // Materialize the git credential helper under the data dir so repo clones can
116
116
  // authenticate without ever writing a token into their remote URL / .git/config.
117
117
  configureGitAuth(appDir);
118
+ // Never leave the daemon parked in its installed package directory. A global
119
+ // npm update atomically replaces that directory while the old process drains;
120
+ // any later child process (notably git's remote/credential helpers) then aborts
121
+ // before running with "Unable to read current working directory". appDir is the
122
+ // durable state root and every workspace/asset path below is already absolute.
123
+ // Anchoring the process itself fixes all subprocess paths, not just `git clone`.
124
+ process.chdir(appDir);
118
125
  // Make `bivy update` and user-scoped npm globals available to agents/tools
119
126
  // launched by the daemon even when the node runs under systemd/launchd with a
120
127
  // minimal PATH. The installer also symlinks ~/.local/bin/bivy, but adding these
@@ -2377,6 +2384,9 @@ const RELAY_COMMANDS = {
2377
2384
  "node.rename"(msg, ctx) {
2378
2385
  const prev = identity.name;
2379
2386
  const name = identity.setName(String(msg.name ?? ""));
2387
+ // Queue routing follows the node name (`bivy/<name>`). Update the live
2388
+ // poller now instead of leaving it on its startup-time label until restart.
2389
+ refreshControlPlaneTaskLabels();
2380
2390
  void advertiseNodeName(name, prev);
2381
2391
  ctx.broadcast({ type: "node.updated", name });
2382
2392
  ctx.reply({ type: "node.updated", name });
@@ -3850,6 +3860,7 @@ async function advertiseNodeName(name, prevName) {
3850
3860
  const accepted = typeof data?.node?.name === "string" ? data.node.name : undefined;
3851
3861
  if (accepted && accepted !== identity.name) {
3852
3862
  identity.setName(accepted);
3863
+ refreshControlPlaneTaskLabels();
3853
3864
  broadcast({ type: "node.updated", name: accepted });
3854
3865
  relay?.sendEvent({ type: "node.updated", name: accepted });
3855
3866
  }
@@ -3859,6 +3870,7 @@ async function advertiseNodeName(name, prevName) {
3859
3870
  // optimistic local change so the node name stays consistent with the account.
3860
3871
  if (prevName && prevName !== identity.name) {
3861
3872
  identity.setName(prevName);
3873
+ refreshControlPlaneTaskLabels();
3862
3874
  broadcast({ type: "node.updated", name: prevName });
3863
3875
  relay?.sendEvent({ type: "node.updated", name: prevName });
3864
3876
  }
@@ -4524,6 +4536,14 @@ async function startGitHubTasksIfConfigured() {
4524
4536
  githubPoller.start();
4525
4537
  }
4526
4538
  let controlPlanePoller;
4539
+ /** Refresh a running queue poller's labels after the node is renamed. */
4540
+ function refreshControlPlaneTaskLabels() {
4541
+ if (!controlPlanePoller)
4542
+ return;
4543
+ const cfg = resolveControlPlaneTaskConfig(loadRelayConfig(appDir), process.env, identity.name);
4544
+ if (cfg)
4545
+ controlPlanePoller.setLabels(cfg.labels);
4546
+ }
4527
4547
  let githubApps;
4528
4548
  const installationIdByRepo = new Map();
4529
4549
  /** Resolve (once) the GitHub Apps configured on this node. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-staging.21",
4
4
  "type": "module",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",