clawdie-ai/setup/verify.ts
Operator & Codex 391ed30cb0 Add mac_do verification notes for FreeBSD 15 (Codex)
Document the FreeBSD 15 mac_do rule shape and expose soft setup verification for module/rule state without enforcing live host changes.

---
Build: pass | Tests: pass — 2373 passed (704 files)
2026-05-10 21:54:29 +02:00

734 lines
22 KiB
TypeScript

/**
* Step: verify — End-to-end health check of the full installation.
* Replaces 09-verify.sh
*
* Uses Postgres pool directly, platform-aware service checks.
*/
import { SERVICE_NAME } from '../src/platform-identity.js';
import { execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import pg from 'pg';
import {
AGENT_CONFIG_DIR,
CMS_WEBROOT,
CODE_HOSTING_MODE,
FEATURE_GITEA,
FEATURE_GIT,
FEATURE_LLAMA_CPP,
FEATURE_OLLAMA,
LOCAL_LLM_PROVIDER,
GIT_DEFAULT_REPO_NAME,
GIT_STORAGE_ROOT,
OPS_DB_URL,
PLATFORM_RUNTIME_HOME,
STORE_DIR,
TENANT_ID,
GIT_JAIL_NAME,
} from '../src/config.js';
import { logger } from '../src/logger.js';
import { getTenantSiteAvailability } from '../src/site-availability.js';
import { collectSplitBrainStatus } from '../src/split-brain-status.js';
import {
deriveStripeStatus,
getStripeKeyMode,
type StripeKeyMode,
type StripeStatus,
} from '../src/stripe-config.js';
import { loadTenantSitePublishStatus } from '../src/tenant-site-publish.js';
import {
loadTenantRegistry,
type PlatformRegistry,
type TenantSiteRecord,
} from '../src/tenant-registry.js';
import { commandExists, getPlatform } from './platform.js';
import { loadPackageList } from './packages.js';
import { emitStatus } from './status.js';
const WORKER_PACKAGES = loadPackageList('worker-jail.txt');
const GIT_PACKAGES = loadPackageList('git-jail.txt');
const OLLAMA_PACKAGES = loadPackageList('ollama-jail.txt');
const LLAMA_CPP_PACKAGES = loadPackageList('llama-cpp-jail.txt');
export function getVerifyServicePidCandidates(projectRoot: string): string[] {
return [
path.join('/var/run', `${SERVICE_NAME}.pid`),
path.join(projectRoot, `${SERVICE_NAME}.pid`),
];
}
export function getMountAllowlistHomes(currentHome: string): string[] {
return [
...new Set([
currentHome,
PLATFORM_RUNTIME_HOME,
path.join('/home', TENANT_ID),
]),
];
}
export interface MacDoVerification {
module: 'loaded' | 'not_loaded' | 'unknown' | 'not_applicable';
rules: 'empty' | 'configured' | 'unavailable' | 'not_applicable';
}
export function verifyMacDoState(
platform: string,
execImpl: typeof execSync = execSync,
): MacDoVerification {
if (platform !== 'freebsd') {
return { module: 'not_applicable', rules: 'not_applicable' };
}
let module: MacDoVerification['module'] = 'unknown';
try {
execImpl('/sbin/kldstat -m mac_do', { stdio: 'ignore' });
module = 'loaded';
} catch {
module = 'not_loaded';
}
let rules: MacDoVerification['rules'] = 'unavailable';
try {
const output = execImpl('/sbin/sysctl -n security.mac.do.rules', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
rules = output.trim().length === 0 ? 'empty' : 'configured';
} catch {
rules = 'unavailable';
}
return { module, rules };
}
export interface TenantSitePublishVerification {
state:
| 'not_declared'
| 'planned_only'
| 'partial'
| 'available'
| 'inconsistent';
declaredSites: number;
enabledSites: number;
availableSites: number;
plannedSites: number;
disabledSites: number;
manifestMissing: number;
staleManifest: number;
manifestMismatch: number;
}
function siteManifestMatches(
manifest: ReturnType<typeof loadTenantSitePublishStatus>,
tenantId: string,
site: TenantSiteRecord,
availability: ReturnType<typeof getTenantSiteAvailability>,
): boolean {
if (!manifest) return false;
return (
manifest.result === 'published' &&
manifest.tenantId === tenantId &&
manifest.siteId === site.id &&
manifest.siteFqdn === site.fqdn &&
manifest.targetDir === availability.outputDir &&
manifest.targetIndex === availability.outputIndex
);
}
export function verifyTenantSitePublishState(
registry: PlatformRegistry,
webroot: string,
existsSyncImpl: (targetPath: string) => boolean = fs.existsSync,
loadStatusImpl: (
root: string,
tenantId: string,
siteId: string,
) => ReturnType<typeof loadTenantSitePublishStatus> = loadTenantSitePublishStatus,
): TenantSitePublishVerification {
let declaredSites = 0;
let enabledSites = 0;
let availableSites = 0;
let plannedSites = 0;
let disabledSites = 0;
let manifestMissing = 0;
let staleManifest = 0;
let manifestMismatch = 0;
for (const tenant of Object.values(registry.tenants)) {
for (const site of tenant.sites) {
declaredSites += 1;
if (site.exposure === 'disabled') {
disabledSites += 1;
continue;
}
enabledSites += 1;
const availability = getTenantSiteAvailability(
webroot,
tenant.id,
site.id,
existsSyncImpl,
);
const manifest = loadStatusImpl(webroot, tenant.id, site.id);
const hasManifest = !!manifest;
const manifestOk = siteManifestMatches(
manifest,
tenant.id,
site,
availability,
);
if (availability.hasOutput) {
availableSites += 1;
if (!hasManifest) {
manifestMissing += 1;
} else if (!manifestOk) {
manifestMismatch += 1;
}
} else {
plannedSites += 1;
if (hasManifest) {
staleManifest += 1;
if (!manifestOk) {
manifestMismatch += 1;
}
}
}
}
}
const inconsistent =
manifestMissing > 0 || staleManifest > 0 || manifestMismatch > 0;
const state: TenantSitePublishVerification['state'] =
enabledSites === 0
? 'not_declared'
: inconsistent
? 'inconsistent'
: availableSites === 0
? 'planned_only'
: plannedSites === 0
? 'available'
: 'partial';
return {
state,
declaredSites,
enabledSites,
availableSites,
plannedSites,
disabledSites,
manifestMissing,
staleManifest,
manifestMismatch,
};
}
export async function run(_args: string[]): Promise<void> {
const projectRoot = process.cwd();
const platform = getPlatform();
const homeDir = os.homedir();
logger.info('Starting verification');
// 1. Check service status
let service = 'not_found';
const pidCandidates = getVerifyServicePidCandidates(projectRoot);
for (const pidFile of pidCandidates) {
if (!fs.existsSync(pidFile)) continue;
try {
const pid = fs.readFileSync(pidFile, 'utf-8').trim();
if (pid) {
execSync(`kill -0 ${pid}`, { stdio: 'ignore' });
service = 'running';
}
} catch {
service = 'stopped';
}
break;
}
logger.info({ service }, 'Service status');
const macDo = verifyMacDoState(platform);
// 2. Check jail runtime tools
let jailRuntime = 'missing';
try {
execSync('command -v bastille', { stdio: 'ignore' });
execSync('command -v jls', { stdio: 'ignore' });
jailRuntime = 'available';
} catch {
// No jail toolchain
}
// 3. Check credentials
let credentials = 'missing';
const envFile = path.join(projectRoot, '.env');
if (fs.existsSync(envFile)) {
const envContent = fs.readFileSync(envFile, 'utf-8');
if (
/^(ANTHROPIC_API_KEY|OPENAI_API_KEY|AZURE_OPENAI_API_KEY|GEMINI_API_KEY|GROQ_API_KEY|CEREBRAS_API_KEY|XAI_API_KEY|OPENROUTER_API_KEY|AI_GATEWAY_API_KEY|ZAI_API_KEY|MISTRAL_API_KEY|MINIMAX_API_KEY|OPENCODE_API_KEY|KIMI_API_KEY|AWS_BEARER_TOKEN_BEDROCK|AWS_ACCESS_KEY_ID)=/m.test(
envContent,
)
) {
credentials = 'configured';
}
}
// 4. Check Telegram bot configuration
let telegramAuth = 'not_configured';
let displayLocale = 'unset';
let timeZone = 'unset';
let stripe: StripeStatus = 'disabled';
let stripeKeyMode: StripeKeyMode = 'missing';
let stripeRefunds = 'no';
const stripeRuntimePresent = fs.existsSync(
path.join(projectRoot, 'jail', 'agent-runner', 'src', 'stripe-tools.ts'),
);
let stripeRuntime = stripeRuntimePresent ? 'present' : 'missing';
let cmsEnabled = 'no';
let cmsJailName = 'cms';
let cmsJail = 'not_configured';
let cmsNginx = 'not_configured';
let cmsAstro = 'not_configured';
let cmsStrapi = 'not_configured';
let cmsScreenshots = 'not_configured';
let featureGit = FEATURE_GIT ? 'YES' : 'NO';
let featureGitea = FEATURE_GITEA ? 'YES' : 'NO';
let codeHostingMode = CODE_HOSTING_MODE;
let gitJailName = GIT_JAIL_NAME;
let gitJail = 'not_configured';
let gitPackages = 'not_configured';
let gitStorage = 'not_configured';
let gitRepo = 'not_configured';
let gitStorageRoot = GIT_STORAGE_ROOT;
let gitDefaultRepoName = GIT_DEFAULT_REPO_NAME;
let forgejoService = 'not_configured';
let localLlmProvider = LOCAL_LLM_PROVIDER;
let featureOllama = FEATURE_OLLAMA ? 'YES' : 'NO';
let featureLlamaCpp = FEATURE_LLAMA_CPP ? 'YES' : 'NO';
let ollamaJailName = 'ollama';
let llamaCppJailName = 'llamacpp';
let ollamaJail = 'not_configured';
let ollamaPackages = 'not_configured';
let llamaCppJail = 'not_configured';
let llamaCppPackages = 'not_configured';
let envContent = '';
if (fs.existsSync(envFile)) {
envContent = fs.readFileSync(envFile, 'utf-8');
if (/^TELEGRAM_BOT_TOKEN=.+/m.test(envContent)) {
telegramAuth = 'configured';
}
const stripeKey =
envContent.match(/^STRIPE_SECRET_KEY=(.+)$/m)?.[1]?.trim() || '';
stripeKeyMode = getStripeKeyMode(stripeKey);
stripe = deriveStripeStatus(stripeKeyMode, stripeRuntimePresent);
stripeRefunds =
envContent.match(/^STRIPE_ENABLE_REFUNDS=(.+)$/m)?.[1]?.trim() || 'no';
displayLocale =
envContent.match(/^DISPLAY_LOCALE=(.+)$/m)?.[1]?.trim() || 'unset';
timeZone = envContent.match(/^TZ=(.+)$/m)?.[1]?.trim() || 'unset';
cmsEnabled = envContent.match(/^CMS_ENABLE=(.+)$/m)?.[1]?.trim() || 'yes';
cmsJailName =
envContent.match(/^CMS_JAIL_NAME=(.+)$/m)?.[1]?.trim() || cmsJailName;
featureGit =
envContent.match(/^FEATURE_GIT=(.+)$/m)?.[1]?.trim() || featureGit;
featureGitea =
envContent.match(/^FEATURE_GITEA=(.+)$/m)?.[1]?.trim() || featureGitea;
codeHostingMode =
envContent.match(/^CODE_HOSTING_MODE=(.+)$/m)?.[1]?.trim() ||
codeHostingMode;
localLlmProvider =
envContent.match(/^LOCAL_LLM_PROVIDER=(.+)$/m)?.[1]?.trim() ||
localLlmProvider;
featureOllama =
envContent.match(/^FEATURE_OLLAMA=(.+)$/m)?.[1]?.trim() || featureOllama;
featureLlamaCpp =
envContent.match(/^FEATURE_LLAMA_CPP=(.+)$/m)?.[1]?.trim() ||
featureLlamaCpp;
ollamaJailName =
envContent.match(/^OLLAMA_JAIL_NAME=(.+)$/m)?.[1]?.trim() ||
ollamaJailName;
llamaCppJailName =
envContent.match(/^LLAMA_CPP_JAIL_NAME=(.+)$/m)?.[1]?.trim() ||
llamaCppJailName;
gitJailName =
envContent.match(/^GIT_JAIL_NAME=(.+)$/m)?.[1]?.trim() || gitJailName;
gitStorageRoot =
envContent.match(/^GIT_STORAGE_ROOT=(.+)$/m)?.[1]?.trim() ||
gitStorageRoot;
gitDefaultRepoName =
envContent.match(/^GIT_DEFAULT_REPO_NAME=(.+)$/m)?.[1]?.trim() ||
gitDefaultRepoName;
}
// 5. Check registered groups (via Postgres)
let registeredGroups = 0;
try {
const pool = new pg.Pool({ connectionString: OPS_DB_URL, max: 3 });
const { rows } = await pool.query(
'SELECT COUNT(*) as count FROM registered_groups',
);
registeredGroups = parseInt((rows[0] as { count: string }).count, 10);
await pool.end();
} catch {
// Table might not exist
}
const groupRegistration = registeredGroups > 0 ? 'configured' : 'pending';
// 6. Check mount allowlist
let mountAllowlist = 'missing';
const allowlistHomes = getMountAllowlistHomes(homeDir);
for (const baseDir of allowlistHomes) {
if (
fs.existsSync(
path.join(baseDir, '.config', AGENT_CONFIG_DIR, 'mount-allowlist.json'),
)
) {
mountAllowlist = 'configured';
break;
}
}
// 7. Check host/jail package baseline and CMS runtime
const rsyncTool = commandExists('rsync') ? 'available' : 'missing';
let workerJailName = 'worker';
let workerJailPackages = 'unknown';
if (platform === 'freebsd') {
try {
const jails = execSync('jls -N name', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
const jailList = jails.split('\n').map((line) => line.trim());
if (/^(YES|yes|true|TRUE|1)$/u.test(cmsEnabled)) {
cmsJailName = 'cms';
}
if (jailList.some((line) => line === workerJailName)) {
try {
for (const pkg of WORKER_PACKAGES) {
if (pkg === 'rsync') {
execSync(`bastille cmd ${workerJailName} command -v rsync`, {
stdio: 'ignore',
});
continue;
}
execSync(`bastille cmd ${workerJailName} pkg info ${pkg}`, {
stdio: 'ignore',
});
}
workerJailPackages = 'available';
} catch {
workerJailPackages = 'missing';
}
} else {
workerJailPackages = 'not_running';
}
if (
/^(YES|yes|true|TRUE|1)$/u.test(featureGit) ||
codeHostingMode === 'git' ||
codeHostingMode === 'gitea'
) {
if (jailList.some((line) => line === gitJailName)) {
gitJail = 'running';
try {
for (const pkg of GIT_PACKAGES) {
if (pkg === 'rsync') {
execSync(`bastille cmd ${gitJailName} command -v rsync`, {
stdio: 'ignore',
});
continue;
}
execSync(`bastille cmd ${gitJailName} pkg info ${pkg}`, {
stdio: 'ignore',
});
}
gitPackages = 'available';
} catch {
gitPackages = 'missing';
}
try {
execSync(`bastille cmd ${gitJailName} test -d ${gitStorageRoot}`, {
stdio: 'ignore',
});
gitStorage = 'available';
} catch {
gitStorage = 'missing';
}
try {
execSync(
`bastille cmd ${gitJailName} git --git-dir ${path.posix.join(gitStorageRoot, gitDefaultRepoName)} rev-parse --is-bare-repository`,
{ stdio: 'ignore' },
);
gitRepo = 'available';
} catch {
gitRepo = 'missing';
}
} else {
gitJail = 'missing';
gitPackages = 'missing';
gitStorage = 'missing';
gitRepo = 'missing';
}
}
if (
/^(YES|yes|true|TRUE|1)$/u.test(featureOllama) ||
localLlmProvider === 'ollama'
) {
if (jailList.some((line) => line === ollamaJailName)) {
ollamaJail = 'running';
try {
for (const pkg of OLLAMA_PACKAGES) {
execSync(`bastille cmd ${ollamaJailName} pkg info ${pkg}`, {
stdio: 'ignore',
});
}
ollamaPackages = 'available';
} catch {
ollamaPackages = 'missing';
}
} else {
ollamaJail = 'missing';
ollamaPackages = 'missing';
}
}
if (
/^(YES|yes|true|TRUE|1)$/u.test(featureLlamaCpp) ||
localLlmProvider === 'llama_cpp'
) {
if (jailList.some((line) => line === llamaCppJailName)) {
llamaCppJail = 'running';
try {
for (const pkg of LLAMA_CPP_PACKAGES) {
execSync(`bastille cmd ${llamaCppJailName} pkg info ${pkg}`, {
stdio: 'ignore',
});
}
llamaCppPackages = 'available';
} catch {
llamaCppPackages = 'missing';
}
} else {
llamaCppJail = 'missing';
llamaCppPackages = 'missing';
}
}
// Forgejo web UI check (inside git jail)
if (
codeHostingMode === 'gitea' ||
/^(YES|yes|true|TRUE|1)$/u.test(featureGitea)
) {
if (gitJail === 'running') {
try {
execSync(`bastille cmd ${gitJailName} service forgejo onestatus`, {
stdio: 'ignore',
});
forgejoService = 'running';
} catch {
forgejoService = 'stopped';
}
} else {
forgejoService = 'jail_missing';
}
}
if (/^(YES|yes|true|TRUE|1)$/u.test(cmsEnabled)) {
if (jailList.some((line) => line === cmsJailName)) {
cmsJail = 'running';
try {
execSync(`bastille cmd ${cmsJailName} service nginx onestatus`, {
stdio: 'ignore',
});
cmsNginx = 'running';
} catch {
cmsNginx = 'missing';
}
try {
execSync(`bastille cmd ${cmsJailName} test -f ${CMS_WEBROOT}/index.html`, {
stdio: 'ignore',
});
cmsAstro = 'available';
} catch {
cmsAstro = 'missing';
}
try {
execSync(`bastille cmd ${cmsJailName} service strapi onestatus`, {
stdio: 'ignore',
});
execSync(
`bastille cmd ${cmsJailName} test -f /home/${SERVICE_NAME}/strapi/package.json`,
{ stdio: 'ignore' },
);
cmsStrapi = 'running';
} catch {
cmsStrapi = 'missing';
}
try {
execSync(
`bastille cmd ${cmsJailName} test -d ${CMS_WEBROOT}/screenshots`,
{ stdio: 'ignore' },
);
cmsScreenshots = 'available';
} catch {
cmsScreenshots = 'missing';
}
} else {
cmsJail = 'missing';
cmsNginx = 'missing';
cmsAstro = 'missing';
cmsStrapi = 'missing';
cmsScreenshots = 'missing';
}
}
} catch {
workerJailPackages = 'unknown';
if (/^(YES|yes|true|TRUE|1)$/u.test(cmsEnabled)) {
cmsJail = 'unknown';
cmsNginx = 'unknown';
cmsAstro = 'unknown';
cmsStrapi = 'unknown';
cmsScreenshots = 'unknown';
}
}
} else {
workerJailPackages = 'not_applicable';
cmsJail = 'not_applicable';
cmsNginx = 'not_applicable';
cmsAstro = 'not_applicable';
cmsStrapi = 'not_applicable';
cmsScreenshots = 'not_applicable';
}
const splitBrain = await collectSplitBrainStatus();
const skillsArtifactExpected =
splitBrain.skillsArtifactSql === 'present' &&
splitBrain.skillsArtifactMetadata === 'present';
const skillsArtifactHealthy =
!skillsArtifactExpected || splitBrain.skillsArtifact === 'ready';
const tenantSitePublish = verifyTenantSitePublishState(
loadTenantRegistry(),
CMS_WEBROOT,
);
// Determine overall status
const cmsStrapiOk = ['running', 'missing', 'not_configured'].includes(
cmsStrapi,
);
const cmsHealthy =
!/^(YES|yes|true|TRUE|1)$/u.test(cmsEnabled) ||
(cmsJail === 'running' &&
cmsNginx === 'running' &&
cmsAstro === 'available' &&
cmsStrapiOk);
const gitHealthy =
(!/^(YES|yes|true|TRUE|1)$/u.test(featureGit) &&
!['git', 'gitea'].includes(codeHostingMode)) ||
(gitJail === 'running' &&
gitPackages === 'available' &&
gitStorage === 'available' &&
gitRepo === 'available');
const status =
service === 'running' &&
credentials !== 'missing' &&
telegramAuth !== 'not_configured' &&
stripe !== 'invalid' &&
stripe !== 'misconfigured' &&
rsyncTool === 'available' &&
splitBrain.skillsDb === 'available' &&
skillsArtifactHealthy &&
splitBrain.skillsRuntimeLookup === 'present' &&
splitBrain.memoryDb === 'available' &&
tenantSitePublish.state !== 'inconsistent' &&
(workerJailPackages === 'available' ||
workerJailPackages === 'not_applicable') &&
gitHealthy &&
cmsHealthy
? 'success'
: 'failed';
logger.info({ status }, 'Verification complete');
emitStatus('VERIFY', {
SERVICE: service,
JAIL_RUNTIME: jailRuntime,
MAC_DO_MODULE: macDo.module,
MAC_DO_RULES: macDo.rules,
CREDENTIALS: credentials,
TELEGRAM_AUTH: telegramAuth,
STRIPE: stripe,
STRIPE_KEY_MODE: stripeKeyMode,
STRIPE_REFUNDS: stripeRefunds,
STRIPE_RUNTIME: stripeRuntime,
DISPLAY_LOCALE: displayLocale,
TZ: timeZone,
REGISTERED_GROUPS: registeredGroups,
GROUP_REGISTRATION: groupRegistration,
MOUNT_ALLOWLIST: mountAllowlist,
RSYNC: rsyncTool,
WORKER_JAIL_PACKAGES: workerJailPackages,
WORKER_JAIL_RSYNC: workerJailPackages,
FEATURE_GIT: featureGit,
CODE_HOSTING_MODE: codeHostingMode,
LOCAL_LLM_PROVIDER: localLlmProvider,
FEATURE_OLLAMA: featureOllama,
FEATURE_LLAMA_CPP: featureLlamaCpp,
OLLAMA_JAIL: ollamaJail,
OLLAMA_JAIL_NAME: ollamaJailName,
OLLAMA_PACKAGES: ollamaPackages,
LLAMA_CPP_JAIL: llamaCppJail,
LLAMA_CPP_JAIL_NAME: llamaCppJailName,
LLAMA_CPP_PACKAGES: llamaCppPackages,
GIT_JAIL: gitJail,
GIT_JAIL_NAME: gitJailName,
GIT_PACKAGES: gitPackages,
GIT_STORAGE: gitStorage,
GIT_REPO: gitRepo,
FORGEJO_SERVICE: forgejoService,
SKILLS_DB: splitBrain.skillsDb,
SKILLS_ARTIFACT_SQL: splitBrain.skillsArtifactSql,
SKILLS_ARTIFACT_METADATA: splitBrain.skillsArtifactMetadata,
SKILLS_ARTIFACT_EXPECTED: skillsArtifactExpected ? 'yes' : 'no',
SKILLS_ARTIFACT: splitBrain.skillsArtifact,
SKILLS_ARTIFACT_VERSION: splitBrain.skillsArtifactVersion,
SKILLS_ARTIFACT_DB_VERSION: splitBrain.skillsArtifactDbVersion,
SKILLS_RUNTIME_LOOKUP: splitBrain.skillsRuntimeLookup,
SKILLS_ARTIFACT_ROWS: splitBrain.skillsArtifactRows,
SKILLS_DOCUMENT_ROWS: splitBrain.skillsDocumentRows,
SKILLS_CHUNK_ROWS: splitBrain.skillsChunkRows,
MEMORY_DB: splitBrain.memoryDb,
MEMORY_ROWS: splitBrain.memoryRows,
MEMORY_CHUNK_ROWS: splitBrain.memoryChunkRows,
MEMORY_EMBEDDING_ROWS: splitBrain.memoryEmbeddingRows,
CMS_ENABLE: cmsEnabled,
CMS_JAIL: cmsJail,
CMS_NGINX: cmsNginx,
CMS_ASTRO: cmsAstro,
CMS_STRAPI: cmsStrapi,
TENANT_SITE_PUBLISH: tenantSitePublish.state,
TENANT_SITES_DECLARED: tenantSitePublish.declaredSites,
TENANT_SITES_ENABLED: tenantSitePublish.enabledSites,
TENANT_SITES_AVAILABLE: tenantSitePublish.availableSites,
TENANT_SITES_PLANNED: tenantSitePublish.plannedSites,
TENANT_SITES_DISABLED: tenantSitePublish.disabledSites,
TENANT_SITE_MANIFEST_MISSING: tenantSitePublish.manifestMissing,
TENANT_SITE_MANIFEST_STALE: tenantSitePublish.staleManifest,
TENANT_SITE_MANIFEST_MISMATCH: tenantSitePublish.manifestMismatch,
CMS_SCREENSHOTS: cmsScreenshots,
STATUS: status,
LOG: 'logs/setup.log',
});
if (status === 'failed') process.exit(1);
}