/** * Module registry publish — Phase 4 of `celilo publish`. * * Phase 4 is the slimmest phase: revision selection and tarball * construction live inside `celilo module publish` itself, so the plan * only enumerates which module directories to ship. The executor * shells out to a single `celilo module publish ` invocation * (using the local source, not the global install, so this script * always runs the just-built CLI from this repo). */ import { spawnSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { basename, join, relative } from 'node:path'; import { REPO_ROOT, listModuleDirs } from './helpers'; import type { ModuleItem } from './types'; // ─── Planner ─────────────────────────────────────────────────────── /** * Pure wrapper: turn a list of module directories into the * corresponding `ModuleItem[]`. Exists so the test surface can * exercise the wrapping contract without mocking `listModuleDirs` * (which lives in helpers.ts and is process-globally referenced by * other modules — mocking it would leak to unrelated tests). */ export function mapModuleDirsToItems(dirs: string[]): ModuleItem[] { return dirs.map((moduleDir) => ({ moduleDir })); } /** * Leave the modules named by `--skip-module` out of the sweep. A name * that matches no module directory throws, so a typo cannot skip * nothing and publish the module it meant to hold back. */ export function applyModuleSkips(items: ModuleItem[], skippedModules: string[]): ModuleItem[] { const known = new Set(items.map((m) => basename(m.moduleDir))); const unknown = skippedModules.filter((id) => !known.has(id)); if (unknown.length > 0) { throw new Error(`--skip-module names no module directory: ${unknown.join(', ')}`); } const skipped = new Set(skippedModules); return items.filter((m) => !skipped.has(basename(m.moduleDir))); } export function planModulePublish(skippedModules: string[]): ModuleItem[] { return applyModuleSkips(mapModuleDirsToItems(listModuleDirs()), skippedModules); } // ─── Executor ────────────────────────────────────────────────────── export function executeModulePublish( items: ModuleItem[], opts: { allowStale: boolean; skippedModules: string[] }, ): void { if (items.length === 0) { console.log('\nNo modules with manifest.yml found — skipping module-publish phase.'); return; } const modulesRoot = join(REPO_ROOT, 'modules'); if (!existsSync(modulesRoot)) { console.log('\nNo modules/ directory at repo root — skipping module-publish phase.'); return; } console.log('\n──────────────────────────────────────────────'); console.log(' Publishing modules to celilo registry'); console.log('──────────────────────────────────────────────'); for (const id of opts.skippedModules) console.log(` modules/${id} (skipped: --skip-module)`); for (const m of items) console.log(` ${relative(REPO_ROOT, m.moduleDir)}`); console.log(); const cliEntry = join(REPO_ROOT, 'apps', 'celilo', 'src', 'cli', 'index.ts'); const args = ['run', cliEntry, 'module', 'publish', ...items.map((m) => m.moduleDir)]; if (opts.allowStale) args.push('--allow-stale'); const r = spawnSync('bun', args, { cwd: REPO_ROOT, stdio: 'inherit' }); if (r.status !== 0) { console.error(`\n✗ Module publish failed (exit ${r.status}). Aborting.`); process.exit(r.status ?? 1); } }