feat: deploy Pascal from vendored source

This commit is contained in:
134 2026-07-12 00:55:11 +08:00
parent 40ff3ca851
commit 5260502507
9 changed files with 136 additions and 178 deletions

2
.gitignore vendored
View File

@ -87,7 +87,7 @@ checkpoints/
# 第三方参考源码(不提交到git) # 第三方参考源码(不提交到git)
reference-sources/ reference-sources/
# Local self-hosted Pascal Editor checkout. # Legacy external Pascal Editor checkout (the managed source is under services/).
.pascal-editor/ .pascal-editor/
# Drizzle migrations must be versioned (override the global *.sql ignore above) # Drizzle migrations must be versioned (override the global *.sql ignore above)

View File

@ -3,6 +3,7 @@
/* include utility classes in streamdown */ /* include utility classes in streamdown */
@source '../node_modules/streamdown/dist/index.js'; @source '../node_modules/streamdown/dist/index.js';
@source not '../services/pascal-editor';
/* custom variant for setting dark mode programmatically */ /* custom variant for setting dark mode programmatically */
@custom-variant dark (&:is(.dark, .dark *)); @custom-variant dark (&:is(.dark, .dark *));

View File

@ -13,6 +13,7 @@
"!lib/db/migrations/meta", "!lib/db/migrations/meta",
"!public/vendor", "!public/vendor",
"!public/three", "!public/three",
"!services/pascal-editor",
"!vercel.json" "!vercel.json"
] ]
}, },

View File

@ -2,10 +2,11 @@
## Current Shape ## Current Shape
The app now exposes a first-phase Pascal Editor workspace at `/pascal-editor`. The app exposes a Pascal Editor workspace at `/pascal-editor`. Its customized
It is intentionally iframe-based so Pascal can be self-hosted as a standalone source is versioned with the main repository under `services/pascal-editor`,
Next app without forcing its unpublished `@pascal-app/nodes` workspace package while it remains an independently built Next app embedded through an iframe.
into the main application dependency graph. This keeps Pascal's Bun workspace and `@pascal-app/nodes` dependency graph out
of the host application's pnpm dependency graph.
The wrapper route is protected by the host application's existing login system. The wrapper route is protected by the host application's existing login system.
Unauthenticated users are redirected to `/login?callbackUrl=/pascal-editor`. Unauthenticated users are redirected to `/login?callbackUrl=/pascal-editor`.
@ -26,11 +27,10 @@ loading a broken iframe.
## Why Iframe First ## Why Iframe First
`@pascal-app/core`, `@pascal-app/viewer`, and `@pascal-app/editor` are published `@pascal-app/core`, `@pascal-app/viewer`, and `@pascal-app/editor` are published
on npm, but the official standalone app still depends on `@pascal-app/nodes` on npm, but the standalone app also depends on its monorepo workspace packages.
from its monorepo workspace. That package registers the built-in node Keeping the complete Pascal source tree below `services/pascal-editor` preserves
definitions used by walls, doors, windows, levels, roofs, stairs, and other the built-in walls, doors, windows, levels, roofs, stairs, MCP tools, and our AI
editable building elements. Until it is published or vendored, a pure npm-based Agent customization without coupling the host build to those packages.
native React integration would be incomplete.
## Deployment Options ## Deployment Options
@ -42,9 +42,9 @@ Run Pascal Editor as a local standalone service:
pnpm pascal:dev pnpm pascal:dev
``` ```
The helper clones `pascalorg/editor` into the ignored `.pascal-editor/` The helper uses `services/pascal-editor`, installs its dependencies when needed,
directory on first run, installs its dependencies, and starts `apps/editor` on and starts `apps/editor` on `http://localhost:3002`. Set `PASCAL_EDITOR_DIR` only
`http://localhost:3002`. when deliberately testing a different Pascal checkout.
Then open: Then open:
@ -121,13 +121,20 @@ For repeatable deployment and upgrades on additional servers, use
variables, health check, real-model smoke test, and rollback behavior are variables, health check, real-model smoke test, and rollback behavior are
documented in `docs/pascal-production-deployment.md`. documented in `docs/pascal-production-deployment.md`.
## Next Step ## Source Maintenance
After the iframe POC is stable, the native integration path is: `services/pascal-editor` was imported with Git subtree so its upstream history
can still be refreshed deliberately. Apply and test upstream updates in a
dedicated branch before merging them with our localization, host authentication,
scene storage, and AI Agent changes:
1. Pin a Pascal upstream commit. ```bash
2. Vendor or workspace-link `packages/nodes` as `@pascal-app/nodes`. git subtree pull \
3. Add Pascal packages to `transpilePackages`. --prefix=services/pascal-editor \
4. Upgrade `three` to the Pascal-compatible `^0.184` line and retest existing https://github.com/pascalorg/editor.git main \
3D/CAD surfaces. --squash
5. Wire `Editor` `onLoad` / `onSave` to our project storage APIs. ```
Production servers do not run this command. They only pull the reviewed main
repository and let `pnpm pascal:deploy` sync the vendored source into the
isolated runtime directory.

View File

@ -1,32 +1,30 @@
# Pascal Editor 可复用生产部署 # Pascal Editor 可复用生产部署
本项目把 Pascal Editor 作为独立 Bun/Next.js 服务运行,并通过主站 Nginx Pascal Editor 的定制源码位于主仓库 `services/pascal-editor`,以独立
同域挂载。部署脚本会复用主站登录态、系统设置中显式标记的“多模态模型”和主站 Bun/Next.js 服务运行,并通过主站 Nginx 同域挂载。新服务器只需拉取
Git 仓库里的 `pascal-editor-ai-agent` 独立分支。 主仓库,无需再克隆或维护 Pascal 独立分支。
## 默认结构 ## 默认结构
| 项目 | 默认值 | | 项目 | 默认值 |
| --- | --- | | --- | --- |
| 主站目录 | 当前仓库目录 | | 主站源码 | 当前仓库目录 |
| Pascal 内置源码 | `services/pascal-editor` |
| Pascal 运行副本 | `/opt/pascal-editor` |
| Pascal 用户数据 | `/var/lib/pascal-editor` |
| 主站 PM2 服务 | `ai-chatbot` | | 主站 PM2 服务 | `ai-chatbot` |
| 主站内部地址 | `http://127.0.0.1:3125` |
| Pascal 源码 | `/opt/pascal-editor` |
| Pascal 数据 | `/var/lib/pascal-editor` |
| Pascal PM2 服务 | `pascal-editor` | | Pascal PM2 服务 | `pascal-editor` |
| Pascal 监听 | `127.0.0.1:3002` | | Pascal 监听 | `127.0.0.1:3002` |
| 同域路径 | `/pascal-service` | | 同域路径 | `/pascal-service` |
| Pascal Git 分支 | `pascal-editor-ai-agent` |
Pascal 端口不对公网开放。Nginx 会先通过主站 `/opt/pascal-editor` 是部署脚本生成的运行副本,不是第二个源码仓库,
`/api/internal/pascal/access` 校验登录 Cookie,再把可信用户 ID 注入 Pascal。 不应在该目录直接修改代码。Pascal 端口不对公网开放。Nginx 先通过
场景的创建、列表、读取、保存、删除、事件流和 AI Agent 都检查同一个 `/api/internal/pascal/access` 校验主站登录 Cookie,再注入可信用户 ID。
`ownerId`。 场景数据按 `ownerId` 隔离。
## 首次部署 ## 首次部署
在目标服务器先部署好主站并确认主站 Git remote 能读取 目标服务器部署好主站后执行:
`pascal-editor-ai-agent` 分支,然后执行:
```bash ```bash
cd /var/www/ai-chatbot cd /var/www/ai-chatbot
@ -41,39 +39,43 @@ sudo env \
脚本会按顺序: 脚本会按顺序:
1. 生成或复用 `PASCAL_PROXY_SECRET`,写入主站 `.env.local`。 1. 生成或复用 `PASCAL_PROXY_SECRET`,写入主站 `.env.local`。
2. 构建并重启主站。 2. 安装主站开发依赖,构建并重启主站。
3. 安装固定版本 Bun(默认 `1.3.0`)。 3. 安装固定版本 Bun(默认 `1.3.0`)和 `rsync`。
4. 克隆或快进 Pascal 独立分支。 4. 将 `services/pascal-editor` 同步为独立运行副本。
5. 写入仅 root 可读的 Pascal 生产环境文件。 5. 写入仅 root 可读的 Pascal 生产环境文件。
6. 安装依赖、构建 Pascal,并注册 PM2 服务。 6. 安装依赖、构建 Pascal,并注册 PM2 服务。
7. 渲染 Nginx 片段、执行 `nginx -t`,成功后热重载。 7. 渲染 Nginx 片段,校验成功后热重载。
8. 检查主站、Pascal、Agent 状态和主站模型网关。 8. 检查主站、Pascal、Agent 状态和主站模型网关。
主站环境文件和 Nginx 配置会备份到 `/var/backups/pascal-deploy`。Nginx 主站环境文件、Nginx 配置以及旧 Pascal Git 元数据会备份到
校验失败时,脚本会立即恢复执行前的配置。 `/var/backups/pascal-deploy`。Nginx 校验失败时会恢复原配置。
## 日常更新 ## 日常更新
主站和 Pascal 分支有新提交后,重复执行同一个命令即可。脚本只允许 Git 以后只需更新主仓库,并重复执行部署命令:
快进更新;如果 `/opt/pascal-editor` 有本地修改,会停止部署,不会覆盖现场
文件。 ```bash
git pull --ff-only origin main
sudo env PASCAL_PUBLIC_ORIGIN=https://example.com pnpm pascal:deploy
```
`rsync --delete` 会更新运行副本,但保留 `/opt/pascal-editor/.env.production`。
用户数据始终位于独立的 `PASCAL_DATA_DIR`,不会被代码同步覆盖。
只做无模型费用的健康检查: 只做无模型费用的健康检查:
```bash ```bash
sudo env \ sudo env PASCAL_PUBLIC_ORIGIN=https://example.com pnpm pascal:check
PASCAL_PUBLIC_ORIGIN=https://example.com \
pnpm pascal:check
``` ```
执行一次真实自然语言建模、验证节点已落库并自动删除临时场景: 执行一次真实自然语言建模,验证节点落库后删除临时场景:
```bash ```bash
sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke
``` ```
真实冒烟会调用当前系统设置中显式标记的“多模态模型”,因此默认不会在一键部署时 真实冒烟会调用系统设置中显式标记的“多模态模型”,默认不在一键部署
执行。需要每次部署自动执行时设置 `RUN_AGENT_SMOKE_TEST=true`。 时执行。需要每次部署自动执行时设置 `RUN_AGENT_SMOKE_TEST=true`。
## 常用覆盖项 ## 常用覆盖项
@ -83,16 +85,15 @@ sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke
| `MAIN_ENV_FILE` | 主站环境文件 | | `MAIN_ENV_FILE` | 主站环境文件 |
| `MAIN_PM2_NAME` | 主站 PM2 服务名 | | `MAIN_PM2_NAME` | 主站 PM2 服务名 |
| `MAIN_INTERNAL_URL` | Pascal 调用主站网关的地址 | | `MAIN_INTERNAL_URL` | Pascal 调用主站网关的地址 |
| `MAIN_NGINX_UPSTREAM` | Nginx 主站 upstream,例如 `127.0.0.1:3125` | | `MAIN_NGINX_UPSTREAM` | Nginx 主站 upstream |
| `BUILD_MAIN_APP=false` | 跳过主站安装、构建和重启 | | `BUILD_MAIN_APP=false` | 跳过主站安装、构建和重启 |
| `PASCAL_INSTALL_DIR` | Pascal 源码目录 | | `PASCAL_SOURCE_DIR` | Pascal 内置源码目录 |
| `PASCAL_DATA_DIR` | SQLite 数据目录 | | `PASCAL_INSTALL_DIR` | Pascal 运行副本目录 |
| `PASCAL_GIT_REPO` | Pascal 分支所在 Git 仓库;默认使用主站 origin | | `PASCAL_DATA_DIR` | SQLite 用户数据目录 |
| `PASCAL_GIT_REF` | Pascal 分支或标签 |
| `PASCAL_PORT` | Pascal 本地监听端口 | | `PASCAL_PORT` | Pascal 本地监听端口 |
| `PASCAL_BASE_PATH` | 同域挂载路径 | | `PASCAL_BASE_PATH` | 同域挂载路径 |
| `PASCAL_PUBLIC_ORIGIN` | 公网主站 Origin,不带末尾斜杠 | | `PASCAL_PUBLIC_ORIGIN` | 公网主站 Origin,不带末尾斜杠 |
| `NGINX_SITE_CONFIG` | 需要加入 Pascal include 的 HTTPS server 配置 | | `NGINX_SITE_CONFIG` | 需加入 Pascal include 的 HTTPS server 配置 |
| `INSTALL_NGINX=false` | 仅部署服务,不修改 Nginx | | `INSTALL_NGINX=false` | 仅部署服务,不修改 Nginx |
| `PASCAL_BUN_VERSION` | 固定 Bun 版本 | | `PASCAL_BUN_VERSION` | 固定 Bun 版本 |
| `PASCAL_BACKUP_DIR` | 环境与 Nginx 配置备份目录 | | `PASCAL_BACKUP_DIR` | 环境与 Nginx 配置备份目录 |
@ -103,9 +104,10 @@ sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke
pm2 status pm2 status
pm2 logs pascal-editor --lines 100 pm2 logs pascal-editor --lines 100
curl http://127.0.0.1:3002/pascal-service/api/health curl http://127.0.0.1:3002/pascal-service/api/health
cat /opt/pascal-editor/.pascal-source-version
nginx -t nginx -t
``` ```
升级时不要把主站源码复制到 Pascal 目录,也不要把本地测试数据库上传到 不要将本地测试数据库放进 `services/pascal-editor`,也不要直接修改
服务器。主站始终更新 `main`,Pascal 始终更新独立分支,用户场景只保存在 `/opt/pascal-editor`。代码只更新主仓 `main`,用户场景只保存在
`PASCAL_DATA_DIR`。 `PASCAL_DATA_DIR`。

View File

@ -12,9 +12,8 @@ MAIN_NGINX_UPSTREAM="${MAIN_NGINX_UPSTREAM:-127.0.0.1:3125}"
BUILD_MAIN_APP="${BUILD_MAIN_APP:-true}" BUILD_MAIN_APP="${BUILD_MAIN_APP:-true}"
PASCAL_INSTALL_DIR="${PASCAL_INSTALL_DIR:-/opt/pascal-editor}" PASCAL_INSTALL_DIR="${PASCAL_INSTALL_DIR:-/opt/pascal-editor}"
PASCAL_SOURCE_DIR="${PASCAL_SOURCE_DIR:-$MAIN_APP_DIR/services/pascal-editor}"
PASCAL_DATA_DIR="${PASCAL_DATA_DIR:-/var/lib/pascal-editor}" PASCAL_DATA_DIR="${PASCAL_DATA_DIR:-/var/lib/pascal-editor}"
PASCAL_GIT_REF="${PASCAL_GIT_REF:-pascal-editor-ai-agent}"
PASCAL_GIT_REPO="${PASCAL_GIT_REPO:-}"
PASCAL_PM2_NAME="${PASCAL_PM2_NAME:-pascal-editor}" PASCAL_PM2_NAME="${PASCAL_PM2_NAME:-pascal-editor}"
PASCAL_PORT="${PASCAL_PORT:-3002}" PASCAL_PORT="${PASCAL_PORT:-3002}"
PASCAL_NGINX_UPSTREAM="${PASCAL_NGINX_UPSTREAM:-127.0.0.1:${PASCAL_PORT}}" PASCAL_NGINX_UPSTREAM="${PASCAL_NGINX_UPSTREAM:-127.0.0.1:${PASCAL_PORT}}"
@ -127,6 +126,17 @@ install_bun() {
[ -x "$PASCAL_BUN_BIN" ] || die "Bun installation did not create $PASCAL_BUN_BIN" [ -x "$PASCAL_BUN_BIN" ] || die "Bun installation did not create $PASCAL_BUN_BIN"
} }
install_rsync() {
if command -v rsync >/dev/null 2>&1; then
return
fi
[ "$(id -u)" -eq 0 ] || die "Installing rsync requires root; install rsync manually"
require_command apt-get
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq rsync
}
configure_main_app() { configure_main_app() {
local secret="$1" local secret="$1"
local next_env_backup="" local next_env_backup=""
@ -176,29 +186,40 @@ configure_main_app() {
) )
} }
resolve_pascal_repo() { sync_pascal_source() {
if [ -n "$PASCAL_GIT_REPO" ]; then [ -f "$PASCAL_SOURCE_DIR/package.json" ] ||
printf '%s' "$PASCAL_GIT_REPO" die "Vendored Pascal source not found: $PASCAL_SOURCE_DIR"
return [ -f "$PASCAL_SOURCE_DIR/apps/editor/package.json" ] ||
fi die "Pascal editor package not found below $PASCAL_SOURCE_DIR"
git -C "$MAIN_APP_DIR" remote get-url origin [ "$PASCAL_INSTALL_DIR" != "/" ] || die "PASCAL_INSTALL_DIR cannot be /"
} [ "$PASCAL_INSTALL_DIR" != "$MAIN_APP_DIR" ] ||
die "PASCAL_INSTALL_DIR cannot overwrite MAIN_APP_DIR"
[ "$PASCAL_INSTALL_DIR" != "$PASCAL_SOURCE_DIR" ] ||
die "PASCAL_INSTALL_DIR must be separate from PASCAL_SOURCE_DIR"
install_rsync
mkdir -p "$PASCAL_INSTALL_DIR" "$PASCAL_BACKUP_DIR"
checkout_pascal() {
local repo="$1"
if [ -d "$PASCAL_INSTALL_DIR/.git" ]; then if [ -d "$PASCAL_INSTALL_DIR/.git" ]; then
[ -z "$(git -C "$PASCAL_INSTALL_DIR" status --porcelain)" ] || [ -z "$(git -C "$PASCAL_INSTALL_DIR" status --porcelain)" ] ||
die "$PASCAL_INSTALL_DIR has local Git changes; refusing to overwrite them" die "$PASCAL_INSTALL_DIR has legacy local Git changes; refusing to overwrite them"
git -C "$PASCAL_INSTALL_DIR" fetch origin "$PASCAL_GIT_REF" local legacy_git_backup="$PASCAL_BACKUP_DIR/legacy-pascal-git-$(date +%Y%m%d%H%M%S)"
git -C "$PASCAL_INSTALL_DIR" checkout "$PASCAL_GIT_REF" log "Archiving legacy Pascal Git metadata to $legacy_git_backup"
git -C "$PASCAL_INSTALL_DIR" pull --ff-only origin "$PASCAL_GIT_REF" mv "$PASCAL_INSTALL_DIR/.git" "$legacy_git_backup"
return
fi fi
[ ! -e "$PASCAL_INSTALL_DIR" ] || log "Syncing vendored Pascal source from $PASCAL_SOURCE_DIR"
die "$PASCAL_INSTALL_DIR exists but is not a Git checkout" rsync -a --delete \
mkdir -p "$(dirname "$PASCAL_INSTALL_DIR")" --exclude '.env.production' \
git clone --single-branch --branch "$PASCAL_GIT_REF" "$repo" "$PASCAL_INSTALL_DIR" --exclude '.git/' \
--exclude '.next/' \
--exclude '.turbo/' \
--exclude 'node_modules/' \
"$PASCAL_SOURCE_DIR/" "$PASCAL_INSTALL_DIR/"
if git -C "$MAIN_APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then
git -C "$MAIN_APP_DIR" rev-parse HEAD >"$PASCAL_INSTALL_DIR/.pascal-source-version"
fi
} }
write_pascal_env() { write_pascal_env() {
@ -226,7 +247,7 @@ build_pascal() {
mkdir -p "$PASCAL_DATA_DIR" mkdir -p "$PASCAL_DATA_DIR"
chmod 700 "$PASCAL_DATA_DIR" chmod 700 "$PASCAL_DATA_DIR"
export PATH="$(dirname "$PASCAL_BUN_BIN"):$PATH" export PATH="$(dirname "$PASCAL_BUN_BIN"):$PATH"
log "Installing and building Pascal from $PASCAL_GIT_REF" log "Installing and building vendored Pascal service"
( (
cd "$PASCAL_INSTALL_DIR" cd "$PASCAL_INSTALL_DIR"
"$PASCAL_BUN_BIN" install --frozen-lockfile "$PASCAL_BUN_BIN" install --frozen-lockfile
@ -369,7 +390,7 @@ main() {
[ "$(id -u)" -eq 0 ] || die "Production deployment must run as root" [ "$(id -u)" -eq 0 ] || die "Production deployment must run as root"
configure_main_app "$secret" configure_main_app "$secret"
install_bun install_bun
checkout_pascal "$(resolve_pascal_repo)" sync_pascal_source
write_pascal_env "$secret" write_pascal_env "$secret"
build_pascal build_pascal
start_pascal start_pascal

View File

@ -1,22 +1,13 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawn, spawnSync } from "node:child_process"; import { spawn, spawnSync } from "node:child_process";
import { import { existsSync, writeFileSync } from "node:fs";
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import path from "node:path"; import path from "node:path";
const cwd = process.cwd(); const cwd = process.cwd();
const pascalDir = path.resolve( const pascalDir = path.resolve(
cwd, cwd,
process.env.PASCAL_EDITOR_DIR || ".pascal-editor" process.env.PASCAL_EDITOR_DIR || "services/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"; const port = process.env.PASCAL_EDITOR_PORT || "3002";
function run(command, args, options = {}) { function run(command, args, options = {}) {
@ -36,72 +27,6 @@ function commandExists(command) {
return result.status === 0; 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) { function runPackageScript(packageManager, packageDir, scriptName) {
if (packageManager === "bun") { if (packageManager === "bun") {
run("bun", ["--cwd", packageDir, "run", scriptName], { cwd: pascalDir }); run("bun", ["--cwd", packageDir, "run", scriptName], { cwd: pascalDir });
@ -127,36 +52,18 @@ function ensureBuiltWorkspacePackages(packageManager) {
} }
} }
if (!existsSync(pascalDir)) { if (!existsSync(path.join(pascalDir, "apps/editor/package.json"))) {
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( console.error(
`${pascalDir} already exists but is not a git checkout. Move it away or set PASCAL_EDITOR_DIR.` `Vendored Pascal Editor source was not found at ${pascalDir}. Set PASCAL_EDITOR_DIR to override it.`
); );
process.exit(1); process.exit(1);
} }
ensureSparseCheckout();
patchCssImports();
const envLocalPath = path.join(pascalDir, ".env.local"); const envLocalPath = path.join(pascalDir, ".env.local");
if (!existsSync(envLocalPath)) { if (!existsSync(envLocalPath)) {
writeFileSync(envLocalPath, `PORT=${port}\n`, "utf8"); writeFileSync(envLocalPath, `PORT=${port}\n`, "utf8");
} }
patchNestedNextRoot();
const hasBun = commandExists("bun"); const hasBun = commandExists("bun");
const hasNpm = commandExists("npm"); const hasNpm = commandExists("npm");
const hasNodeModules = existsSync(path.join(pascalDir, "node_modules")); const hasNodeModules = existsSync(path.join(pascalDir, "node_modules"));

View File

@ -0,0 +1,19 @@
# Host Application Integration
This directory is the source of truth for the Pascal Editor service shipped
with the host application. It originated from `pascalorg/editor` and includes
the host authentication, user-scoped scene storage, Chinese localization, and
AI Agent integration maintained by this project.
## Boundaries
- The host application uses pnpm and does not install this workspace as part of
its own dependency graph.
- Pascal uses its own Bun lockfile and is built as an independent Next service.
- Production deploys copy this directory to `/opt/pascal-editor`.
- Runtime secrets remain in `/opt/pascal-editor/.env.production`.
- User scene data remains outside both source trees in `/var/lib/pascal-editor`.
Run `pnpm pascal:dev` from the host repository for local development. Run
`pnpm pascal:deploy` as root on a production server after pulling the host main
branch. Never commit runtime environment files or SQLite scene databases here.

View File

@ -31,5 +31,5 @@
"next.config.js", "next.config.js",
".next/dev/types/**/*.ts" ".next/dev/types/**/*.ts"
], ],
"exclude": ["node_modules"] "exclude": ["node_modules", "services/pascal-editor"]
} }