From a6785f74eeafc485415662512b7935dfdb79bdc6 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 22 Sep 2026 08:30:20 +0200 Subject: [PATCH] chore(plan): add targeted hosted schema command --- README.md | 8 +++++--- package.json | 1 + scripts/apply-plan-schema.ts | 23 ++++++++++++++++++++++ scripts/plan-schema.ts | 24 ++++++++++++++++++++++ scripts/setup-remote.ts | 29 ++++----------------------- src/lib/planning/schema.test.ts | 35 +++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 scripts/apply-plan-schema.ts create mode 100644 scripts/plan-schema.ts create mode 100644 src/lib/planning/schema.test.ts diff --git a/README.md b/README.md index 2018b75..ce41f77 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,11 @@ in `projects.plan_data`, separate from `cards.start_date` (which also controls commit linking). Client plan writes verify the signed-in account and project ownership before updating the project through the admin connection. -After deploying the new code, run the schema command above once to add the -optional `projects.plan_data` JSON field. Existing projects start with an empty -draft; no existing card dates are overwritten. +Before using the new planner on the hosted instance, load `.env.local` as shown +above and run `npm run setup-plan-schema`. This targeted command only adds the +optional `projects.plan_data` JSON field; it does not run the full setup script +or rewrite existing records. Existing projects start with an empty draft, and +no existing card dates are overwritten. ## Specs and plans diff --git a/package.json b/package.json index ce44218..40c8fc5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test:all": "vitest run", "seed": "tsx scripts/seed-demo.ts", "setup-remote": "tsx scripts/setup-remote.ts", + "setup-plan-schema": "tsx scripts/apply-plan-schema.ts", "lint": "next lint" }, "dependencies": { diff --git a/scripts/apply-plan-schema.ts b/scripts/apply-plan-schema.ts new file mode 100644 index 0000000..32d3219 --- /dev/null +++ b/scripts/apply-plan-schema.ts @@ -0,0 +1,23 @@ +/** Apply only the Plan field to the shared PocketBase instance. */ +import PocketBase from "pocketbase"; + +import { ensureProjectPlanDataField } from "./plan-schema"; + +async function main() { + const url = process.env.NEXT_PUBLIC_PB_URL; + const email = process.env.PB_SUPERUSER_EMAIL; + const password = process.env.PB_SUPERUSER_PASSWORD; + if (!url || !email || !password) { + throw new Error("NEXT_PUBLIC_PB_URL and PocketBase superuser credentials are required"); + } + const pb = new PocketBase(url); + pb.autoCancellation(false); + await pb.collection("_superusers").authWithPassword(email, password); + const added = await ensureProjectPlanDataField(pb); + console.log(added ? "Added projects.plan_data" : "projects.plan_data already exists"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/plan-schema.ts b/scripts/plan-schema.ts new file mode 100644 index 0000000..aa9db2d --- /dev/null +++ b/scripts/plan-schema.ts @@ -0,0 +1,24 @@ +import type PocketBase from "pocketbase"; + +/** Add the optional JSON field used by the collaborative project planner. */ +export async function ensureProjectPlanDataField(pb: PocketBase): Promise { + const projects = await pb.collections.getOne("projects"); + if (projects.fields.some((field: { name: string }) => field.name === "plan_data")) { + return false; + } + await pb.collections.update(projects.id, { + fields: [ + ...(projects.fields as unknown[]), + { + name: "plan_data", + type: "json", + required: false, + maxSize: 400_000, + presentable: false, + hidden: false, + system: false, + }, + ], + }); + return true; +} diff --git a/scripts/setup-remote.ts b/scripts/setup-remote.ts index e99fff1..7c09085 100644 --- a/scripts/setup-remote.ts +++ b/scripts/setup-remote.ts @@ -16,6 +16,7 @@ */ import PocketBase from "pocketbase"; +import { ensureProjectPlanDataField } from "./plan-schema"; const PB_URL = process.env.NEXT_PUBLIC_PB_URL ?? @@ -182,30 +183,6 @@ async function ensureProjectIngestFields(pb: PocketBase): Promise { } } -/** One project JSON record keeps plan submission and approval together. */ -async function ensureProjectPlanDataField(pb: PocketBase): Promise { - const projects = await pb.collections.getOne("projects"); - if (projects.fields.some((field: { name: string }) => field.name === "plan_data")) { - console.log(" = projects.plan_data already present"); - return; - } - await pb.collections.update(projects.id, { - fields: [ - ...(projects.fields as unknown[]), - { - name: "plan_data", - type: "json", - required: false, - maxSize: 400_000, - presentable: false, - hidden: false, - system: false, - }, - ], - }); - console.log(" + projects field: plan_data"); -} - /** * Add the `summary`, `summary_version`, `summary_attempts` fields to commits. * @@ -1578,7 +1555,9 @@ async function main(): Promise { // Add the project/prototype kind field for Phase 6. await ensureProjectKindField(pb); await ensureProjectBackgroundMdField(pb); - await ensureProjectPlanDataField(pb); + console.log((await ensureProjectPlanDataField(pb)) + ? " + projects field: plan_data" + : " = projects.plan_data already present"); // Add the per-commit summary fields used by Phase 3b's AI one-liner job. await ensureCommitSummaryFields(pb); diff --git a/src/lib/planning/schema.test.ts b/src/lib/planning/schema.test.ts new file mode 100644 index 0000000..45a849b --- /dev/null +++ b/src/lib/planning/schema.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it, vi } from "vitest"; + +import { ensureProjectPlanDataField } from "../../../scripts/plan-schema"; + +describe("targeted plan schema update", () => { + it("adds only the optional project plan field", async () => { + const update = vi.fn(async () => ({})); + const pb = { + collections: { + getOne: vi.fn(async () => ({ id: "projects-id", fields: [{ name: "name", type: "text" }] })), + update, + }, + }; + expect(await ensureProjectPlanDataField(pb as never)).toBe(true); + expect(update).toHaveBeenCalledOnce(); + expect(update).toHaveBeenCalledWith("projects-id", { + fields: [ + { name: "name", type: "text" }, + expect.objectContaining({ name: "plan_data", type: "json", required: false }), + ], + }); + }); + + it("does nothing when the field already exists", async () => { + const update = vi.fn(); + const pb = { + collections: { + getOne: vi.fn(async () => ({ id: "projects-id", fields: [{ name: "plan_data" }] })), + update, + }, + }; + expect(await ensureProjectPlanDataField(pb as never)).toBe(false); + expect(update).not.toHaveBeenCalled(); + }); +});