|
1 | 1 | import { execFile } from "node:child_process"; |
2 | 2 | import { mkdir, writeFile } from "node:fs/promises"; |
3 | 3 | import { dirname, resolve } from "node:path"; |
| 4 | +import { pathToFileURL } from "node:url"; |
4 | 5 | import { promisify } from "node:util"; |
5 | 6 | import type { PromoData } from "../src/assets/data/promos/types"; |
6 | 7 |
|
@@ -105,6 +106,11 @@ export type CampaignBundle = { |
105 | 106 | ignoredEntries: string[]; |
106 | 107 | }; |
107 | 108 |
|
| 109 | +type ExistingCampaignPromos = { |
| 110 | + bannerPromos: Record<string, PromoData>; |
| 111 | + videoPromos: Record<string, PromoData>; |
| 112 | +}; |
| 113 | + |
108 | 114 | type ClaudeCliConfig = { command: string; model?: string }; |
109 | 115 | type ApiConfig = { apiKey: string; apiUrl: string; model: string }; |
110 | 116 | type LlmBackend = |
@@ -572,6 +578,48 @@ export function buildCampaignBundle( |
572 | 578 | return { bannerPromos, videoPromos, summary, ignoredEntries }; |
573 | 579 | } |
574 | 580 |
|
| 581 | +export function mergeCampaignBundleAdditively( |
| 582 | + existing: ExistingCampaignPromos, |
| 583 | + fresh: CampaignBundle, |
| 584 | + today = new Date().toISOString().slice(0, 10), |
| 585 | +): CampaignBundle { |
| 586 | + const bannerPromos = { |
| 587 | + ...existing.bannerPromos, |
| 588 | + ...fresh.bannerPromos, |
| 589 | + }; |
| 590 | + const videoPromos = { |
| 591 | + ...existing.videoPromos, |
| 592 | + ...fresh.videoPromos, |
| 593 | + }; |
| 594 | + const summary = `${Object.keys(bannerPromos).length} banner + ${Object.keys(videoPromos).length} video promo(s) after additive merge as of ${today}`; |
| 595 | + |
| 596 | + return { |
| 597 | + bannerPromos, |
| 598 | + videoPromos, |
| 599 | + summary, |
| 600 | + ignoredEntries: fresh.ignoredEntries, |
| 601 | + }; |
| 602 | +} |
| 603 | + |
| 604 | +async function loadExistingCampaignPromos( |
| 605 | + outputPath: string, |
| 606 | +): Promise<ExistingCampaignPromos> { |
| 607 | + try { |
| 608 | + const moduleHref = `${pathToFileURL(outputPath).href}?t=${Date.now()}`; |
| 609 | + const imported = (await import(moduleHref)) as { |
| 610 | + campaignBannerPromos?: Record<string, PromoData>; |
| 611 | + campaignVideoPromos?: Record<string, PromoData>; |
| 612 | + }; |
| 613 | + |
| 614 | + return { |
| 615 | + bannerPromos: imported.campaignBannerPromos ?? {}, |
| 616 | + videoPromos: imported.campaignVideoPromos ?? {}, |
| 617 | + }; |
| 618 | + } catch { |
| 619 | + return { bannerPromos: {}, videoPromos: {} }; |
| 620 | + } |
| 621 | +} |
| 622 | + |
575 | 623 | // --------------------------------------------------------------------------- |
576 | 624 | // URL integrity |
577 | 625 | // --------------------------------------------------------------------------- |
@@ -845,7 +893,9 @@ export async function runPullCampaigns( |
845 | 893 | } |
846 | 894 |
|
847 | 895 | const timeline = resolveTimeline(rows); |
848 | | - const bundle = buildCampaignBundle(rows, timeline); |
| 896 | + const freshBundle = buildCampaignBundle(rows, timeline); |
| 897 | + const existingPromos = await loadExistingCampaignPromos(options.outputPath); |
| 898 | + const bundle = mergeCampaignBundleAdditively(existingPromos, freshBundle); |
849 | 899 |
|
850 | 900 | if (options.cleanCopy) { |
851 | 901 | const backend = resolveLlmBackend(env); |
|
0 commit comments