fix: seed worktree dev previews locally (#2693)

This commit is contained in:
Patrick Erichsen
2026-06-16 12:53:59 -07:00
committed by GitHub
parent 7efc6c4555
commit 5471fb280f
7 changed files with 203 additions and 13 deletions
+2 -2
View File
@@ -8,13 +8,13 @@ env = "bun run setup:worktree -- --quiet"
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 }}"
dev = "bun scripts/dev-worktree.ts --detach --seed --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 }}"
dev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --seed --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 }}"
+2 -2
View File
@@ -24,8 +24,8 @@ Keep this section as the command map agents normally need, not a full `package.j
- `bunx convex dev --typecheck=disable` — local Convex backend/function watcher for manual setup.
- `bunx convex codegen` — regenerate `convex/_generated` after Convex API/schema changes.
- `bun run setup:worktree` — link `.env.local` and `.convex` from a usable source worktree into the current worktree. Use `-- --from <path>` or `CLAWHUB_WORKTREE_SOURCE=<path>` when auto-discovery picks the wrong source.
- `bun run dev:worktree` — Worktrunk-managed detached worktree server. Requires `wt` on `PATH`; from that worktree use `wt --yes url` to print the branch URL and `wt --yes stop` to stop it.
- `bun run seed:dev`canonical local seed path; runs worktree setup, waits for local Convex, seeds local fixtures plus the public corpus, and refreshes stats.
- `bun run dev:worktree` — Worktrunk-managed detached worktree server that also seeds local fixtures plus the public corpus once before starting the app when `VITE_CONVEX_URL` and `CONVEX_DEPLOYMENT` are local. Requires `wt` on `PATH`; from that worktree use `wt --yes url` to print the branch URL and `wt --yes stop` to stop it.
- `bun run seed:dev`manual reseed path; runs worktree setup, waits for local Convex, seeds local fixtures plus the public corpus, and refreshes stats.
- `bun run build` — production build (Vite + Nitro).
- `bun run ci:static` — required pre-handoff static gate: peer checks, audit, formatting, lint, and dead-code checks.
- `bun run ci:unit` — Vitest coverage gate; required for source/test PRs unless docs/config-only.
+3 -3
View File
@@ -99,7 +99,7 @@ bun run setup:worktree -- --from /path/to/source/worktree
CLAWHUB_WORKTREE_SOURCE=/path/to/source/worktree bun run setup:worktree
```
`dev:worktree` is the Worktrunk entrypoint. It runs the hooks in `.config/wt.toml`, copies ignored dependencies listed in `.worktreeinclude` when possible, falls back to `bun install` if Vite is missing, and starts detached services on a branch-hashed loopback port. Use `wt --yes url` from the same worktree to print the URL.
`dev:worktree` is the Worktrunk entrypoint. It runs the hooks in `.config/wt.toml`, copies ignored dependencies listed in `.worktreeinclude` when possible, falls back to `bun install` if Vite is missing, seeds local fixtures plus the public corpus once when `VITE_CONVEX_URL` and `CONVEX_DEPLOYMENT` are local, refreshes cached global stats, and starts detached services on a branch-hashed loopback port. Use `wt --yes url` from the same worktree to print the URL.
The detached server writes runtime state under `.codex/runtime/`. Stop it with `wt --yes stop` before removing the worktree.
@@ -121,9 +121,9 @@ one.
Without those workers, local ClawScan and Skill Card jobs stay pending until you
opt in, seed/mock results, or use the production workflows.
### Seed the database
### Reseed the database
Populate local QA fixtures and the committed public corpus so the UI isn't empty:
`dev:worktree` seeds local QA fixtures and the committed public corpus before starting the app when `VITE_CONVEX_URL` points at local Convex and `CONVEX_DEPLOYMENT` is an anonymous/local deployment marker, then records `.codex/runtime/dev-worktree.seeded` so ordinary restarts skip the expensive corpus pass. Remote-backed previews or mismatched deployment markers skip seeding and keep starting. To force the seed path without restarting the preview:
```bash
bun run seed:dev
+113
View File
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
applyLocalDevWorkerToken,
@@ -12,10 +13,22 @@ import {
isRunningPid,
parseArgs,
parseEnv,
shouldSeedLocalData,
shouldStartDevWorkers,
} from "./dev-worktree";
describe("dev-worktree helpers", () => {
it("seeds local fixtures and public corpus when Worktrunk starts dev services", () => {
const config = readFileSync(".config/wt.toml", "utf8");
expect(config).toContain(
'[post-start]\ndev = "bun scripts/dev-worktree.ts --detach --seed --port',
);
expect(config).toContain(
'[aliases]\ndev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --seed --port',
);
});
it("parses env files without treating inline comments as values", () => {
expect(
parseEnv(`
@@ -165,6 +178,106 @@ describe("dev-worktree helpers", () => {
});
});
it("seeds local data only for local Convex URLs", () => {
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
),
).toEqual({
seed: true,
fatal: false,
reason: null,
});
expect(
shouldSeedLocalData(
{ seed: false, seedOnly: false },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
),
).toEqual({
seed: false,
fatal: false,
reason: "--seed was not passed",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"https://example.convex.cloud",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: false,
reason: "VITE_CONVEX_URL is not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: true },
"https://example.convex.cloud",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: true,
reason: "VITE_CONVEX_URL is not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"http://127.0.0.1:3210",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: false,
reason: "CONVEX_DEPLOYMENT is missing or not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: true },
"http://127.0.0.1:3210",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: true,
reason: "CONVEX_DEPLOYMENT is missing or not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
"anonymous:anonymous-agent\nhttp://127.0.0.1:3210",
),
).toEqual({
seed: false,
fatal: false,
reason: "local seed already completed for this Convex deployment",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: true },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
"anonymous:anonymous-agent\nhttp://127.0.0.1:3210",
),
).toEqual({
seed: true,
fatal: false,
reason: null,
});
});
it("does not start dev workers by default for non-local Convex URLs", () => {
expect(shouldStartDevWorkers({ workers: true }, "https://example.convex.cloud")).toEqual({
start: false,
+72 -1
View File
@@ -21,6 +21,7 @@ const REACHABILITY_POLL_MS = 500;
const RUNTIME_DIR = ".codex/runtime";
const DETACHED_PID_FILE = `${RUNTIME_DIR}/dev-worktree.pid`;
const DETACHED_LOG_FILE = `${RUNTIME_DIR}/dev-worktree.log`;
const SEED_SENTINEL_FILE = `${RUNTIME_DIR}/dev-worktree.seeded`;
const LOCAL_DEV_WORKER_TOKEN = "local-dev-worker-token";
const managedChildren = new Set<ChildProcess>();
@@ -81,6 +82,36 @@ export function shouldStartDevWorkers(options: Pick<Options, "workers">, convexU
return { start: true, reason: null };
}
export function shouldSeedLocalData(
options: Pick<Options, "seed" | "seedOnly">,
convexUrl: string,
convexDeployment: string | null | undefined,
completedSeedTarget: string | null = null,
) {
if (!options.seed) return { seed: false, fatal: false, reason: "--seed was not passed" };
if (!isLocalConvexUrl(convexUrl)) {
return { seed: false, fatal: options.seedOnly, reason: "VITE_CONVEX_URL is not local" };
}
if (!isLocalConvexDeployment(convexDeployment)) {
return {
seed: false,
fatal: options.seedOnly,
reason: "CONVEX_DEPLOYMENT is missing or not local",
};
}
if (
!options.seedOnly &&
completedSeedTarget === buildSeedTarget(convexUrl, convexDeployment.trim())
) {
return {
seed: false,
fatal: false,
reason: "local seed already completed for this Convex deployment",
};
}
return { seed: true, fatal: false, reason: null };
}
export function applyLocalDevWorkerToken(env: NodeJS.ProcessEnv) {
env.SECURITY_SCAN_WORKER_TOKEN = LOCAL_DEV_WORKER_TOKEN;
return LOCAL_DEV_WORKER_TOKEN;
@@ -145,6 +176,20 @@ export function isLocalConvexUrl(value: string) {
}
}
export function isLocalConvexDeployment(value: string | null | undefined) {
const deployment = value?.trim();
return (
deployment === "anonymous-agent" ||
deployment?.startsWith("anonymous:") ||
deployment?.startsWith("local:") ||
false
);
}
function buildSeedTarget(convexUrl: string, convexDeployment: string) {
return `${convexDeployment}\n${convexUrl}`;
}
function readDetachedPid() {
if (!existsSync(DETACHED_PID_FILE)) return null;
const raw = readFileSync(DETACHED_PID_FILE, "utf8").trim();
@@ -153,6 +198,19 @@ function readDetachedPid() {
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
}
function readSeedSentinel() {
try {
return readFileSync(SEED_SENTINEL_FILE, "utf8").trim() || null;
} catch {
return null;
}
}
function writeSeedSentinel(convexUrl: string, convexDeployment: string) {
mkdirSync(RUNTIME_DIR, { recursive: true });
writeFileSync(SEED_SENTINEL_FILE, `${buildSeedTarget(convexUrl, convexDeployment)}\n`);
}
export function isRunningPid(pid: number | null) {
if (pid === null || !Number.isSafeInteger(pid) || pid <= 0) return false;
try {
@@ -475,11 +533,23 @@ async function main() {
process.exit(1);
}
const convexDeployment = process.env.CONVEX_DEPLOYMENT;
const seed = shouldSeedLocalData(options, convexUrl, convexDeployment, readSeedSentinel());
if (options.seed && !seed.seed) {
const message = `Skipping local fixtures and public corpus seed: ${seed.reason}.`;
if (seed.fatal) {
console.error(message);
console.error("Manual dev seeding only runs when VITE_CONVEX_URL points at localhost.");
process.exit(1);
}
console.warn(`${message} Continuing without seeding.`);
}
applyLocalConvexEnvForUrl(process.env, convexUrl);
await ensureConvex(convexUrl);
await configureLocalConvexEnv(convexUrl);
if (options.seed) {
if (seed.seed) {
console.log("Seeding local fixtures and public corpus...");
const seedStatus = await runConvexFunctionWhenReady([
"convex",
@@ -499,6 +569,7 @@ async function main() {
"statsMaintenance:updateGlobalStatsAction",
]);
if (statsStatus !== 0) exitAfterStoppingManagedChildren(statsStatus);
writeSeedSentinel(convexUrl, convexDeployment?.trim() ?? "");
}
if (options.seedOnly) {
+8 -3
View File
@@ -10,9 +10,14 @@ read_when:
Local fixture seeding is command-driven by default:
- CLI seeding (`bun run seed:dev`) populates shared catalog fixtures under `@local`, including
skill, plugin, scanner, and moderation fixtures. It also imports the committed public corpus and
refreshes cached global stats. This is the documented local setup path.
- Worktree dev startup (`bun run dev:worktree`) seeds shared catalog fixtures under `@local`,
including skill, plugin, scanner, and moderation fixtures, before starting the local app when
`VITE_CONVEX_URL` points at local Convex and `CONVEX_DEPLOYMENT` is an anonymous/local deployment
marker. It also imports the committed public corpus, refreshes cached global stats, and writes
`.codex/runtime/dev-worktree.seeded` so routine restarts skip the expensive corpus pass. This is
the documented first-run local setup path.
- CLI seeding (`bun run seed:dev`) runs the same seed path manually without starting the preview and
bypasses the first-run sentinel.
- `bun run seed:public-corpus` is the lower-level corpus-only import command. Use it for corpus
fixture work, not as the default local setup command.
- `bun run validate:public-corpus` validates the committed public corpus fixture without seeding.
+3 -2
View File
@@ -58,10 +58,11 @@ The copy step is best effort. If `.convex` is already a symlink to the source wo
## Runtime Contract
`scripts/dev-worktree.ts` loads `.env.local`, checks `VITE_CONVEX_URL`, starts local Convex if it is not reachable, then starts Vite on the requested port. Detached runtime state lives under `.codex/runtime/`:
`scripts/dev-worktree.ts` loads `.env.local`, checks `VITE_CONVEX_URL`, starts local Convex if it is not reachable, optionally seeds local fixtures plus the public corpus once when both `VITE_CONVEX_URL` and `CONVEX_DEPLOYMENT` describe a local target, then starts Vite on the requested port. Worktrunk passes `--seed` for normal `dev:worktree` startup. Detached runtime state lives under `.codex/runtime/`:
- `.codex/runtime/dev-worktree.pid`
- `.codex/runtime/dev-worktree.log`
- `.codex/runtime/dev-worktree.seeded`
Use `wt --yes stop` before removing or recreating a worktree. If a stale pid blocks startup, stop the service and inspect the runtime log before deleting files by hand.
@@ -75,4 +76,4 @@ operator provides `CODEX_HOME`.
## Seeding Contract
`bun run seed:dev` uses the same worktree setup helper and the same local Convex readiness checks as the detached dev server. It must remain the documented default seed command. Lower-level Convex calls and `seed:public-corpus` are recovery or fixture-authoring tools, not the first-run path.
`bun run dev:worktree` is the documented first-run path and seeds before starting the detached app when both the Convex URL and deployment marker are local. Successful automatic seeding records the Convex deployment plus URL in `.codex/runtime/dev-worktree.seeded`, so restarts against the same local backend skip seeding. Remote-backed previews or mismatched deployment markers skip seeding and keep starting. `bun run seed:dev` uses the same worktree setup helper and the same local Convex readiness checks as the detached dev server for manual reseeding, bypasses the sentinel, and remains local-only. Lower-level Convex calls and `seed:public-corpus` are recovery or fixture-authoring tools, not the first-run path.