mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
fix: seed worktree dev previews locally (#2693)
This commit is contained in:
+2
-2
@@ -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 }}"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user