diff --git a/README.md b/README.md index 0b47634..8239dfb 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ Optional: local Supabase (`yarn db:start`) and keys for image generation / payme ## Ship mashups (ops CLI) -Do not generate mashups by clicking the website. `yarn generate:mashup` is the publish path: it reuses Style DNA merge, `buildGenerationPrompt`, `executeImageGeneration` (Vercel AI Gateway), sharp variants, the public `generation-images` bucket, and a **published** `generations` row. It does not debit Dodo credits. +Do not generate mashups by clicking the website. `yarn generate:mashup` reuses Style DNA merge, `buildGenerationPrompt`, `executeImageGeneration` (Vercel AI Gateway), sharp variants, the public `generation-images` bucket, and a `generations` row. The default insert is **draft** (not on the public feed, sitemap, or locker). Pass `--publish` only after reviewing the image. It does not debit Dodo credits. ```bash yarn generate:mashup --help ``` -`--dry-run` (or `DRY_RUN=1`) prints the fully built prompt and skips the paid Gateway call, upload, and DB insert. +`--dry-run` (or `DRY_RUN=1`) prints the fully built prompt and skips the paid Gateway call, upload, and DB insert. `--publish` inserts `visibility=published` so the row is listed publicly. Example pairings (live picker slugs; extras are prompt notes, not new catalog nouns): diff --git a/scripts/generate-mashup.ts b/scripts/generate-mashup.ts index 4994b55..57cf6e9 100644 --- a/scripts/generate-mashup.ts +++ b/scripts/generate-mashup.ts @@ -1,6 +1,7 @@ /** - * Ops mashup generator — ships published generations through the production - * pipeline. Do not generate mashups by clicking the website. + * Ops mashup generator — ships generations through the production pipeline. + * Default insert is draft (unlisted). Pass --publish only after review. + * Do not generate mashups by clicking the website. * * Usage: * yarn generate:mashup --help @@ -20,7 +21,7 @@ import { import { generateMashup, type MashupDryRunResult, - type MashupPublishedResult, + type MashupInsertedResult, } from "@/lib/ops/mashup-run"; import { generationVariantObjectPath } from "@/lib/generation-media-url"; @@ -38,16 +39,31 @@ function printDryRun(result: MashupDryRunResult) { console.log(result.prompt); } -function printPublished(result: MashupPublishedResult) { - console.log("--- mashup published ---"); +function printInserted(result: MashupInsertedResult) { + switch (result.visibility) { + case "published": + console.log("--- mashup published ---"); + break; + case "draft": + console.log("--- mashup saved (draft) ---"); + break; + default: { + const _exhaustive: never = result.visibility; + throw new Error(`Unhandled mashup visibility: ${String(_exhaustive)}`); + } + } console.log(`builder: ${result.builder.name} (${result.builder.id})`); console.log(`target: ${result.target.name} (${result.target.id})`); console.log(`id: ${result.id}`); console.log(`slug: ${result.slug}`); + console.log(`visibility: ${result.visibility}`); console.log(`image: ${result.imagePath}`); console.log( `variants: ${generationVariantObjectPath(result.imagePath, "card")}, ${generationVariantObjectPath(result.imagePath, "detail")}, ${generationVariantObjectPath(result.imagePath, "og")}`, ); + if (result.visibility === "draft") { + console.log("Not listed on the public feed, sitemap, or locker. Pass --publish after review."); + } } async function main() { @@ -63,8 +79,8 @@ async function main() { case "dry-run": printDryRun(result); return; - case "published": - printPublished(result); + case "inserted": + printInserted(result); return; default: { const _exhaustive: never = result; diff --git a/src/lib/ops/__tests__/mashup-cli.test.ts b/src/lib/ops/__tests__/mashup-cli.test.ts index 3cc9c93..928f724 100644 --- a/src/lib/ops/__tests__/mashup-cli.test.ts +++ b/src/lib/ops/__tests__/mashup-cli.test.ts @@ -11,6 +11,7 @@ import { assembleMashupPrompt, assertMashupArgs, combineUserExtraDetails, + mashupListingFields, MASHUP_HELP, parseMashupArgs, } from "@/lib/ops/mashup-cli"; @@ -38,6 +39,7 @@ describe("parseMashupArgs", () => { expect(args.extraDetails).toContain("Some assembly required"); expect(args.screenType).toBe("desktop"); expect(args.dryRun).toBe(false); + expect(args.publish).toBe(false); expect(args.help).toBe(false); }); @@ -63,6 +65,16 @@ describe("parseMashupArgs", () => { expect(args.inventedName).toBe("Halo"); expect(args.screenType).toBe("mobile"); expect(args.dryRun).toBe(true); + expect(args.publish).toBe(false); + }); + + it("parses --publish without changing --dry-run", () => { + const args = parseMashupArgs( + ["--builder", "ikea", "--target", "figma", "--publish"], + {}, + ); + expect(args.publish).toBe(true); + expect(args.dryRun).toBe(false); }); it("honors DRY_RUN=1 without hitting the gateway path", () => { @@ -118,11 +130,28 @@ describe("parseMashupArgs", () => { }); }); +describe("mashupListingFields", () => { + it("defaults to draft so the row is not publicly listed", () => { + expect(mashupListingFields(false)).toEqual({ + visibility: "draft", + moderation_status: "visible", + }); + }); + + it("maps --publish to published + visible", () => { + expect(mashupListingFields(true)).toEqual({ + visibility: "published", + moderation_status: "visible", + }); + }); +}); + describe("MASHUP_HELP", () => { it("documents flags and the four example pairings", () => { expect(MASHUP_HELP).toContain("--builder"); expect(MASHUP_HELP).toContain("--target"); expect(MASHUP_HELP).toContain("--dry-run"); + expect(MASHUP_HELP).toContain("--publish"); expect(MASHUP_HELP).toContain("SKISSA"); expect(MASHUP_HELP).toContain("Halo"); expect(MASHUP_HELP).toContain("Perch"); diff --git a/src/lib/ops/mashup-cli.ts b/src/lib/ops/mashup-cli.ts index 0be4fd6..0a4219f 100644 --- a/src/lib/ops/mashup-cli.ts +++ b/src/lib/ops/mashup-cli.ts @@ -8,10 +8,28 @@ export type MashupCliArgs = { inventedName: string; screenType: string | null; dryRun: boolean; + publish: boolean; help: boolean; creatorId: string | null; }; +/** Listing columns written on CLI insert. Matches generations_visibility_ck / feed filters. */ +export type MashupListingFields = { + visibility: "draft" | "published"; + moderation_status: "visible"; +}; + +/** + * Default insert is unlisted (`draft`). `--publish` is the only way to land a + * public feed/sitemap/locker row (`published` + `visible`). + */ +export function mashupListingFields(publish: boolean): MashupListingFields { + return { + visibility: publish ? "published" : "draft", + moderation_status: "visible", + }; +} + const FLAG_ALIASES: Record = { "-b": "--builder", "-t": "--target", @@ -29,7 +47,9 @@ Ops CLI for shipping mashups. Do not generate by clicking the website. Reuses the production path: Style DNA merge (mergeCompanyPair) → buildGenerationPrompt → executeImageGeneration (Vercel AI Gateway, default openai/gpt-image-2) → sharp card/detail/og variants → upload to -the public generation-images bucket → insert a published generations row. +the public generation-images bucket → insert a generations row. +Default insert is visibility=draft (not on the public feed, sitemap, or +locker). Pass --publish only after the image has been reviewed. Does not debit Dodo credits and does not require a logged-in session. Options: @@ -39,7 +59,8 @@ Options: --invented-name Invented on-screen product name --screen-type mobile | desktop (default: desktop, same as /api/generate) --dry-run Print the fully built prompt; skip Gateway, upload, DB insert - --creator-id auth.users UUID that owns the published row + --publish Insert visibility=published (public feed). Default is draft + --creator-id auth.users UUID that owns the row -h, --help Show this help Env: @@ -71,6 +92,9 @@ Example pairings (picker brands already live; extras are prompt notes): Dry-run (no paid image call): yarn generate:mashup --builder ikea --target figma --dry-run + +Publish after review (same flags, plus --publish): + yarn generate:mashup --builder ikea --target figma --invented-name SKISSA --publish `; function envDryRun(env: Record): boolean { @@ -102,6 +126,7 @@ export function parseMashupArgs( inventedName: "", screenType: null, dryRun: envDryRun(env), + publish: false, help: false, creatorId: null, }; @@ -133,6 +158,9 @@ export function parseMashupArgs( case "--dry-run": out.dryRun = true; break; + case "--publish": + out.publish = true; + break; case "--builder": out.builder = takeValue(); break; diff --git a/src/lib/ops/mashup-run.ts b/src/lib/ops/mashup-run.ts index 33b284b..6fac902 100644 --- a/src/lib/ops/mashup-run.ts +++ b/src/lib/ops/mashup-run.ts @@ -13,7 +13,9 @@ import { executeImageGeneration } from "@/lib/generation/execute-image"; import { assembleMashupPrompt, combineUserExtraDetails, + mashupListingFields, type MashupCliArgs, + type MashupListingFields, } from "@/lib/ops/mashup-cli"; import { mergeCompanyPair } from "@/lib/prompt/merge-company-pair"; import { normalizeRenderMode } from "@/lib/screen-type"; @@ -35,8 +37,9 @@ export type MashupDryRunResult = { prompt: string; }; -export type MashupPublishedResult = { - kind: "published"; +export type MashupInsertedResult = { + kind: "inserted"; + visibility: MashupListingFields["visibility"]; builder: ProfileLookup; target: ProfileLookup; screenType: string; @@ -48,7 +51,7 @@ export type MashupPublishedResult = { imagePath: string; }; -export type MashupGenerateResult = MashupDryRunResult | MashupPublishedResult; +export type MashupGenerateResult = MashupDryRunResult | MashupInsertedResult; function gatewayImageModel(): string { return process.env.AI_GATEWAY_IMAGE_MODEL?.trim() || DEFAULT_GATEWAY_IMAGE_MODEL; @@ -85,7 +88,7 @@ async function resolveOpsCreatorId(explicit: string | null): Promise { } throw new Error( - "Could not resolve a creator for the published row. Pass --creator-id or set GENERATION_OPS_CREATOR_ID. No credits are debited.", + "Could not resolve a creator for the row. Pass --creator-id or set GENERATION_OPS_CREATOR_ID. No credits are debited.", ); } @@ -102,7 +105,7 @@ async function resolvePair( return { builder, target }; } -async function insertPublishedGeneration(args: { +async function insertGenerationRow(args: { creatorId: string; builderName: string; targetName: string; @@ -110,6 +113,7 @@ async function insertPublishedGeneration(args: { prompt: string; screenType: string; vibeTags: string[]; + listing: MashupListingFields; }): Promise<{ id: number; slug: string; objectPath: string }> { const supabase = createSupabaseServiceClient(); const baseSlug = makeGenerationSlugSnippet({ @@ -137,8 +141,8 @@ async function insertPublishedGeneration(args: { extra_details: args.extraDetails, generated_prompt: args.prompt, image_path: objectPath, - visibility: "published", - moderation_status: "visible", + visibility: args.listing.visibility, + moderation_status: args.listing.moderation_status, status: "queued", image_ready: false, }) @@ -158,8 +162,9 @@ async function insertPublishedGeneration(args: { } /** - * Build the production prompt and either return it (--dry-run) or publish - * through executeImageGeneration (no Dodo debit). + * Build the production prompt and either return it (--dry-run) or insert + * through executeImageGeneration (no Dodo debit). Default listing is draft; + * pass --publish after review to insert a public row. */ export async function generateMashup( args: MashupCliArgs, @@ -195,7 +200,8 @@ export async function generateMashup( assertAiGatewayConfigured(); const creatorId = await resolveOpsCreatorId(args.creatorId); - const inserted = await insertPublishedGeneration({ + const listing = mashupListingFields(args.publish); + const inserted = await insertGenerationRow({ creatorId, builderName: merged.builder, targetName: merged.target, @@ -203,6 +209,7 @@ export async function generateMashup( prompt, screenType, vibeTags: merged.builderDefaultVibeTags, + listing, }); try { @@ -235,7 +242,8 @@ export async function generateMashup( }); return { - kind: "published", + kind: "inserted", + visibility: listing.visibility, builder, target, screenType,