@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.
- package/dist/control-plane-tasks.js +17 -0
- package/dist/git-auth.js +7 -1
- package/dist/repo-workspace.js +19 -13
- package/dist/server.js +21 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/repo-workspace.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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. */
|