chore: route worktree startup through Worktrunk (#2297)

* fix: add worktree preflight checks

* fix: remove worktree preflight spec note

* chore: compare worktrunk worktree startup

* fix: stop worktrunk dev process group

* chore: route worktree startup through worktrunk

* chore: keep dev seeding as project command

* chore: clarify worktrunk worktree script boundaries

* fix: keep worktree CI green after main merge
This commit is contained in:
Patrick Erichsen
2026-05-18 14:02:14 -07:00
committed by GitHub
parent 7cf16ebb28
commit 2aa2a449e5
7 changed files with 65 additions and 88 deletions
+20
View File
@@ -0,0 +1,20 @@
[list]
url = "http://127.0.0.1:{{ (repo ~ '-' ~ branch) | hash_port }}"
[[pre-start]]
env = "bun run setup:worktree -- --quiet"
[[pre-start]]
deps = "wt step copy-ignored || true; test -x node_modules/.bin/vite || bun install"
[post-start]
dev = "bun scripts/dev-worktree.ts --detach --port {{ (repo ~ '-' ~ branch) | hash_port }}"
[pre-remove]
dev = "if test -f .codex/runtime/dev-worktree.pid; then pid=$(cat .codex/runtime/dev-worktree.pid); kill -TERM -$pid 2>/dev/null || kill $pid 2>/dev/null || true; rm -f .codex/runtime/dev-worktree.pid; fi"
[aliases]
dev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --port {{ (repo ~ '-' ~ branch) | hash_port }}"
setup = "wt --yes hook pre-start"
stop = "if test -f .codex/runtime/dev-worktree.pid; then pid=$(cat .codex/runtime/dev-worktree.pid); kill -TERM -$pid 2>/dev/null || kill $pid 2>/dev/null || true; rm -f .codex/runtime/dev-worktree.pid; fi"
url = "echo http://127.0.0.1:{{ (repo ~ '-' ~ branch) | hash_port }}"
+1
View File
@@ -0,0 +1 @@
node_modules/
+3 -3
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "clawhub",
@@ -141,6 +142,7 @@
"dompurify": "3.4.1",
"next": "16.2.6",
"postcss": "8.5.12",
"ws": "8.20.1",
},
"packages": {
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
@@ -1379,7 +1381,7 @@
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
@@ -1473,8 +1475,6 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tanstack/devtools-event-bus/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+3 -2
View File
@@ -35,7 +35,7 @@
"deadcode:files": "bunx knip@6.8.0 --config knip.config.ts --production --no-progress --reporter compact --files --no-config-hints",
"deadcode:knip": "bun run deadcode:files && bun run deadcode:dependencies && bun run deadcode:exports",
"dev": "bun --bun vite dev --port 3000",
"dev:worktree": "bun run setup:worktree -- --quiet && bun scripts/dev-worktree.ts",
"dev:worktree": "wt --yes dev",
"docs:list": "bun scripts/docs-list.ts",
"docs:run": "bun scripts/docs-run.ts",
"eval:clawscan:security-signals": "bun scripts/eval/clawscan-security-signals.ts",
@@ -143,6 +143,7 @@
"overrides": {
"dompurify": "3.4.1",
"next": "16.2.6",
"postcss": "8.5.12"
"postcss": "8.5.12",
"ws": "8.20.1"
}
}
+14 -25
View File
@@ -2,11 +2,11 @@ import { describe, expect, it } from "vitest";
import {
buildForegroundArgs,
buildEnvFileCandidates,
buildViteArgs,
isConvexFunctionUnavailableOutput,
isRunningPid,
parseArgs,
parseEnv,
parseGitWorktreeList,
} from "./dev-worktree";
describe("dev-worktree helpers", () => {
@@ -26,7 +26,7 @@ describe("dev-worktree helpers", () => {
});
});
it("discovers the primary worktree env file after the current checkout", () => {
it("uses only the local checkout env unless an env file is explicit", () => {
expect(
buildEnvFileCandidates({
explicit: null,
@@ -37,16 +37,6 @@ describe("dev-worktree helpers", () => {
"/tmp/worktrees/other-feature",
],
}),
).toEqual([".env.local", "/Users/me/Git/openclaw/clawhub/.env.local"]);
});
it("does not scan every sibling worktree for env files", () => {
expect(
buildEnvFileCandidates({
explicit: null,
cwd: "/tmp/worktrees/feature",
worktrees: ["/tmp/worktrees/feature", "/tmp/worktrees/other-feature"],
}),
).toEqual([".env.local"]);
});
@@ -60,19 +50,6 @@ describe("dev-worktree helpers", () => {
).toEqual(["/secure/shared.env"]);
});
it("parses git worktree porcelain output", () => {
expect(
parseGitWorktreeList(`worktree /Users/me/Git/openclaw/clawhub
HEAD abc123
branch refs/heads/main
worktree /tmp/worktrees/feature
HEAD def456
branch refs/heads/feature
`),
).toEqual(["/Users/me/Git/openclaw/clawhub", "/tmp/worktrees/feature"]);
});
it("recognizes Convex functions that are not queryable yet", () => {
expect(
isConvexFunctionUnavailableOutput(`
@@ -96,6 +73,18 @@ branch refs/heads/feature
expect(buildForegroundArgs(["--detach", "--port", "3999"])).toEqual(["--port", "3999"]);
});
it("binds Vite to the same loopback host advertised by Worktrunk", () => {
expect(buildViteArgs("3999")).toEqual([
"--bun",
"vite",
"dev",
"--host",
"127.0.0.1",
"--port",
"3999",
]);
});
it("treats invalid detached pid file values as not running", () => {
expect(isRunningPid(null)).toBe(false);
expect(isRunningPid(0)).toBe(false);
+23 -35
View File
@@ -59,6 +59,10 @@ export function buildForegroundArgs(argv: string[]) {
return argv.filter((arg) => arg !== "--detach");
}
export function buildViteArgs(port: string) {
return ["--bun", "vite", "dev", "--host", "127.0.0.1", "--port", port];
}
function readDetachedPid() {
if (!existsSync(DETACHED_PID_FILE)) return null;
const raw = readFileSync(DETACHED_PID_FILE, "utf8").trim();
@@ -77,43 +81,19 @@ export function isRunningPid(pid: number | null) {
}
}
export function parseGitWorktreeList(text: string) {
return text
.split(/\r?\n/)
.filter((line) => line.startsWith("worktree "))
.map((line) => line.slice("worktree ".length).trim())
.filter(Boolean);
}
function listGitWorktrees() {
const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
cwd: process.cwd(),
encoding: "utf8",
});
if (result.status !== 0 || typeof result.stdout !== "string") return [];
return parseGitWorktreeList(result.stdout);
}
export function buildEnvFileCandidates(options: {
explicit: string | null;
cwd: string;
worktrees?: string[];
}) {
if (options.explicit) return [options.explicit];
const primaryWorktree = options.worktrees?.[0];
return [
...DEFAULT_ENV_SOURCES,
...(primaryWorktree && resolve(primaryWorktree) !== resolve(options.cwd)
? [`${primaryWorktree}/.env.local`]
: []),
];
return DEFAULT_ENV_SOURCES;
}
function findEnvFile(explicit: string | null) {
const candidates = buildEnvFileCandidates({
explicit,
cwd: process.cwd(),
worktrees: explicit ? [] : listGitWorktrees(),
});
return candidates
.map((candidate) => resolve(candidate))
@@ -173,7 +153,11 @@ async function isReachable(url: string) {
}
}
function runSync(command: string, args: string[], extraEnv: Record<string, string | undefined>) {
export function runSync(
command: string,
args: string[],
extraEnv: Record<string, string | undefined>,
) {
return (
spawnSync(command, args, {
cwd: process.cwd(),
@@ -237,6 +221,7 @@ async function runConvexFunctionWhenReady(args: string[]) {
function spawnManaged(command: string, args: string[]) {
const child = spawn(command, args, {
cwd: process.cwd(),
detached: process.platform !== "win32",
env: process.env,
stdio: "inherit",
});
@@ -247,7 +232,16 @@ function spawnManaged(command: string, args: string[]) {
function stopManagedChildren() {
for (const child of managedChildren) {
if (!child.killed) child.kill("SIGTERM");
if (child.killed) continue;
if (process.platform !== "win32" && child.pid) {
try {
process.kill(-child.pid, "SIGTERM");
continue;
} catch {
// Fall through to the direct child signal below.
}
}
child.kill("SIGTERM");
}
}
@@ -337,7 +331,7 @@ async function main() {
const envFile = findEnvFile(options.envFile);
if (!envFile) {
console.error(
"Could not find .env.local in this checkout or the primary git worktree. Pass --env-file <path> or set CLAWHUB_ENV_FILE to a shared local env file.",
"Could not find .env.local in this checkout. Run bun run setup:worktree, pass --env-file <path>, or set CLAWHUB_ENV_FILE.",
);
process.exit(1);
}
@@ -350,12 +344,6 @@ async function main() {
process.exit(1);
}
if (!existsSync("node_modules/.bin/vite")) {
console.log("Installing dependencies for this worktree...");
const installStatus = runSync("bun", ["install"], {});
if (installStatus !== 0) process.exit(installStatus);
}
await ensureConvex(convexUrl);
if (options.seed) {
@@ -387,7 +375,7 @@ async function main() {
console.log(`Starting ClawHub from ${process.cwd()}`);
console.log(`Using env file: ${envFile}`);
const vite = spawnManaged("bun", ["--bun", "vite", "dev", "--port", options.port]);
const vite = spawnManaged("bun", buildViteArgs(options.port));
process.exit(await waitForExit(vite));
}
+1 -23
View File
@@ -210,37 +210,15 @@ function linkFromSource(name: string, sourcePath: string, force: boolean) {
return true;
}
function copyOnWriteDirectory(name: string, sourceRoot: string, quiet: boolean) {
const target = resolve(process.cwd(), name);
if (existsSync(target)) return;
const source = resolve(sourceRoot, name);
if (!existsSync(source)) return;
const cpArgs = process.platform === "darwin" ? ["-cR", source, target] : ["-a", source, target];
const result = spawnSync("cp", cpArgs, { stdio: quiet ? "ignore" : "inherit" });
if (result.status !== 0) {
if (!quiet) console.log(`Copy-on-write clone failed for ${name}; running bun install instead.`);
spawnSync("bun", ["install"], { stdio: "inherit" });
}
}
function main() {
const options = parseArgs(process.argv.slice(2));
const source = findSource(options);
linkFromSource(".env.local", resolve(source.path, ".env.local"), options.force);
linkFromSource(".convex", resolve(source.path, ".convex"), options.force);
copyOnWriteDirectory("node_modules", source.path, options.quiet);
if (!existsSync("node_modules/.bin/vite")) {
if (!options.quiet) console.log("Installing dependencies for this worktree...");
const result = spawnSync("bun", ["install"], { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status ?? 1);
}
if (!options.quiet) {
console.log(`Worktree setup complete using ${source.path}`);
console.log(`Worktree env setup complete using ${source.path}`);
}
}