@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.
- package/dist/worktree-provision.js +104 -0
- package/dist/worktree.js +11 -2
- package/package.json +1 -1
|
@@ -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