@bivy/bivy 0.10.1-staging.351 → 0.10.1-staging.352

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.
@@ -24,6 +24,7 @@
24
24
  import crypto from "node:crypto";
25
25
  import fs from "node:fs";
26
26
  import path from "node:path";
27
+ import { spawn, spawnSync } from "node:child_process";
27
28
  import { detectCloneStrategy, cloneDir } from "./harness/cow-clone.js";
28
29
  // Order within `lockfiles` is the fingerprint precedence (first present wins).
29
30
  const ECOSYSTEMS = [
@@ -92,6 +93,109 @@ export function planCowProvision(opts) {
92
93
  }
93
94
  return plan;
94
95
  }
96
+ // Precedence order: the FIRST manager whose lockfile is present wins per `dir`.
97
+ export const INSTALL_MANAGERS = [
98
+ { lockfile: "pnpm-lock.yaml", dir: "node_modules", command: "pnpm", args: ["install", "--frozen-lockfile"] },
99
+ { lockfile: "package-lock.json", dir: "node_modules", command: "npm", args: ["ci"] },
100
+ { lockfile: "npm-shrinkwrap.json", dir: "node_modules", command: "npm", args: ["ci"] },
101
+ { lockfile: "yarn.lock", dir: "node_modules", command: "yarn", args: ["install", "--frozen-lockfile"] },
102
+ { lockfile: "bun.lockb", dir: "node_modules", command: "bun", args: ["install"] },
103
+ { lockfile: "Cargo.lock", dir: "target", command: "cargo", args: ["fetch"] },
104
+ { lockfile: "uv.lock", dir: ".venv", command: "uv", args: ["sync"] },
105
+ { lockfile: "poetry.lock", dir: ".venv", command: "poetry", args: ["install"] },
106
+ { lockfile: "Pipfile.lock", dir: ".venv", command: "pipenv", args: ["install"] },
107
+ { lockfile: "requirements.txt", dir: ".venv", command: "pip", args: ["install", "-r", "requirements.txt"] },
108
+ { lockfile: "go.mod", dir: "", command: "go", args: ["mod", "download"] },
109
+ ];
110
+ function fileExists(p) {
111
+ try {
112
+ return fs.statSync(p).isFile();
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Plan the installs a worktree needs to provision its deps from scratch: for each
120
+ * ecosystem, the first manager (by precedence) whose lockfile is present AND whose
121
+ * target dir isn't already populated. Pure filesystem logic, so it is fully
122
+ * unit-testable. Already-provisioned dirs (a CoW clone landed, or the agent ran an
123
+ * install already) are skipped, so this is safe to re-run.
124
+ */
125
+ export function planInstallProvision(worktreePath) {
126
+ const plan = [];
127
+ const claimed = new Set();
128
+ for (const m of INSTALL_MANAGERS) {
129
+ if (m.dir && claimed.has(m.dir))
130
+ continue; // a higher-precedence manager already owns this dir
131
+ if (!fileExists(path.join(worktreePath, m.lockfile)))
132
+ continue;
133
+ if (m.dir && isDir(path.join(worktreePath, m.dir))) {
134
+ claimed.add(m.dir);
135
+ continue;
136
+ } // already provisioned
137
+ plan.push({ dir: m.dir, command: m.command, args: m.args, lockfile: m.lockfile });
138
+ if (m.dir)
139
+ claimed.add(m.dir);
140
+ }
141
+ return plan;
142
+ }
143
+ /**
144
+ * Provision a worktree's deps by running each detected manager's install
145
+ * (cross-Machine fallback when CoW found no sibling). Opt-in via
146
+ * BIVY_WORKTREE_AUTO_INSTALL. Best-effort: a missing manager is skipped and one
147
+ * failing install never blocks the others (or the caller). The default runner
148
+ * spawns real processes; tests inject a fake.
149
+ */
150
+ export async function provisionDepsByInstall(opts, runner = defaultInstallRunner()) {
151
+ if (!process.env.BIVY_WORKTREE_AUTO_INSTALL)
152
+ return { strategy: "disabled", ran: [], skipped: [] };
153
+ const ran = [];
154
+ const skipped = [];
155
+ for (const item of planInstallProvision(opts.worktreePath)) {
156
+ if (!runner.has(item.command)) {
157
+ skipped.push({ command: item.command, reason: "not on PATH" });
158
+ opts.log?.(`[worktree] provision skipped: ${item.command} not installed (${item.lockfile})`);
159
+ continue;
160
+ }
161
+ try {
162
+ opts.log?.(`[worktree] provisioning ${item.dir || item.command} via ${item.command} ${item.args.join(" ")}`);
163
+ await runner.run(item, opts.worktreePath);
164
+ ran.push(item.command);
165
+ }
166
+ catch (error) {
167
+ skipped.push({ command: item.command, reason: error instanceof Error ? error.message : String(error) });
168
+ opts.log?.(`[worktree] provision of ${item.dir || item.command} failed: ${error instanceof Error ? error.message : String(error)}`);
169
+ }
170
+ }
171
+ return { strategy: "ran", ran, skipped };
172
+ }
173
+ function defaultInstallRunner() {
174
+ return {
175
+ has(command) {
176
+ const probe = process.platform === "win32" ? "where" : "command";
177
+ const args = process.platform === "win32" ? [command] : ["-v", command];
178
+ const result = spawnSync(probe, args, { stdio: "ignore", shell: process.platform === "win32" });
179
+ return result.status === 0;
180
+ },
181
+ run(item, cwd) {
182
+ return new Promise((resolve, reject) => {
183
+ const child = spawn(item.command, item.args, { cwd, stdio: "ignore" });
184
+ // Bound each install so a wedged manager can't provision forever.
185
+ const timer = setTimeout(() => { child.kill("SIGKILL"); reject(new Error("install timed out")); }, INSTALL_TIMEOUT_MS);
186
+ child.on("error", (err) => { clearTimeout(timer); reject(err); });
187
+ child.on("exit", (code) => {
188
+ clearTimeout(timer);
189
+ if (code === 0)
190
+ resolve();
191
+ else
192
+ reject(new Error(`exit ${code}`));
193
+ });
194
+ });
195
+ },
196
+ };
197
+ }
198
+ const INSTALL_TIMEOUT_MS = 15 * 60 * 1000;
95
199
  /**
96
200
  * Opportunistically CoW-clone installed dirs into a freshly-created worktree from
97
201
  * a sibling of the same repo. Gated on BIVY_WORKTREE_COW_CLONE and on the
package/dist/worktree.js CHANGED
@@ -4,7 +4,7 @@ import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
- import { cowProvisionDeps } from "./worktree-provision.js";
7
+ import { cowProvisionDeps, provisionDepsByInstall } from "./worktree-provision.js";
8
8
  const exec = promisify(execFile);
9
9
  /** Slugify arbitrary text into a safe git branch / directory segment. */
10
10
  export function branchSlug(input) {
@@ -103,7 +103,16 @@ export async function createWorktree(opts) {
103
103
  // target, .venv) via copy-on-write when the filesystem supports it, so a
104
104
  // second worktree of this repo costs ~its diff instead of a full re-install.
105
105
  // Opt-in (BIVY_WORKTREE_COW_CLONE), CoW-only, lockfile-matched, best-effort.
106
- cowProvisionDeps({ worktreePath: wtPath, worktreesRoot: root, log: (m) => console.log(m) });
106
+ const cow = cowProvisionDeps({ worktreePath: wtPath, worktreesRoot: root, log: (m) => console.log(m) });
107
+ // Cross-Machine fallback (1D): when CoW cloned nothing (fresh destination with
108
+ // no sibling — the exact cross-Machine move case), provision the deps by running
109
+ // the detected managers' installs so the agent doesn't hit a cold tree. Opt-in
110
+ // (BIVY_WORKTREE_AUTO_INSTALL), background (never blocks worktree creation),
111
+ // best-effort. `planInstallProvision` skips any dir a CoW clone already filled.
112
+ if (cow.cloned.length === 0) {
113
+ void provisionDepsByInstall({ worktreePath: wtPath, log: (m) => console.log(m) })
114
+ .catch((error) => console.log(`[worktree] auto-provision error: ${error instanceof Error ? error.message : String(error)}`));
115
+ }
107
116
  return { path: wtPath, branch, repoRoot };
108
117
  }
109
118
  /** True if `ref` (a fully-qualified ref) resolves in the repo. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.351",
3
+ "version": "0.10.1-staging.352",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",