Make the docs renderer name match its purpose, add CMS_DOCS_SITE_PATH with ASTRO_SITE_PATH compatibility, and update docs publishing paths. --- Build: pass | Tests: pass — 2372 passed (704 files)
310 lines
8.1 KiB
TypeScript
310 lines
8.1 KiB
TypeScript
import { SERVICE_NAME } from '../src/platform-identity.js';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { CMS_DOCS_SITE_PATH, CMS_WEBROOT } from '../src/config.js';
|
|
import { logger } from '../src/logger.js';
|
|
import {
|
|
resolveTenantSiteDocs,
|
|
resolveTenantSiteContentSource,
|
|
TENANT_SITE_SNAPSHOT_ROOT,
|
|
} from '../src/tenant-site-content.js';
|
|
import {
|
|
buildTenantSiteBuildEnv,
|
|
buildTenantSitePublishPlan,
|
|
hostVisibleJailPath,
|
|
writeTenantSitePublishStatusFiles,
|
|
type TenantSitePublishStatus,
|
|
} from '../src/tenant-site-publish.js';
|
|
import { loadTenantRegistry, type TenantRecord } from '../src/tenant-registry.js';
|
|
import { bastille, jailExists } from './bastille-helpers.js';
|
|
import { readEnvFile } from '../src/env.js';
|
|
|
|
interface ParsedArgs {
|
|
tenantId: string;
|
|
siteId: string;
|
|
skipBuild: boolean;
|
|
}
|
|
|
|
function parseArgs(args: string[]): ParsedArgs {
|
|
let tenantId = '';
|
|
let siteId = '';
|
|
let skipBuild = false;
|
|
|
|
for (let i = 0; i < args.length; i += 1) {
|
|
const arg = args[i];
|
|
if (arg === '--tenant') {
|
|
tenantId = (args[i + 1] || '').trim();
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg === '--site') {
|
|
siteId = (args[i + 1] || '').trim();
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg === '--skip-build') {
|
|
skipBuild = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!tenantId || !siteId) {
|
|
throw new Error(
|
|
'Usage: tsx setup/publish-tenant-site.ts --tenant <tenant> --site <site> [--skip-build]',
|
|
);
|
|
}
|
|
|
|
return { tenantId, siteId, skipBuild };
|
|
}
|
|
|
|
function resolveCmsJailName(): string {
|
|
const envOverrides = readEnvFile(['CMS_JAIL_NAME']);
|
|
return (
|
|
process.env.CMS_JAIL_NAME ||
|
|
envOverrides.CMS_JAIL_NAME ||
|
|
'cms'
|
|
).trim();
|
|
}
|
|
|
|
function syncHostSiteSlices(jailName: string): void {
|
|
const hostMount = `${CMS_DOCS_SITE_PATH}-host`;
|
|
const syncScripts = bastille(
|
|
'cmd',
|
|
jailName,
|
|
'sh',
|
|
'-c',
|
|
`test -d ${hostMount}/scripts && rsync -a ${hostMount}/scripts/ ${CMS_DOCS_SITE_PATH}/scripts/`,
|
|
);
|
|
if (!syncScripts.ok) {
|
|
throw new Error(`Failed to sync host scripts into local Astro project: ${syncScripts.output}`);
|
|
}
|
|
|
|
const syncAstroSource = bastille(
|
|
'cmd',
|
|
jailName,
|
|
'sh',
|
|
'-c',
|
|
`test -f ${hostMount}/src/astro/astro.config.mjs && install -d -o ${SERVICE_NAME} -g ${SERVICE_NAME} -m 755 ${CMS_DOCS_SITE_PATH}/src/astro && cp ${hostMount}/src/astro/astro.config.mjs ${CMS_DOCS_SITE_PATH}/src/astro/astro.config.mjs`,
|
|
);
|
|
if (!syncAstroSource.ok) {
|
|
throw new Error(`Failed to sync host Astro config source into local project: ${syncAstroSource.output}`);
|
|
}
|
|
}
|
|
|
|
function buildAstroSite(
|
|
jailName: string,
|
|
buildEnv: {
|
|
ASTRO_OUT_DIR: string;
|
|
ASTRO_SITE_TITLE: string;
|
|
ASTRO_SITE_URL: string;
|
|
ASTRO_SITE_VARIANT: string;
|
|
},
|
|
): void {
|
|
const ownership = bastille(
|
|
'cmd',
|
|
jailName,
|
|
'chown',
|
|
'-R',
|
|
`${SERVICE_NAME}:${SERVICE_NAME}`,
|
|
CMS_DOCS_SITE_PATH,
|
|
);
|
|
if (!ownership.ok) {
|
|
throw new Error(`Failed to normalize Astro project ownership in ${jailName}: ${ownership.output}`);
|
|
}
|
|
|
|
syncHostSiteSlices(jailName);
|
|
|
|
const build = bastille(
|
|
'cmd',
|
|
jailName,
|
|
'su',
|
|
'-',
|
|
SERVICE_NAME,
|
|
'-c',
|
|
`cd ${CMS_DOCS_SITE_PATH} && env ASTRO_OUT_DIR=${JSON.stringify(buildEnv.ASTRO_OUT_DIR)} ASTRO_SITE_TITLE=${JSON.stringify(buildEnv.ASTRO_SITE_TITLE)} ASTRO_SITE_URL=${JSON.stringify(buildEnv.ASTRO_SITE_URL)} ASTRO_SITE_VARIANT=${JSON.stringify(buildEnv.ASTRO_SITE_VARIANT)} npm run build`,
|
|
);
|
|
if (!build.ok) {
|
|
throw new Error(`Astro build failed in ${jailName}: ${build.output}`);
|
|
}
|
|
}
|
|
|
|
function applyTenantSiteDocs(
|
|
jailName: string,
|
|
tenant: TenantRecord,
|
|
siteId: string,
|
|
): () => void {
|
|
const site = tenant.sites.find((entry) => entry.id === siteId);
|
|
if (!site) {
|
|
throw new Error(`Unknown tenant site in registry: ${tenant.id}/${siteId}`);
|
|
}
|
|
|
|
const docsRoot = hostVisibleJailPath(
|
|
jailName,
|
|
path.join(CMS_DOCS_SITE_PATH, 'src/content/docs'),
|
|
);
|
|
const generatedDocs = resolveTenantSiteDocs(
|
|
tenant,
|
|
site,
|
|
TENANT_SITE_SNAPSHOT_ROOT,
|
|
);
|
|
const previous = new Map<string, string | null>();
|
|
|
|
for (const page of generatedDocs) {
|
|
const pagePath = path.join(docsRoot, page.relPath);
|
|
previous.set(pagePath, fs.existsSync(pagePath) ? fs.readFileSync(pagePath, 'utf-8') : null);
|
|
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
fs.writeFileSync(pagePath, page.content, 'utf-8');
|
|
}
|
|
|
|
return () => {
|
|
for (const [pagePath, priorContent] of previous.entries()) {
|
|
if (priorContent === null) {
|
|
if (fs.existsSync(pagePath)) {
|
|
fs.rmSync(pagePath);
|
|
}
|
|
continue;
|
|
}
|
|
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
fs.writeFileSync(pagePath, priorContent, 'utf-8');
|
|
}
|
|
};
|
|
}
|
|
|
|
function ensureTargetDir(
|
|
jailName: string,
|
|
targetDir: string,
|
|
): void {
|
|
const mkdir = bastille(
|
|
'cmd',
|
|
jailName,
|
|
'install',
|
|
'-d',
|
|
'-o',
|
|
SERVICE_NAME,
|
|
'-g',
|
|
SERVICE_NAME,
|
|
'-m',
|
|
'755',
|
|
targetDir,
|
|
);
|
|
if (!mkdir.ok) {
|
|
throw new Error(`Failed to create tenant site target dir ${targetDir}: ${mkdir.output}`);
|
|
}
|
|
}
|
|
|
|
function syncBuiltSite(
|
|
jailName: string,
|
|
sourceDistDir: string,
|
|
targetDir: string,
|
|
): void {
|
|
const sync = bastille(
|
|
'cmd',
|
|
jailName,
|
|
'sh',
|
|
'-c',
|
|
`rsync -a --delete ${sourceDistDir}/ ${targetDir}/`,
|
|
);
|
|
if (!sync.ok) {
|
|
throw new Error(`Failed to sync tenant site output into ${targetDir}: ${sync.output}`);
|
|
}
|
|
}
|
|
|
|
function writePublishManifest(
|
|
jailName: string,
|
|
status: TenantSitePublishStatus,
|
|
): void {
|
|
writeTenantSitePublishStatusFiles(
|
|
path.join(
|
|
hostVisibleJailPath(jailName, status.targetDir),
|
|
'.clawdie-publish.json',
|
|
),
|
|
status,
|
|
);
|
|
}
|
|
|
|
export async function run(rawArgs: string[]): Promise<void> {
|
|
const args = parseArgs(rawArgs);
|
|
const jailName = resolveCmsJailName();
|
|
if (!jailExists(jailName)) {
|
|
throw new Error(`CMS jail does not exist: ${jailName}`);
|
|
}
|
|
|
|
const registry = loadTenantRegistry();
|
|
const plan = buildTenantSitePublishPlan(registry, {
|
|
tenantId: args.tenantId,
|
|
siteId: args.siteId,
|
|
astroSitePath: CMS_DOCS_SITE_PATH,
|
|
webroot: CMS_WEBROOT,
|
|
});
|
|
const buildEnv = buildTenantSiteBuildEnv(plan);
|
|
|
|
if (!args.skipBuild) {
|
|
const tenant = registry.tenants[args.tenantId];
|
|
if (!tenant) {
|
|
throw new Error(`Unknown tenant in registry: ${args.tenantId}`);
|
|
}
|
|
const restoreDocs = applyTenantSiteDocs(jailName, tenant, args.siteId);
|
|
try {
|
|
buildAstroSite(jailName, buildEnv);
|
|
} finally {
|
|
restoreDocs();
|
|
}
|
|
}
|
|
|
|
const hostDistDir = hostVisibleJailPath(jailName, plan.sourceDistDir);
|
|
if (!fs.existsSync(hostDistDir)) {
|
|
throw new Error(`Built Astro dist/ not found on host path: ${hostDistDir}`);
|
|
}
|
|
|
|
ensureTargetDir(jailName, plan.targetDir);
|
|
syncBuiltSite(jailName, plan.sourceDistDir, plan.targetDir);
|
|
|
|
const tenant = registry.tenants[args.tenantId];
|
|
const site = tenant?.sites.find((entry) => entry.id === args.siteId);
|
|
if (!tenant || !site) {
|
|
throw new Error(`Unknown tenant site in registry: ${args.tenantId}/${args.siteId}`);
|
|
}
|
|
writePublishManifest(jailName, {
|
|
tenantId: plan.tenantId,
|
|
siteId: plan.siteId,
|
|
siteFqdn: plan.siteFqdn,
|
|
contentSource: resolveTenantSiteContentSource(
|
|
tenant,
|
|
site,
|
|
TENANT_SITE_SNAPSHOT_ROOT,
|
|
),
|
|
publishedAt: new Date().toISOString(),
|
|
targetDir: plan.targetDir,
|
|
targetIndex: plan.targetIndex,
|
|
sourceDistDir: plan.sourceDistDir,
|
|
result: 'published',
|
|
});
|
|
|
|
logger.info(
|
|
{
|
|
tenantId: plan.tenantId,
|
|
siteId: plan.siteId,
|
|
siteFqdn: plan.siteFqdn,
|
|
sourceDistDir: plan.sourceDistDir,
|
|
targetDir: plan.targetDir,
|
|
targetIndex: plan.targetIndex,
|
|
cmsJail: jailName,
|
|
},
|
|
'Published tenant site build into CMS webroot',
|
|
);
|
|
}
|
|
|
|
const entrypoint = process.argv[1]
|
|
? path.resolve(process.argv[1])
|
|
: null;
|
|
const isDirectRun =
|
|
!!entrypoint && import.meta.url === new URL(`file://${entrypoint}`).href;
|
|
|
|
if (isDirectRun) {
|
|
run(process.argv.slice(2)).catch((error) => {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error(message);
|
|
process.exit(1);
|
|
});
|
|
}
|