feat(deploy): 产物包部署链路与 naku 打包 CLI

Docker Compose 五服务 + Debian slim 单文件运行时;migrate 幂等门闩;
naku pack/build(citty + clack);部署方案文档入档。
This commit is contained in:
2026-10-13 00:56:43 +08:00
parent 86782c63ad
commit 905d4ce77e
18 changed files with 1050 additions and 6 deletions
+19
View File
@@ -0,0 +1,19 @@
# 构建上下文只保留 release/ 与 Caddyfile(Dockerfile 仅 COPY 这两处)
.git
.env
.env.*
!.env.example
node_modules
**/node_modules
apps
packages
scripts
docs
*.log
api*.json
session-archive
coverage
*.tsbuildinfo
.claude
.agents
agent
+5 -2
View File
@@ -1,12 +1,14 @@
# ── new-api-key-usage · 环境变量示例 ──────────────────────────
# 复制为 .env 后填写(.env 已被 .gitignore,勿提交)
# 开发:任意可用的 PostgreSQL,DATABASE_URL 指向即可
# 部署:放在服务器约定目录(不进产物包);chmod 600;与库同等备份
# 本服务端口(避开 3000/3001 等常见占用)
# 本服务端口(避开 3000/3001 等常见占用;compose 内 server 固定 18456)
PORT=18456
# PostgreSQL 连接串——数据库配置的唯一来源
# 指向你本机/已有的库(主机 127.0.0.1 或 localhost)
# 开发:指向你本机/已有的库(主机 127.0.0.1 或 localhost)
# 部署:同一份即可;compose 会把主机改写为容器服务名 postgres
# 密码含 @ : / 等字符时请 URL 编码
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/new_api_key_usage
@@ -22,6 +24,7 @@ NEWAPI_BASE=http://127.0.0.1:8080
# 会话 cookie 是否带 Secure(默认 true)。
# 本地 http://localhost 或 127.0.0.1 调试:现代浏览器可保留 true;
# 局域网 IP / 自定义域名 / 出现「登录后立刻 401」时改为 false。
# HTTPS + 域名部署务必 true。
COOKIE_SECURE=false
# 业务日志级别:debug | info | warning | error | fatal(访问日志另由 hono/logger 输出)
+3
View File
@@ -17,8 +17,11 @@
# 依赖与构建产物
node_modules/
dist/
bin/
build/
.turbo/
release/
.bun-cache/
# 测试与工具
coverage/
+4
View File
@@ -33,6 +33,10 @@ Bun 工具链(选型见 `docs/design/技术选型.md`)。**Windows 上 `bun`
| `bun run dev:engine` | 开发跑同步引擎(独立进程,读 `SYNC_*` env) |
| `bun run dev:web` | 开发跑前端 Vite |
| `bun run build:web` | 前端类型检查 + 产物构建 |
| `bun run build:server` | 编译后端单文件 → `bin/naku-server`(当前平台) |
| `bun run build:engine` | 编译同步引擎单文件 → `bin/naku-engine`(当前平台) |
| `bun run naku` | 打包/构建 CLI(citty + clack):`naku pack` 交互打部署包 → `release/`;`naku build` 本地构建 → `bin/`;支持 `--yes` / `--target` / `--only` |
| `bun run pack:release` | 非交互打部署包(= `naku pack --yes`,目标默认 linux-x64) |
环境变量见 `.env.example`(复制为 `.env` 后填写)。
+17
View File
@@ -0,0 +1,17 @@
# 站点地址按部署环境改:
# 本地/内网 HTTP → :80
# 公网 HTTPS → your.domain.example(Caddy 自动签证书)
:80 {
root * /srv
# 接口反代到 server 容器
handle /api/* {
reverse_proxy server:18456
}
# SPA:非 /api 的路径回退 index.html
handle {
try_files {path} /index.html
file_server
}
}
+20
View File
@@ -0,0 +1,20 @@
# 语法:docker/dockerfile:1
# 只装配 release/ 产物,构建期不编译业务代码
# ---------- 应用运行时 ----------
# 用 Debian slim:与 Bun --compile(glibc)同源,兼容面更稳;
# CA 与 libstdc++ 显式安装,不依赖 slim 是否自带
FROM debian:bookworm-slim AS app
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libstdc++6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY release/naku-server release/naku-engine release/naku-migrate /app/
COPY release/drizzle /app/drizzle
ENV MIGRATIONS_FOLDER=/app/drizzle
CMD ["/app/naku-server"]
# ---------- 网关(Caddy + 前端静态) ----------
FROM caddy:2-alpine AS proxy
COPY Caddyfile /etc/caddy/Caddyfile
COPY release/dist /srv
+37
View File
@@ -79,12 +79,49 @@ bun run dev:web # 前端 Vite(5175)
更多命令与协作约定见 [`AGENTS.md`](AGENTS.md)。
## 部署
方案详文见 [`docs/design/部署方案.md`](docs/design/部署方案.md)。形态一句话:**本地打产物包 → 覆盖服务器 `release/` → 一条 compose 命令 → 看容器**。
### 本地打包
```bash
bun run naku pack
# 交互选:目标平台 · 打包内容 · 是否 tar.gz
# 产出 release/:dist · naku-server · naku-engine · naku-migrate · drizzle/
# 非交互(CI / 一键):
bun run pack:release
# 或 bun run naku pack --yes --target=bun-linux-arm64 --only=dist,naku-server
```
### 服务器
首次安装:把 `docker-compose.yml` · `Dockerfile` · `.dockerignore` · `Caddyfile` · `entrypoint-*.sh` 放到约定目录(如 `/opt/naku/`),再放 `release/`,填写 `.env`(`chmod 600`)。日常更新只覆盖 `release/`。
```bash
cd /opt/naku
docker compose up -d --build
docker compose ps # migrate 应为 exited(0),其余 running/healthy
```
- 页面:`http://<服务器>/`;接口文档:`http://<服务器>/api/docs`
- 迁移:由 `migrate` 一次性服务在启动时幂等执行(journal),重复执行不会多灌
- HTTPS:`Caddyfile` 站点行改成域名,`.env` 设 `COOKIE_SECURE=true`,再执行固定命令
### 注意
1. `.env` 不进产物包、不进 git;含 `SK_KEY_ENCRYPTION_SECRET`,须与数据库同等备份
2. `COOKIE_SECURE` 必须与访问方式匹配:HTTP + `true` 会「登录后立刻 401」
3. `DATABASE_URL` 密码含 `@` `:` `/` 等字符时做 URL 编码
## 文档地图
| 文档 | 什么时候读 | 状态 |
|------|-----------|------|
| [`docs/design/需求清单.md`](docs/design/需求清单.md) | **需求权威索引与裁决档案**——R1–R16 定案;含支付拆表与成员档案/空壳删除裁决 | 定稿(2026-10-10) |
| [`docs/design/技术选型.md`](docs/design/技术选型.md) | 技术栈裁决:全栈 TS;Bun + Hono + Drizzle/pg;前端 Vue 3 + antdv-next;分层落地与回退 | 定稿(2026-10-02) |
| [`docs/design/部署方案.md`](docs/design/部署方案.md) | 母体交付与更新:产物包、服务器目录、容器编排、迁移时机、安全约定 | 已实现(定稿 2026-10-12) |
| [`docs/design/api_keys表字段设计.md`](docs/design/api_keys表字段设计.md) | key 登记、生命周期与成员归属 | 定稿(2026-10-01) |
| [`docs/design/usage_logs表字段设计.md`](docs/design/usage_logs表字段设计.md) | 日志明细、来源提列、去重与写入策略 | 同上 |
| [`docs/design/payments建模评估.md`](docs/design/payments建模评估.md) | 支付建模复盘:订阅权益 vs 充值入账两分;拆表已采纳 | 定稿(2026-10-09) |
+16
View File
@@ -5,7 +5,9 @@
"": {
"name": "new-api-key-usage",
"devDependencies": {
"@clack/prompts": "^1.8.1",
"@types/bun": "^1.4.2",
"citty": "^0.2.2",
"typescript": "^7.0.2",
},
},
@@ -180,6 +182,10 @@
"@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="],
"@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="],
"@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="],
"@csstools/color-helpers": ["@csstools/color-helpers@6.1.2", "", {}, "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow=="],
"@csstools/css-calc": ["@csstools/css-calc@3.4.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA=="],
@@ -714,6 +720,8 @@
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
@@ -822,6 +830,12 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
"fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -1120,6 +1134,8 @@
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
+98
View File
@@ -0,0 +1,98 @@
# 一键部署:docker compose up -d --build
# 验收:docker compose ps(migrate 应为 exited 0)
# 环境变量写在 .env(不进产物包;生产务必改密钥与 COOKIE_SECURE)
# 注意:DATABASE_URL 密码含 @ : / 等字符时需 URL 编码
services:
postgres:
container_name: naku-postgres
image: postgres:17-alpine
entrypoint: ["/bin/sh", "/entrypoint-pg.sh"]
environment:
DATABASE_URL: ${DATABASE_URL:?set DATABASE_URL in .env}
volumes:
- ./entrypoint-pg.sh:/entrypoint-pg.sh:ro
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$(echo $$DATABASE_URL | sed -E 's#^[^:]+://([^:]+):.*#\\1#')"]
interval: 5s
timeout: 3s
retries: 12
restart: unless-stopped
migrate:
container_name: naku-migrate
build:
context: .
target: app
entrypoint: ["/bin/sh", "/entrypoint-app.sh"]
command: ["/app/naku-migrate"]
environment:
DATABASE_URL: ${DATABASE_URL}
MIGRATIONS_FOLDER: /app/drizzle
volumes:
- ./entrypoint-app.sh:/entrypoint-app.sh:ro
depends_on:
postgres:
condition: service_healthy
restart: "no"
server:
container_name: naku-server
build:
context: .
target: app
entrypoint: ["/bin/sh", "/entrypoint-app.sh"]
command: ["/app/naku-server"]
environment:
PORT: "18456"
DATABASE_URL: ${DATABASE_URL}
SK_KEY_ENCRYPTION_SECRET: ${SK_KEY_ENCRYPTION_SECRET:?set SK_KEY_ENCRYPTION_SECRET in .env}
NEWAPI_BASE: ${NEWAPI_BASE:?set NEWAPI_BASE in .env}
COOKIE_SECURE: ${COOKIE_SECURE:-true}
LOG_LEVEL: ${LOG_LEVEL:-info}
ADMIN_INIT_USERNAME: ${ADMIN_INIT_USERNAME:-}
ADMIN_INIT_PASSWORD: ${ADMIN_INIT_PASSWORD:-}
MIGRATIONS_FOLDER: /app/drizzle
volumes:
- ./entrypoint-app.sh:/entrypoint-app.sh:ro
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
engine:
container_name: naku-engine
build:
context: .
target: app
entrypoint: ["/bin/sh", "/entrypoint-app.sh"]
command: ["/app/naku-engine"]
environment:
DATABASE_URL: ${DATABASE_URL}
SK_KEY_ENCRYPTION_SECRET: ${SK_KEY_ENCRYPTION_SECRET}
NEWAPI_BASE: ${NEWAPI_BASE}
SYNC_INTERVAL_MS: ${SYNC_INTERVAL_MS:-300000}
LOG_LEVEL: ${LOG_LEVEL:-info}
MIGRATIONS_FOLDER: /app/drizzle
volumes:
- ./entrypoint-app.sh:/entrypoint-app.sh:ro
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
caddy:
container_name: naku-caddy
build:
context: .
target: proxy
ports:
- "80:80"
- "443:443"
depends_on:
- server
restart: unless-stopped
volumes:
pgdata:
+327
View File
@@ -0,0 +1,327 @@
# 部署方案
> 母体(完整前后端 + PostgreSQL)的交付与更新方案:产物包形态、服务器目录约定、容器编排、迁移时机、安全边界与固定操作流程。**范围**:只定「东西怎么装到运营方服务器、怎么更新」;不改业务语义,不覆盖 R13 的 CFW 抽取版部署面。设计对齐作者现场运维心智——**本地产物 → 传到服务器约定目录 → 一条固定命令 → 看容器状态**。
| 项目 | 内容 |
|------|------|
| 状态 | 已实现(定稿 2026-10-12) |
| 关联 | `技术选型.md`(Bun / Hono / Drizzle / Vue)· `同步引擎设计.md`(独立 `apps/engine` 进程)· `需求清单.md` R13/R14 · `users表字段设计.md`(部署期初始化)· `README.md` |
| 产出 | 产物包布局 · 服务器目录 · Dockerfile / Compose / Caddy 要点 · 打包与更新流程 · 迁移与安全约定 |
---
## 1. 设计约束(来自现场经验与项目事实)
1. **操作者心智**(作者工作流,优先对齐):
- 前端:本地打包 `dist/` → 放到服务器指定目录 → 执行固定命令 → `docker ps` 看容器全绿;
- 后端:拿到 `jar` 类产物 → 上传指定目录 → 同一固定命令 → 看容器。
2. **仓库事实**:`apps/server` 与 `apps/engine` 两个独立进程;前端为静态 `dist`(server 不托管静态);数据在 PostgreSQL;配置全走 env;`packages/db` 已有按 journal 的幂等 `runMigrations`。
3. **能力边界**:作者是前端,Docker 不娴熟;服务器侧只需 Docker,不要求 Bun / Node / 源码构建能力。
4. **协作约定**:交付物保证「一条命令链」可部署(AGENTS);密钥 `SK_KEY_ENCRYPTION_SECRET` 丢失 = 已登记 key 全部无法解密。
由此收敛出的形态一句话:**常驻 Docker 编排 + 每次更新只替换产物包 + 一条 compose 命令完成重建/重启/迁表。**
---
## 2. 总体结构
```text
本地(打包) 服务器(运营方)
────────── ────────────────
bun run pack:release /opt/naku/ ← 约定目录
├─ 前端 dist/ ├─ docker-compose.yml
├─ naku-server(linux 单文件) ├─ Dockerfile
├─ naku-engine(linux 单文件) ├─ .dockerignore
├─ naku-migrate(linux 单文件) ├─ Caddyfile
└─ drizzle/(迁移 SQL + journal) ├─ entrypoint-pg.sh
│ ├─ entrypoint-app.sh
▼ ├─ .env ← 密钥在此,不进包
release/ ──── 上传覆盖 ──────────► └─ release/ ← 每次更新整目录覆盖
固定命令: docker compose up -d --build
验收: docker compose ps
```
| 角色 | 职责 | 谁改 |
|------|------|------|
| 常驻编排(compose / Dockerfile / Caddyfile / entrypoint-*.sh / .env) | 描述系统怎么跑 | 装机时放好;改架构时才动 |
| 产物包 `release/` | 当前要跑的版本 | 每次发版整目录替换 |
首次安装与日常更新**同一套命令**;差别只是首次多一步「把常驻编排放到服务器」。
---
## 3. 产物包 `release/`
本地 `bun run pack:release` 产出,内容固定为:
```text
release/
dist/ # apps/web 构建产物(index.html · assets/…)
naku-server # Bun --compile,目标平台 linux-x64
naku-engine # 同上
naku-migrate # 同上;只跑迁移后退出
drizzle/ # packages/db/drizzle/ 的 SQL + meta/_journal.json
```
### 3.1 打包约定
| 项 | 约定 |
|----|------|
| 命令入口 | 优先 `bun run naku pack`(交互:平台 / 内容 / tar.gz);一键等价 `bun run pack:release` 或 `naku pack --yes` |
| 脚本落点 | `scripts/cli.ts`(citty + clack)· `scripts/pack-core.ts`(纯打包逻辑)· `scripts/pack-release.ts`(非交互兼容入口) |
| 目标平台 | 默认 `bun-linux-x64`;`--target=` 可切 `bun-linux-arm64` / `bun-darwin-*` / `bun-windows-*`(含 arm64) |
| 构建顺序 | 清空输出目录 → `build:web` → 三次 `--compile` → 拷 `drizzle/` →(可选)tar.gz |
| 禁止入包 | `.env` · 源码 · `node_modules` · 任何密钥 |
| 校验 | 打包后检查五条目都在且非空(Windows 本机编译允许 `.exe` 后缀);缺一即失败退出 |
| 本地试跑 | `naku build` / `build:server` / `build:engine` → `bin/`(当前平台),与部署包 `release/` 分离 |
### 3.2 为何是单文件而不是「服务器上构建」或「上传源码」
- 对齐 `jar` 心智:产物即交付物,服务器零构建依赖;
- 更新体积小、失败面小(不跑 `bun install` / `vite build`);
- 运行时镜像只需 Debian slim + `ca-certificates` + `libstdc++6`,不需要 Bun。
代价:目标平台写在打包脚本里;换 CPU 架构需重新打包(在脚本参数上留出口即可)。
---
## 4. 服务器目录(`/opt/naku/`)
路径可改,下文以 `/opt/naku` 指代「约定目录」。
```text
/opt/naku/
docker-compose.yml
Dockerfile
.dockerignore
Caddyfile
entrypoint-pg.sh
entrypoint-app.sh
.env
release/ ← 上传覆盖,不入库
```
---
## 5. 配置文件设计
### 5.1 `Dockerfile`(两阶段,只吃 `release/`)
```dockerfile
# ---------- 应用运行时 ----------
# 用 Debian slim:与 Bun --compile(glibc)同源,兼容面更稳;
# CA 与 libstdc++ 显式安装,不依赖 slim 是否自带
FROM debian:bookworm-slim AS app
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libstdc++6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY release/naku-server release/naku-engine release/naku-migrate /app/
COPY release/drizzle /app/drizzle
ENV MIGRATIONS_FOLDER=/app/drizzle
CMD ["/app/naku-server"]
# ---------- 网关(Caddy + 前端静态) ----------
FROM caddy:2-alpine AS proxy
COPY Caddyfile /etc/caddy/Caddyfile
COPY release/dist /srv
```
要点:
- **构建期不编译业务代码**,只装配产物;`docker compose up -d --build` 的语义是「用当前 `release/` 重建镜像并重启」;
- `app` 阶段一个镜像供 `server` / `engine` / `migrate` 三用,靠 compose 的 `command` 区分;
- 应用运行时选 **`debian:bookworm-slim`**(glibc,对齐 Bun `--compile` 目标);`ca-certificates`(HTTPS 证书校验)与 `libstdc++6`(单文件 C++ 运行时)显式安装——两个都不指望镜像自带;
- 网关仍用官方 `caddy:2-alpine`(与应用基底无关,体积友好);
- PostgreSQL 使用官方镜像,不进本 Dockerfile。
### 5.2 `docker-compose.yml`(五服务)
| 服务 | 来源 | command | 说明 |
|------|------|---------|------|
| `postgres` | `postgres:17-alpine` | 官方入口 | 卷 `pgdata`;`healthcheck` 用 `pg_isready` |
| `migrate` | `target: app` | `/app/naku-migrate` | **一次性**:`restart: "no"`;迁完退出 |
| `server` | `target: app` | `/app/naku-server` | 管理面 / 成员视图 API |
| `engine` | `target: app` | `/app/naku-engine` | 轮询同步 |
| `caddy` | `target: proxy` | 官方入口 | **唯一对外**:`80` / `443` |
依赖关系:
```text
postgres (healthy) ──► migrate (completed successfully) ──► server ──► caddy
└──────► engine
```
要点:
1. **`migrate` 独立服务**,而不是塞进 server 启动:语义是「部署时迁表」,避免 server/engine 双进程抢迁;`depends_on: service_completed_successfully` 保证先迁后跑。
2. **环境变量**:`DATABASE_URL` · `SK_KEY_ENCRYPTION_SECRET` · `NEWAPI_BASE` · `COOKIE_SECURE` · `LOG_LEVEL` · `SYNC_INTERVAL_MS` · 可选 `ADMIN_INIT_*`——均从 `.env` 注入 `server` / `engine`(`migrate` 只需要 `DATABASE_URL`)。
3. **`DATABASE_URL` 单源**:用户 / 密码 / 库名都写在这一串里;`entrypoint-pg.sh` 解析出 `POSTGRES_USER/PASSWORD/DB` 喂给官方库镜像,`entrypoint-app.sh` 把主机改写为 compose 服务名 `postgres`。开发与部署共用同一份 `.env` 语义。
4. 端口:仅 `caddy` 映射 `80:80`、`443:443`;`server` / `engine` / `postgres` 均不对外。
5. **容器名固定**(`container_name`):`naku-postgres` / `naku-migrate` / `naku-server` / `naku-engine` / `naku-caddy`——单副本约定下使用;服务间 DNS 仍用服务名(如 `server:18456`)。
### 5.3 `Caddyfile`
```caddyfile
# 站点地址按部署环境改:
# 本地/内网 HTTP → :80
# 公网 HTTPS → your.domain.example(Caddy 自动签证书)
:80 {
root * /srv
handle /api/* {
reverse_proxy server:18456
}
handle {
try_files {path} /index.html
file_server
}
}
```
与 `API契约-公共约定` 的命名空间约定一致:`/api/*` 归接口,其余归 SPA(非 `/api` 回退 `index.html`)。
### 5.4 `entrypoint-pg.sh` / `entrypoint-app.sh`
| 脚本 | 职责 |
|------|------|
| `entrypoint-pg.sh` | 从 `DATABASE_URL` 拆出 `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB`,`exec` 官方 `docker-entrypoint.sh postgres` |
| `entrypoint-app.sh` | 将 `DATABASE_URL` 的主机段改写为 `postgres`(保留端口与路径),再 `exec "$@"` 启动传入的二进制 |
二者属**常驻编排**,打进镜像或只读挂载皆可(建议只读挂载,改脚本不用重建镜像)。
### 5.5 `packages/db/src/migrate-cli.ts`(新增入口)
```text
loadEnv → createDb(DATABASE_URL) → runMigrations(db, MIGRATIONS_FOLDER) → 退出码
```
- 复用已有 `runMigrations`(Drizzle migrator,**按 journal 幂等**:已应用的迁移号跳过);
- 失败打日志并以非 0 退出,使 `migrate` 服务失败 → `server`/`engine` 不会启动(`service_completed_successfully` 门闩)。
---
## 6. 操作流程
### 6.1 首次安装
```bash
# 本地
bun run pack:release
# 服务器
# 将 docker-compose.yml · Dockerfile · .dockerignore · Caddyfile · entrypoint-*.sh 放入 /opt/naku/
# 将 release/ 上传到 /opt/naku/release/
cp .env.example /opt/naku/.env # 再编辑填写
chmod 600 /opt/naku/.env
cd /opt/naku
docker compose up -d --build
docker compose ps
```
### 6.2 日常更新(与首次同一命令)
```bash
# 本地
bun run pack:release
# 上传,整目录覆盖 /opt/naku/release/
# 服务器(固定命令)
cd /opt/naku
docker compose up -d --build
docker compose ps
```
- 有表变更时,新 `release/drizzle/` 自动带新 SQL,`migrate` 启动时补跑——无需手工;
- 观察构建/迁移日志:`docker compose logs migrate`;同步:`docker compose logs -f engine`。
### 6.3 HTTPS(可选)
1. `Caddyfile` 站点行 `:80` → 域名;
2. `.env` 中 `COOKIE_SECURE=true`;
3. 重新执行固定命令。
### 6.4 验收标准
- `docker compose ps`:`postgres` healthy,`server` / `engine` / `caddy` running,`migrate` exited(0);
- 浏览器打开站点可见页面;`/api/docs` 可访问;
- `docker compose logs engine` 出现就绪/轮次日志(已配置 key 时)。
---
## 7. 迁移策略(裁决)
| 问题 | 裁决 |
|------|------|
| 会不会「多灌几次」? | **不会。** 使用 Drizzle `migrate()`(journal 表),重复执行只应用未跑过的迁移号 |
| 谁来跑? | compose 一次性服务 `migrate`(`naku-migrate` 单文件),不随 server/engine 长驻 |
| 何时跑? | 每次固定命令(含首次)启动/更新时 |
| 不用什么? | **不用 `db:push` 进部署链**——它是 schema diff 硬推,不适合生产更新命令 |
| 失败怎么办? | `migrate` 非 0 退出 → `server`/`engine` 不启动;修复 SQL 或数据后重跑固定命令 |
---
## 8. 安全约定
| 点 | 约定 |
|----|------|
| `.env` 位置 | 只在服务器约定目录;**不进产物包、不进 git** |
| 权限 | `chmod 600 .env`,仅部署用户可读 |
| `SK_KEY_ENCRYPTION_SECRET` | 生成一次、入库后不可更换;**与数据库同等备份**(`.env` 必须备份) |
| 对外暴露 | 仅 Caddy 的 80/443;库与应用进程不映射公网端口 |
| `COOKIE_SECURE` | 必须与访问方式匹配:HTTP + `true` 会「登录后立刻 401」;HTTPS/域名用 `true` |
| 日志 | 不打印密钥与完整 `DATABASE_URL`(含密码) |
| 范围说明 | 单运营方内部服务器场景;不上 vault/KMS。若未来有多租户或公有多环境,再升档 |
---
## 9. 与开发方式的关系
| | 开发 | 部署 |
|--|------|------|
| 进程 | `bun run dev:server` / `dev:engine` / `dev:web` | compose:`server` / `engine` / `caddy` |
| 库 | 任意 PostgreSQL,`DATABASE_URL` 指向即可 | compose 内 `postgres`,同一 `DATABASE_URL` 语义(主机由 entrypoint 改写) |
| 建表 | `bun run db:push`(开发期) | `migrate` 服务(生产幂等迁移) |
| 前端 | Vite dev server | `release/dist` 由 Caddy 托管 |
| 配置 | `.env` | 服务器 `.env` |
开发**不依赖**本方案的 Docker 配置;部署也**不依赖**本机 Bun。
---
## 10. 文件改动清单
| 文件 | 动作 |
|------|------|
| `scripts/cli.ts` · `scripts/pack-core.ts` · `scripts/pack-release.ts` | 打包/构建 CLI 与核心逻辑 |
| `packages/db/src/migrate-cli.ts` | 迁移 CLI 入口 |
| `package.json` | `naku` · `pack:release` · `build:server` · `build:engine` |
| `Dockerfile` · `docker-compose.yml` · `.dockerignore` | 两阶段镜像 · 五服务(含 `container_name`) |
| `Caddyfile` · `entrypoint-pg.sh` · `entrypoint-app.sh` | 与 compose 同级平铺 |
| `README.md` | 「部署」章与文档地图 |
| `AGENTS.md` | 命令表(pack / build / naku) |
| `.env.example` | 部署语义注释 |
---
## 11. 已知取舍(否决时改这里)
1. **`migrate` 独立单文件**:多一个产物,换来「先迁后跑、失败不启」的清晰门闩。若嫌多,可改为 server 启动时迁(仍幂等),但失去 migrate 失败阻断 engine 的不对称控制。
2. **目标平台写死 linux-x64**:匹配常见服务器;ARM 用打包参数切换,不为此引入交叉编译矩阵。
3. **`DATABASE_URL` 单源 + 两个 entrypoint**:少一处密码不一致;多两个常驻小 shell。若偏好 compose 原生 `POSTGRES_*`,需接受密码在 `.env` 写两处(或 `DATABASE_URL` 仅应用用)。
4. **产物包而非源码包**:服务器零构建;代价是发版必须在本地(或 CI)跑 `pack:release`。
---
## 12. 验收清单(实现后)
- [ ] `bun run naku pack --yes`(或 `pack:release`)在 Windows 产出完整 `release/`(五条目齐全)
- [ ] 首次安装按 §6.1 一次命令链可起,`docker compose ps` 形态符合 §6.4
- [ ] 仅替换 `release/` 后重跑固定命令,版本切换成功且数据保留(卷不重建)
- [ ] 连续两次固定命令:第二次 `migrate` 不重复应用已存在迁移(日志可证)
- [ ] 故意放一份坏 SQL:`migrate` 失败,`server`/`engine` 未启动
- [ ] `COOKIE_SECURE` 与 HTTP/HTTPS 交叉场景符合 §8
- [ ] 产物包内无 `.env`、无源码、无 `node_modules`
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# 把 DATABASE_URL 的主机改写为 compose 服务名 postgres,再启动应用
set -e
if [ -z "$DATABASE_URL" ]; then
echo "DATABASE_URL is required" >&2
exit 1
fi
export DATABASE_URL="$(echo "$DATABASE_URL" | sed -E 's#(@)[^:/]+(:[0-9]+)?(/)#\1postgres\2\3#')"
exec "$@"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# 从 DATABASE_URL 拆出官方镜像建库所需的 POSTGRES_*,再交回原入口
set -e
if [ -z "$DATABASE_URL" ]; then
echo "DATABASE_URL is required" >&2
exit 1
fi
u="${DATABASE_URL#*://}"
auth="${u%%@*}"
rest="${u#*@}"
export POSTGRES_USER="${auth%%:*}"
export POSTGRES_PASSWORD="${auth#*:}"
export POSTGRES_DB="${rest#*/}"
export POSTGRES_DB="${POSTGRES_DB%%\?*}"
exec docker-entrypoint.sh postgres
+13 -3
View File
@@ -2,7 +2,10 @@
"name": "new-api-key-usage",
"private": true,
"type": "module",
"workspaces": ["apps/*", "packages/*"],
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"dev:server": "bun --hot --env-file=.env apps/server/src/index.ts",
@@ -11,10 +14,17 @@
"db:generate": "bun run --cwd packages/db db:generate",
"db:push": "bun run --cwd packages/db db:push",
"db:studio": "bun run --cwd packages/db db:studio",
"build:web": "bun run --cwd apps/web build"
"build:web": "bun run --cwd apps/web build",
"build:server": "bun build --compile --outfile=bin/naku-server apps/server/src/index.ts",
"build:engine": "bun build --compile --outfile=bin/naku-engine apps/engine/src/index.ts",
"pack:release": "bun run scripts/pack-release.ts",
"naku": "bun run scripts/cli.ts"
},
"devDependencies": {
"@clack/prompts": "^1.8.1",
"@types/bun": "^1.4.2",
"citty": "^0.2.2",
"typescript": "^7.0.2"
}
},
"packageManager": "bun@1.4.2"
}
+23
View File
@@ -0,0 +1,23 @@
import { createDb, runMigrations } from "./index.ts";
/**
* 部署用迁移入口:按 journal 幂等应用 packages/db/drizzle,成功 exit 0。
* 只依赖 DATABASE_URL;MIGRATIONS_FOLDER 缺省为同目录 ./drizzle。
*/
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("[migrate] DATABASE_URL is required");
process.exit(1);
}
const migrationsFolder = process.env.MIGRATIONS_FOLDER ?? "./drizzle";
const db = createDb(databaseUrl);
try {
await runMigrations(db, migrationsFolder);
console.log(`[migrate] ok (folder=${migrationsFolder})`);
process.exit(0);
} catch (err) {
console.error("[migrate] failed:", err);
process.exit(1);
}
+255
View File
@@ -0,0 +1,255 @@
/**
* naku 打包/构建 CLI:citty 定义命令,@clack/prompts 负责交互。
* 有 TTY 且未给全 flag 时进交互;--yes / 无 TTY 则纯 flag。
*/
import { defineCommand, runMain } from "citty";
import * as p from "@clack/prompts";
import {
PACK_ITEMS,
PACK_TARGETS,
currentPlatformTarget,
packRelease,
type PackItem,
} from "./pack-core.ts";
function isInteractive(yes: boolean | undefined): boolean {
if (yes) return false;
return Boolean(process.stdin.isTTY);
}
function cancel(): never {
p.cancel("Operation cancelled");
process.exit(0);
}
function assertNotCancel<T>(value: T | symbol): T {
if (p.isCancel(value)) cancel();
return value as T;
}
const targetOption = {
target: {
type: "string",
description: `编译目标,如 ${PACK_TARGETS[0]};默认交互选择或 ${PACK_TARGETS[0]}`,
},
} as const;
const pack = defineCommand({
meta: {
name: "pack",
description: "打部署产物包(dist + 单文件 + drizzle)",
},
args: {
...targetOption,
only: {
type: "string",
description: `产物范围,逗号分隔;可选 ${PACK_ITEMS.join("|")}`,
},
out: {
type: "string",
default: "release",
description: "输出目录",
},
tarball: {
type: "boolean",
description: "额外打 tar.gz(默认交互询问,否则否)",
},
yes: {
type: "boolean",
alias: "y",
description: "全部默认,不进交互",
},
},
async run({ args }) {
const interactive = isInteractive(args.yes as boolean | undefined);
p.intro("NAKU 打包");
// --- target ---
let target = args.target as string | undefined;
if (!target) {
if (interactive) {
const picked = assertNotCancel(
await p.select({
message: "目标平台",
options: (() => {
const current = currentPlatformTarget();
const base = PACK_TARGETS.map((t) => ({
label: t === "bun-linux-x64" ? `${t}(服务器常用)` : t,
value: t,
}));
// 当前平台已在列表中则不再追加,避免重复
return PACK_TARGETS.includes(current as (typeof PACK_TARGETS)[number])
? base
: [
...base,
{
label: `${current}(当前平台,仅本地)`,
value: current,
},
];
})(),
initialValue: "bun-linux-x64",
}),
);
target = picked as string;
} else {
target = "bun-linux-x64";
}
}
// --- only ---
let only: PackItem[];
const onlyArg = args.only as string | undefined;
if (onlyArg) {
only = onlyArg
.split(",")
.map((s) => s.trim())
.filter(Boolean) as PackItem[];
for (const item of only) {
if (!PACK_ITEMS.includes(item)) {
p.cancel(`未知产物: ${item}`);
process.exit(1);
}
}
} else if (interactive) {
const picked = assertNotCancel(
await p.multiselect({
message: "打包内容",
options: PACK_ITEMS.map((item) => ({ label: item, value: item })),
initialValues: [...PACK_ITEMS],
required: true,
}),
);
only = picked as PackItem[];
} else {
only = [...PACK_ITEMS];
}
// --- out ---
const outDir = (args.out as string) || "release";
// --- tarball ---
let tarball = args.tarball as boolean | undefined;
if (tarball === undefined) {
if (interactive) {
tarball = assertNotCancel(
await p.confirm({ message: "是否额外打 tar.gz?", initialValue: false }),
) as boolean;
} else {
tarball = false;
}
}
const s = p.spinner();
s.start(`编译 ${target} · ${only.join(" + ")}`);
try {
const out = await packRelease({
target,
only,
outDir,
tarball,
onLog: (msg) => {
// spinner 期间不逐条刷屏,保留关键节点可在结束后看
if (msg.startsWith("tar ") || msg.startsWith("target=")) {
s.message(msg);
}
},
});
s.stop(`完成 → ${out}`);
p.outro(
[
`产物:${only.join(" · ")}`,
`目标:${target}`,
tarball ? "已附 tar.gz(仓库根目录)" : "未打 tar.gz",
"",
"服务器:覆盖 /opt/naku/release/ 后执行 docker compose up -d --build",
].join("\n"),
);
} catch (err) {
s.stop("失败");
p.cancel(err instanceof Error ? err.message : String(err));
process.exit(1);
}
},
});
const build = defineCommand({
meta: {
name: "build",
description: "构建产物到 bin/(当前平台,适合本地试跑)",
},
args: {
item: {
type: "positional",
required: false,
description: `可选:${PACK_ITEMS.join("|")},不填则交互多选`,
},
...targetOption,
yes: {
type: "boolean",
alias: "y",
description: "全部默认,不进交互",
},
},
async run({ args }) {
const interactive = isInteractive(args.yes as boolean | undefined);
const target =
(args.target as string | undefined) ?? currentPlatformTarget();
let only: PackItem[];
const positional = args.item as string | undefined;
if (positional) {
if (!PACK_ITEMS.includes(positional as PackItem)) {
p.cancel(`未知产物: ${positional}`);
process.exit(1);
}
only = [positional as PackItem];
} else if (interactive) {
p.intro("NAKU 本地构建");
only = assertNotCancel(
await p.multiselect({
message: "构建什么",
options: PACK_ITEMS.map((item) => ({ label: item, value: item })),
// 默认不选,由用户自选
initialValues: [],
required: true,
}),
) as PackItem[];
} else {
only = ["naku-server", "naku-engine"];
}
const s = p.spinner();
s.start(`构建 ${only.join(" + ")} → bin/`);
try {
await packRelease({
target,
only,
outDir: "bin",
tarball: false,
// 前端进 bin/dist/,避免与单文件混放
nestDist: true,
onLog: (msg) => {
if (msg.startsWith("compile") || msg.startsWith("build")) s.message(msg);
},
});
s.stop("完成");
p.outro(`已写入 bin/(${target})`);
} catch (err) {
s.stop("失败");
p.cancel(err instanceof Error ? err.message : String(err));
process.exit(1);
}
},
});
const main = defineCommand({
meta: {
name: "naku",
version: "0.0.0",
description: "NAKU 打包 / 本地构建",
},
subCommands: { pack, build },
});
runMain(main);
+153
View File
@@ -0,0 +1,153 @@
/**
* 打包核心:供 CLI / 脚本共用,不带交互。
* 产物:dist/ · naku-server · naku-engine · naku-migrate · drizzle/
*/
import { cp, mkdir, rm, stat, readdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
export const PACK_ITEMS = [
"dist",
"naku-server",
"naku-engine",
"naku-migrate",
"drizzle",
] as const;
export type PackItem = (typeof PACK_ITEMS)[number];
export const PACK_TARGETS = [
"bun-linux-x64",
"bun-linux-arm64",
"bun-darwin-arm64",
"bun-darwin-x64",
"bun-windows-x64",
"bun-windows-arm64",
] as const;
export function currentPlatformTarget(): string {
const arch = process.arch === "arm64" ? "arm64" : "x64";
if (process.platform === "win32") return `bun-windows-${arch}`;
if (process.platform === "darwin") return `bun-darwin-${arch}`;
return `bun-linux-${arch}`;
}
export interface PackOptions {
target: string;
only: PackItem[];
outDir: string;
tarball?: boolean;
root?: string;
/** true:前端放到 <out>/dist/(部署包);false:直接铺在 <out>/(本地 dist) */
nestDist?: boolean;
onLog?: (msg: string) => void;
}
function fail(msg: string): never {
throw new Error(msg);
}
async function mustExist(path: string, label: string): Promise<void> {
if (!existsSync(path)) fail(`missing ${label}: ${path}`);
}
async function nonEmptyDir(path: string, label: string): Promise<void> {
const entries = await readdir(path);
if (entries.length === 0) fail(`empty ${label}: ${path}`);
}
async function run(cmd: string[], cwd: string, onLog?: (m: string) => void) {
onLog?.(`$ ${cmd.join(" ")}`);
const proc = Bun.spawn(cmd, { cwd, stdout: "inherit", stderr: "inherit" });
const code = await proc.exited;
if (code !== 0) fail(`command failed (${code}): ${cmd.join(" ")}`);
}
export async function packRelease(opts: PackOptions): Promise<string> {
const root = opts.root ?? resolve(import.meta.dir, "..");
const releaseDir = resolve(root, opts.outDir);
const log = opts.onLog ?? (() => {});
const only = new Set(opts.only);
const needBin = (name: PackItem) => only.has(name);
log(`target=${opts.target}`);
log(`out=${releaseDir}`);
// 输出目录:只删本次会写入的条目,避免误删未选产物的旧文件?——部署包语义为整包覆盖,仍清空
await rm(releaseDir, { recursive: true, force: true });
await mkdir(releaseDir, { recursive: true });
if (needBin("dist")) {
log("build web…");
await run(["bun", "run", "build:web"], root, log);
const webDist = join(root, "apps", "web", "dist");
await mustExist(webDist, "web dist");
const dest = opts.nestDist === false ? releaseDir : join(releaseDir, "dist");
await cp(webDist, dest, { recursive: true });
}
const compileOne = async (entry: string, name: PackItem) => {
if (!needBin(name)) return;
log(`compile ${name}…`);
await run(
[
"bun",
"build",
"--compile",
`--target=${opts.target}`,
`--outfile=${join(releaseDir, name)}`,
entry,
],
root,
log,
);
};
await compileOne("apps/server/src/index.ts", "naku-server");
await compileOne("apps/engine/src/index.ts", "naku-engine");
await compileOne("packages/db/src/migrate-cli.ts", "naku-migrate");
if (needBin("drizzle")) {
const drizzleSrc = join(root, "packages", "db", "drizzle");
await mustExist(drizzleSrc, "drizzle folder");
await cp(drizzleSrc, join(releaseDir, "drizzle"), { recursive: true });
}
for (const item of opts.only) {
if (item === "dist" && opts.nestDist === false) {
// web 铺在 outDir 根:检查 outDir 非空即可
await nonEmptyDir(releaseDir, "outDir");
continue;
}
// Windows 本机编译可能带 .exe
const candidates = [
join(releaseDir, item),
join(releaseDir, `${item}.exe`),
];
const found = candidates.find((c) => existsSync(c));
if (!found) fail(`missing ${item}: ${candidates[0]}`);
if (item === "dist" || item === "drizzle") {
await nonEmptyDir(found, `release/${item}`);
} else {
const s = await stat(found);
if (s.size <= 0) fail(`empty file: ${item}`);
}
}
if (opts.tarball) {
const date = new Date();
const ymd = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}${String(date.getDate()).padStart(2, "0")}`;
const tarName = `naku-release-${ymd}.tar.gz`;
const tarPath = join(root, tarName);
log(`tar ${tarName}…`);
// 在 outDir 的父目录打包,使 tar 内为 release/ 根
const parent = resolve(releaseDir, "..");
const folderName = releaseDir.split(/[\\/]/).pop() ?? "release";
await run(
["tar", "-czf", tarPath, "-C", parent, folderName],
root,
log,
);
log(`tarball → ${tarPath}`);
}
return releaseDir;
}
+27
View File
@@ -0,0 +1,27 @@
/**
* 兼容入口:等价于 `naku pack --yes --target=bun-linux-x64`。
* 交互请用 `bun run naku pack`。
*/
import { packRelease } from "./pack-core.ts";
function readTarget(): string {
const arg = process.argv.find((a) => a.startsWith("--target="));
return arg?.slice("--target=".length) || process.env.PACK_TARGET || "bun-linux-x64";
}
const target = readTarget();
console.log(`[pack-release] non-interactive pack (target=${target})`);
try {
const out = await packRelease({
target,
only: ["dist", "naku-server", "naku-engine", "naku-migrate", "drizzle"],
outDir: "release",
tarball: false,
onLog: (m) => console.log("[pack-release]", m),
});
console.log("[pack-release] done →", out);
} catch (err) {
console.error("[pack-release]", err instanceof Error ? err.message : err);
process.exit(1);
}
+2 -1
View File
@@ -26,6 +26,7 @@
"apps/server/test/**/*.ts",
"packages/*/src/**/*.ts",
"packages/*/test/**/*.ts",
"packages/*/drizzle.config.ts"
"packages/*/drizzle.config.ts",
"scripts/**/*.ts"
]
}