#!/usr/bin/env node import { spawn, spawnSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs"; import path from "node:path"; const cwd = process.cwd(); const pascalDir = path.resolve( cwd, process.env.PASCAL_EDITOR_DIR || ".pascal-editor" ); const repoUrl = process.env.PASCAL_EDITOR_REPO || "https://github.com/pascalorg/editor.git"; const repoRef = process.env.PASCAL_EDITOR_REF || "main"; const port = process.env.PASCAL_EDITOR_PORT || "3002"; function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd, stdio: "inherit", ...options, }); if (result.status !== 0) { process.exit(result.status ?? 1); } } function commandExists(command) { const result = spawnSync(command, ["--version"], { stdio: "ignore" }); return result.status === 0; } function patchNestedNextRoot() { const configPath = path.join(pascalDir, "apps/editor/next.config.ts"); if (!existsSync(configPath)) { return; } let config = readFileSync(configPath, "utf8"); if (!config.includes("import path from 'node:path'")) { config = config.replace( "import type { NextConfig } from 'next'\n", "import path from 'node:path'\nimport type { NextConfig } from 'next'\n" ); } if (!config.includes("root: path.resolve(process.cwd(), '../..')")) { config = config.replace( " turbopack: {\n", " turbopack: {\n root: path.resolve(process.cwd(), '../..'),\n" ); } writeFileSync(configPath, config, "utf8"); } function ensureSparseCheckout() { run( "git", [ "-C", pascalDir, "sparse-checkout", "set", "apps/editor", "packages", "tooling", "styles", ], { cwd } ); } function patchCssImports() { const source = path.join(pascalDir, "styles/elevation.css"); const targetDir = path.join(pascalDir, "apps/editor/styles"); const target = path.join(targetDir, "elevation.css"); const globalsPath = path.join(pascalDir, "apps/editor/app/globals.css"); if (existsSync(source)) { mkdirSync(targetDir, { recursive: true }); copyFileSync(source, target); } if (!existsSync(globalsPath)) { return; } const globals = readFileSync(globalsPath, "utf8"); const patched = globals.replace( '../../../styles/elevation.css', '../styles/elevation.css' ); if (patched !== globals) { writeFileSync(globalsPath, patched, "utf8"); } } function runPackageScript(packageManager, packageDir, scriptName) { if (packageManager === "bun") { run("bun", ["--cwd", packageDir, "run", scriptName], { cwd: pascalDir }); return; } run("npm", ["--prefix", packageDir, "run", scriptName], { cwd: pascalDir }); } function ensureBuiltWorkspacePackages(packageManager) { const requiredBuilds = [ ["packages/core", "dist/index.js"], ["packages/viewer", "dist/index.js"], ["packages/nodes", "dist/index.js"], ["packages/mcp", "dist/index.js"], ]; for (const [packageDir, distFile] of requiredBuilds) { if (existsSync(path.join(pascalDir, packageDir, distFile))) { continue; } runPackageScript(packageManager, packageDir, "build"); } } if (!existsSync(pascalDir)) { mkdirSync(path.dirname(pascalDir), { recursive: true }); run("git", [ "clone", "--depth", "1", "--filter=blob:none", "--sparse", "--branch", repoRef, repoUrl, pascalDir, ]); } else if (!existsSync(path.join(pascalDir, ".git"))) { console.error( `${pascalDir} already exists but is not a git checkout. Move it away or set PASCAL_EDITOR_DIR.` ); process.exit(1); } ensureSparseCheckout(); patchCssImports(); const envLocalPath = path.join(pascalDir, ".env.local"); if (!existsSync(envLocalPath)) { writeFileSync(envLocalPath, `PORT=${port}\n`, "utf8"); } patchNestedNextRoot(); const hasBun = commandExists("bun"); const hasNpm = commandExists("npm"); const hasNodeModules = existsSync(path.join(pascalDir, "node_modules")); if (!hasBun && !hasNpm) { console.error("Pascal Editor needs Bun or npm to install dependencies."); process.exit(1); } if (!hasNodeModules) { if (hasBun) { run("bun", ["install"], { cwd: pascalDir }); } else { run("npm", ["install"], { cwd: pascalDir }); } } const packageManager = hasBun ? "bun" : "npm"; ensureBuiltWorkspacePackages(packageManager); const command = hasBun ? "bun" : "npm"; const args = hasBun ? ["--cwd", "apps/editor", "run", "dev"] : ["--prefix", "apps/editor", "run", "dev"]; console.log(`Starting self-hosted Pascal Editor on http://localhost:${port}`); const child = spawn(command, args, { cwd: pascalDir, stdio: "inherit", env: { ...process.env, PORT: port, }, }); child.on("exit", (code, signal) => { if (signal) { process.kill(process.pid, signal); return; } process.exit(code ?? 0); });