/** Supply-chain export wizard helpers (WordPress plugin + npm postinstall). */ import { spreadTechniqueDocUrl } from './spreadTechniques'; export type SupplyChainFamily = 'wordpress' | 'npm'; export const SUPPLY_CHAIN_WIZARD_STEPS = [ 'pick-build', 'configure', 'download', 'host', ] as const; export type SupplyChainWizardStep = (typeof SUPPLY_CHAIN_WIZARD_STEPS)[number]; export const SUPPLY_CHAIN_STEP_LABELS: Record = { 'pick-build': 'Pick build', configure: 'Configure site/campaign', download: 'Download ZIP', host: 'Copy hosting instructions', }; /** Matches server sanitizeExportSlug in spread_export.go */ export function sanitizeExportSlug(s: string): string { let slug = s.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, ''); if (!slug) slug = 'site'; if (slug.length > 48) slug = slug.slice(0, 48); return slug; } export function wpCampaignSlug(siteName: string): string { return `wp-${sanitizeExportSlug(siteName)}`; } export function wpDownloadUrl(serverUrl: string, siteName: string, buildId: string): string { const base = serverUrl.replace(/\/$/, ''); const c = wpCampaignSlug(siteName); let url = `${base}/get?c=${encodeURIComponent(c)}`; const pin = buildId.trim(); if (pin) url += `&pin=${encodeURIComponent(pin)}`; return url; } export function npmPackageName(campaign: string): string { const slug = sanitizeExportSlug(campaign || 'npm-helper'); return `@aetherforge/${slug}-helper`; } export function npmInstallShUrl(serverUrl: string, campaign: string, buildId: string): string { const base = serverUrl.replace(/\/$/, ''); const parts: string[] = []; const pin = buildId.trim(); const slug = campaign.trim(); if (pin) parts.push(`pin=${encodeURIComponent(pin)}`); if (slug) parts.push(`c=${encodeURIComponent(slug)}`); return parts.length ? `${base}/install.sh?${parts.join('&')}` : `${base}/install.sh`; } export function supplyChainZipFilename(family: SupplyChainFamily, siteOrCampaign: string): string { const slug = sanitizeExportSlug(siteOrCampaign || (family === 'wordpress' ? 'site' : 'npm-helper')); return family === 'wordpress' ? `${slug}-wordpress-plugin.zip` : `${slug}-npm-helper.zip`; } export function supplyChainWikiUrl(family: SupplyChainFamily): string { return family === 'wordpress' ? spreadTechniqueDocUrl('wordpress') : spreadTechniqueDocUrl('npm-helper'); } export function supplyChainHostingChecklistUrl(family: SupplyChainFamily): string { const anchor = family === 'wordpress' ? 'wordpress-hosting-checklist' : 'npm-hosting-checklist'; return spreadTechniqueDocUrl(anchor); } export interface HostingChecklistItem { id: string; label: string; } export function hostingChecklist(family: SupplyChainFamily): HostingChecklistItem[] { if (family === 'wordpress') { return [ { id: 'unzip', label: 'Unzip the downloaded plugin archive locally' }, { id: 'upload', label: 'WP Admin → Plugins → Add New → Upload Plugin' }, { id: 'activate', label: 'Activate the plugin on your owned WordPress host' }, { id: 'verify', label: 'Confirm admin notice links to /get?c=wp-{site} on your deck' }, { id: 'track', label: 'Track wp-{site} hits in Emberwake → Campaign War Room' }, ]; } return [ { id: 'unzip', label: 'Unzip the npm helper package template' }, { id: 'name', label: 'Adjust package.json name/scope if needed' }, { id: 'publish', label: 'Publish to a registry you control (private npm, Verdaccio, GitHub Packages)' }, { id: 'dep', label: 'Add as dependency only in authorized CI/dev environments' }, { id: 'verify', label: 'Run npm install and confirm postinstall curls install.sh' }, ]; } export interface HostingInstructionBlock { title: string; body: string; } export function hostingInstructions( family: SupplyChainFamily, opts: { serverUrl: string; siteName: string; campaign: string; buildId: string }, ): HostingInstructionBlock[] { const { serverUrl, siteName, campaign, buildId } = opts; if (family === 'wordpress') { const slug = sanitizeExportSlug(siteName); const dl = wpDownloadUrl(serverUrl, siteName, buildId); return [ { title: 'Upload path', body: `Plugins → Add New → Upload Plugin → choose ${slug}-wordpress-plugin.zip → Install Now → Activate`, }, { title: 'Campaign tag', body: `War Room tracks connects as ?c=${wpCampaignSlug(siteName)}`, }, { title: 'Download URL (plugin links here)', body: dl, }, { title: 'Wiki playbook', body: supplyChainWikiUrl('wordpress'), }, ]; } const pkg = npmPackageName(campaign); const install = npmInstallShUrl(serverUrl, campaign, buildId); return [ { title: 'Package name', body: pkg, }, { title: 'Publish', body: `cd unpacked-folder && npm publish --access restricted`, }, { title: 'postinstall target', body: install, }, { title: 'Wiki playbook', body: supplyChainWikiUrl('npm'), }, ]; } export function wizardStepIndex(step: SupplyChainWizardStep): number { return SUPPLY_CHAIN_WIZARD_STEPS.indexOf(step); } export function wizardStepStatus( step: SupplyChainWizardStep, current: SupplyChainWizardStep, ): 'pending' | 'active' | 'done' { const idx = wizardStepIndex(step); const cur = wizardStepIndex(current); if (idx < cur) return 'done'; if (idx === cur) return 'active'; return 'pending'; } /** Post-export deployment reel — three beats after a successful ZIP export. */ export type DeploymentReelStepId = 'download' | 'upload' | 'verify-war-room'; export interface DeploymentReelStep { id: DeploymentReelStepId; label: string; /** When set, step label links to wiki or in-app anchor. */ href?: string; } export const DEPLOYMENT_REEL_STEP_IDS: DeploymentReelStepId[] = [ 'download', 'upload', 'verify-war-room', ]; export const DEPLOYMENT_REEL_INITIAL_DELAY_MS = 400; export const DEPLOYMENT_REEL_STEP_MS = 900; /** Wiki anchor for the upload/publish beat in the deployment reel. */ export function deploymentReelUploadWikiUrl(family: SupplyChainFamily): string { const anchor = family === 'wordpress' ? 'wordpress-hosting-checklist' : 'npm-hosting-checklist'; return `${supplyChainWikiUrl(family).split('#')[0]}#${anchor}`; } /** In-app scroll target for War Room verification. */ export const EMBERWAKE_WAR_ROOM_HASH = '#campaign-war-room'; export function emberwakeWarRoomUrl(): string { return `/emberwake${EMBERWAKE_WAR_ROOM_HASH}`; } export function deploymentReelSteps(family: SupplyChainFamily): DeploymentReelStep[] { return [ { id: 'download', label: 'Download ZIP' }, { id: 'upload', label: 'Upload here', href: deploymentReelUploadWikiUrl(family) }, { id: 'verify-war-room', label: 'Verify hit in War Room', href: emberwakeWarRoomUrl() }, ]; } /** How many reel steps should show a completed checkmark at `elapsedMs`. */ export function deploymentReelVisibleCount(elapsedMs: number, stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number { if (elapsedMs < DEPLOYMENT_REEL_INITIAL_DELAY_MS) return 0; const afterStart = elapsedMs - DEPLOYMENT_REEL_INITIAL_DELAY_MS; const count = Math.floor(afterStart / DEPLOYMENT_REEL_STEP_MS) + 1; return Math.min(Math.max(count, 0), stepCount); } /** Index (0-based) of the step currently animating, or -1 before start / after all done. */ export function deploymentReelActiveIndex(visibleCount: number, stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number { if (visibleCount <= 0) return 0; if (visibleCount >= stepCount) return -1; return visibleCount; } export function deploymentReelStepStatus( stepIndex: number, visibleCount: number, ): 'pending' | 'active' | 'done' { if (stepIndex < visibleCount) return 'done'; if (stepIndex === visibleCount && visibleCount < DEPLOYMENT_REEL_STEP_IDS.length) return 'active'; return 'pending'; } export function deploymentReelTotalDurationMs(stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number { return DEPLOYMENT_REEL_INITIAL_DELAY_MS + stepCount * DEPLOYMENT_REEL_STEP_MS; }