64 Commits

Author SHA1 Message Date
e41c4e4515 docs: 更新 AGENTS.md 文档结构和内容
- 新增根目录 AGENTS.md 作为 monorepo 总览
- 移动 desktop AGENTS.md 从 src-tauri/ 到 apps/desktop/
- 修正 server AGENTS.md 目录结构 (src/server/api/ 而非 src/orpc/)
- 明确 desktop 为纯 Tauri 壳子,无前端代码,通过 sidecar 加载 server
2026-02-07 03:29:51 +08:00
49d1f706e7 feat: 添加本地工具包依赖支持项目功能
- 添加本地工具包依赖 @furtherverse/utils 以支持项目功能。
- 添加对 workspace 内 utils 包的依赖引用
2026-01-23 16:18:20 +08:00
d13c3602c9 feat: 添加按路径导入支持及指纹工具导出
- 添加模块导出配置以支持按路径导入源码文件
- 导出指纹工具模块的全部功能。
2026-01-23 16:17:08 +08:00
3755b0f873 feat: 添加硬件指纹生成功能
- 生成硬件指纹,通过收集系统唯一标识、主板、BIOS、系统信息、磁盘布局和网络接口数据并哈希处理
2026-01-23 16:14:58 +08:00
be4e8212ec feat: 添加 ohash 和 systeminformation 依赖支持指纹与系统信息
- 添加 ohash 依赖并更新其版本至 ^2.0.11
- 添加 ohash 依赖以支持哈希功能
- 添加 ohask 和 systeminformation 依赖项并指定其来源为 catalog。
- 引入 ohash 和 systeminformation 库以支持指纹生成和系统信息获取。
2026-01-23 15:59:07 +08:00
0cab61af91 feat: 添加 systeminformation 依赖以支持系统信息获取
- 添加 systeminformation 依赖并更新其版本至 5.30.5,同时配置其运行时环境和二进制文件路径。
- 添加 systeminformation 依赖以获取系统信息
- 添加 systeminformation 依赖项以支持系统信息获取功能。
2026-01-23 15:54:30 +08:00
104b04064d refactor: 重命名文件以更准确反映其功能
- 重命名文件以更准确地反映其功能,将索引文件重命名为指纹处理文件。
2026-01-23 15:52:50 +08:00
b6c413aad9 feat: 添加类型检查与模块别名配置
- 添加开发依赖 @furtherverse/tsconfig 和 typescript 到 utils 包中
- 配置模块导入别名并添加类型检查依赖项
- 添加工具函数库的初始化文件
2026-01-23 15:50:59 +08:00
5a0a899e93 feat: 升级 lightningcss 至 1.30.2 并更新相关依赖
- 升级 lightningcss 及其所有平台相关依赖至 1.30.2 版本,并更新相关依赖项的版本号和哈希值,同时移除已废弃的 vitefu/vite 依赖项。
- 将版本号从 0.0.0 更新为 1.0.0
- 添加新的工具包模块并配置其名称、版本和模块类型。
2026-01-23 15:43:44 +08:00
254bef6162 chore: 升级依赖包至最新版本以兼容新功能和修复问题
- 升级依赖包版本以兼容最新功能和修复已知问题,包括 @effect/platform、@tanstack/react-router 系列、effect、nitro、vite、zod 及相关构建工具的版本更新。
- 升级依赖包版本以获取最新功能和修复,包括 @effect/platform、@tanstack/react-router 系列、effect、nitro、vite 和 zod。
2026-01-23 15:33:57 +08:00
610b81c32d feat: 更新页面标题为“Furtherverse”
- 将页面标题从“Fullstack Starter”更改为“Furtherverse”
2026-01-22 17:00:00 +08:00
afb0880d8e refactor: 统一数据库相关命名规范并优化单例实现
- 统一使用DB类型别名替换Database类型定义
- 将 db 中间件中获取数据库实例的函数名从 getDb 改为 getDB 以保持命名一致性。
- 重命名数据库相关函数和类型以使用一致的命名规范,并确保单例模式正确返回数据库实例。
2026-01-22 16:52:16 +08:00
3b50528435 chore: 更新数据库模式文件路径
- 更新数据库模式文件路径为新的位置。
2026-01-22 16:46:54 +08:00
c4b179464b refactor: 更新导入路径为相对路径别名
- 更新导入路径以使用相对路径别名指向 package.json 文件
2026-01-22 16:44:59 +08:00
6ea358bab5 feat: 移除API文档认证方案配置
- 移除API文档配置中的认证方案设置
2026-01-22 16:43:38 +08:00
dede23ead9 feat: 添加 OpenAPI 支持与集成
- 添加 @orpc/openapi 依赖以支持 OpenAPI 生成和集成。
- 配置 OpenAPI 文档生成与请求拦截,集成 Zod 验证错误处理并支持 Bearer 认证。
- 添加 @orpc/openapi 依赖并更新版本号至 1.13.4
- 添加 @orpc/openapi 依赖以支持 OpenAPI 生成功能。
2026-01-22 16:43:11 +08:00
2b3e91167e refactor: 更好的orpc结构 2026-01-22 16:37:10 +08:00
660ee0a545 refactor: 优化合约类型导出与导入方式
- 导出合约类型以支持类型安全的接口定义
- 更新类型导入以使用 Contract 而非直接导出的 contract 变量
2026-01-22 16:21:08 +08:00
7c8452c731 refactor: 移除废弃的 Context 类型并优化类型定义
- 移除 Context 类型导入并添加空对象类型定义以兼容当前上下文需求
- 移除已废弃的 Context 类型定义并清理注释代码
2026-01-22 16:19:33 +08:00
7beb911efb refactor: 优化数据库实例获取逻辑,移除无用变量
- 移除未使用的 IS_SERVERLESS 变量并直接调用 getDb 不传参数
- 修改数据库实例获取逻辑,将 serverless 参数改为 singleton 标志,控制是否返回单例实例。
2026-01-22 16:13:34 +08:00
a8db6212a1 refactor: 重构数据库实例创建逻辑并优化类型声明
- 重构数据库实例创建逻辑,将 `createDb` 函数改为箭头函数并优化 `getDb` 返回类型声明。
2026-01-22 16:00:39 +08:00
af807eeb53 refactor: 优化数据库连接管理与统一接入方式
- 优化数据库连接管理,直接使用获取数据库实例的函数并传入是否为无服务器环境的标识。
- 统一使用db中间件替代dbProvider,简化数据库连接处理并保持代码一致性。
- 添加数据库实例的单例获取机制并定义类型接口
2026-01-22 15:56:00 +08:00
70252fbd94 chore: 更新依赖至最新稳定版本以优化构建性能
- 更新项目依赖版本以升级 Rollup、LightningCSS、Vite 及其相关构建工具至最新稳定版本,并同步更新多个平台的二进制包以确保兼容性和性能优化。
2026-01-22 15:29:38 +08:00
c364b6d27f feat: 升级 TanStack、Turbo 等依赖至最新版本
- 升级多个 TanStack 相关依赖至 1.154.7 版本,同步更新 h3-v2 和 nitro 的依赖版本以保持兼容性。
- 升级 turbo 和 tanstack 相关依赖至最新版本以获取最新功能和修复。
2026-01-22 15:25:44 +08:00
7632b9a4ef refactor: 移除构建与开发任务中复制操作的依赖
- 移除构建和开发任务中对复制操作的依赖,仅保留对服务器编译任务的依赖。
2026-01-22 00:26:26 +08:00
efd29a9d63 feat: 添加构建脚本中的复制步骤以正确处理二进制文件
- 在构建和开发脚本中添加复制步骤以确保二进制文件正确处理
2026-01-22 00:26:11 +08:00
48bc50e221 feat: 添加构建与开发任务配置
- 添加构建任务的依赖项并指定输出路径,同时在开发任务中启用服务器开发模式并设置输出路径。
2026-01-22 00:24:33 +08:00
da2a7391da refactor: 简化构建脚本并确保产物正确复制
- 移除构建和开发脚本中的冗余bun run命令,直接调用tauri命令
- 在开发配置中添加对复制任务的依赖并指定输出路径,确保构建产物正确复制。
2026-01-22 00:21:27 +08:00
9aec9d2829 refactor: 将 sidecar 名称从 "app" 更改为 "server"
- 将 sidecar 的名称从 "app" 更改为 "server" 以正确启动服务进程。
2026-01-22 00:09:49 +08:00
16181e2e9d fix: 恢复 server-desktop 包依赖配置
- 恢复 server-desktop 包的依赖配置并确保其正确声明
2026-01-22 00:06:36 +08:00
7d3df0ec49 refactor: 重命名桌面应用为 server-desktop
- 更新项目结构中的目录名称为 server-desktop
- 将应用名称从 `app-desktop` 更改为 `server-desktop`
- 将桌面应用的包名和库名从 app-desktop 和 app_desktop_lib 更改为 server-desktop 和 server_desktop_lib。
- 将主函数中的运行入口从 app_desktop_lib 更改为 server_desktop_lib。
- 更新产品名称和标识符以反映新的应用名称为 server-desktop。
2026-01-21 23:59:39 +08:00
fc846fa24d chore: 调整构建任务依赖,添加开发服务器模式支持
- 调整构建任务依赖,为开发环境添加服务器开发模式依赖。
2026-01-21 23:56:15 +08:00
4710166942 chore: 更新构建脚本,清理旧二进制文件并复制资源
- 更新构建和开发脚本,确保在构建和开发前先清理旧的二进制文件并执行复制操作。
2026-01-21 23:50:46 +08:00
96705e965d feat: 添加目录存在性检查并优化复制逻辑
- 添加目录存在性检查功能并更新复制逻辑以使用该功能
2026-01-21 23:44:31 +08:00
fc916c7c1d refactor: 重命名复制文件并更新构建脚本
- 重命名文件以更准确地反映其功能
- 更新构建脚本,将复制二进制文件的命令改为使用新的复制脚本文件。
2026-01-21 23:40:07 +08:00
ece366c4d7 refactor: 移除未使用的类型定义,清理冗余代码
- 移除未使用的类型定义,清理冗余代码。
2026-01-21 23:38:31 +08:00
46984c2687 refactor: 重构桌面端构建配置与依赖管理
- 删除桌面端构建配置文件
- 添加二进制文件复制工具,支持多平台目标映射并自动处理文件路径与扩展名,通过类型安全配置和错误处理确保构建流程稳定可靠。
- 添加依赖项以支持类型安全的模式定义和项目配置,并新增脚本用于复制二进制文件。
- 添加桌面应用的TypeScript配置文件并继承统一的tsconfig设置,排除node_modules和src-tauri目录。
- 添加必要的开发依赖项以支持类型检查和构建工具链
2026-01-21 23:38:16 +08:00
712ed1919f refactor: 重构桌面端构建配置与二进制路径
- 添加桌面端构建配置文件
- 将外部二进制文件路径从 `binaries/app` 更改为 `binaries/server`。
2026-01-21 23:29:31 +08:00
553e055a96 feat: 添加 TypeScript 支持和类型定义
- 添加类型定义和 TypeScript 依赖以支持类型检查和开发环境。
2026-01-21 23:21:02 +08:00
985662cb22 refactor: 重命名 outfilePrefix 为 outfile 并统一更新代码
- 将构建配置中的 outfilePrefix 更名为 outfile,并统一更新相关代码以使用新的字段名。
2026-01-21 22:09:11 +08:00
924bcd6aa2 refactor: 重构构建流程,提升输出灵活性与配置性
- 移除目标映射表并根据配置前缀生成输出文件名,使构建输出更灵活可配置
- 重构构建任务配置,将原有分步构建逻辑合并为统一的编译任务,明确依赖关系和输出路径
2026-01-21 22:02:01 +08:00
896bb0ca7d refactor: 重命名 AGENTS.md 文件至 server 目录
- 重命名 AGENTS.md 文件至 apps/server/ 目录下
2026-01-21 21:46:20 +08:00
3d3765cdbf feat: 添加桌面应用忽略文件及构建依赖配置
- 添加桌面应用的忽略文件配置,包括日志、依赖目录、构建输出及编辑器临时文件。
- 添加桌面端开发任务依赖于服务器编译任务的配置。
2026-01-21 21:46:00 +08:00
13a4a333ff refactor: 清理冗余配置与依赖项
- 移除重复的版本号字段并保持配置文件一致性
- 移除 `@types/bun` 开发依赖项
- 移除 tsconfig 包中多余的 devDependencies 项和无效的 catalog: 依赖配置
2026-01-21 20:14:53 +08:00
e6293ce52f refactor: 移除本地数据库依赖并清理废弃模块
- 移除对本地数据库包的依赖并更新客户端、合约和服务器包的引用为目录源。
- 移除对本地数据库包的依赖引用并清理相关配置
- 删除数据库包的配置文件及依赖项
- 移除对 todo 模式的导出,清理已废弃的模块引用。
- 删除待办事项表的定义及相关字段配置
- 删除已废弃的字段生成工具函数及对应配置,移除对 uuidv7 和 PostgreSQL 特定生成策略的依赖。
- 移除字段工具导出,不再从字段工具模块导出内容
- 删除 SQLite 数据库模块的初始化文件
- 删除数据库包中的 TypeScript 配置文件以统一项目配置
2026-01-21 16:34:33 +08:00
219d6cfb4f refactor: 统一数据库模块入口命名并更新导入路径
- 更新数据库工具导入路径,将创建数据库实例的模块从 '@/db/utils' 改为 '@/db'。
- 将 utils.ts 重命名为 index.ts 以统一模块入口文件命名规范
2026-01-21 16:33:38 +08:00
da5f08f8c1 feat: 添加 UUID 支持及待办事项数据表定义
- 添加 uuid 依赖以支持唯一标识符生成
- 移除对 todoTable 的导出,不再从数据库模式文件中暴露该表定义。
- 导出 todo 模式定义文件中的所有内容
- 添加待办事项数据表定义,包含标题和完成状态字段,并集成自动生成字段。
- 添加用于定义主键、创建时间和更新时间字段的实用工具函数,并支持不同 PostgreSQL 版本的 UUID 生成策略,同时提供生成字段的键名映射。
- 添加 uuid 依赖到项目中
2026-01-21 16:33:03 +08:00
f1608c3546 refactor: 重构ORPC客户端并统一导出路径
- 删除API入口文件中的导出内容
- 更新上下文类型为导入的Context类型并移除注释掉的旧类型定义。
- 添加空的 Context 类型定义以支持上下文类型,暂时忽略复杂度检查警告
- 创建支持服务端和客户端的统一ORPC客户端,基于请求头上下文和Fetch链接实现前后端一致的RPC调用。
- 重构客户端代码,将ORPC客户端初始化逻辑移至独立文件并统一导出,提升代码可维护性和模块化程度。
- 更新导入路径,将 orpc 从 '@/api' 改为 '@/lib/orpc/query-client'。
2026-01-21 16:26:59 +08:00
e49e8606da refactor: 移除旧版 devtools 集成,改用组件化方式
- 移除 TanStack Query 开发工具的集成配置
- 移除对 tanstack-query devtools 的导出
- 移除 TanStack Router 开发工具插件的集成配置
- 移除 tanstack-router 开发工具集成的导出
- 移除旧的 devtools 集成方式,改用新的组件化方式引入 TanStack Router 和 Query 的开发工具面板。
2026-01-21 16:11:46 +08:00
d922c2c242 refactor: 统一路由模块命名规范并更新导入路径
- 更新导入路径,将 router 从 './router' 改为 './routers'。
- 删除API路由配置,移除对todo处理函数的引用及服务器路由实例的定义。
- 添加路由配置,集成待办事项模块到主路由中。
- 重命名文件以统一项目中路由模块的命名规范
- 将导入路径从 '@/api/router' 更新为 '@/api/routers' 以正确引用路由配置。
2026-01-21 16:08:00 +08:00
3dd1beb567 refactor: 重构合约API模块结构并修复路径错误
- 删除已废弃的合约API模块及其导出配置
- 添加合约接口导出,包含待办事项相关接口。
- 修复文件路径拼写错误,将文件名从 `contracts` 重命名为 `contract`。
2026-01-21 16:05:32 +08:00
da661d4495 fix: 移除 biome 配置中对 useHookAtTopLevel 的禁用设置
- 移除 biome 配置中对 useHookAtTopLevel 规则的禁用设置
2026-01-21 16:01:27 +08:00
f5fd28621e rename 2026-01-21 16:00:28 +08:00
76796613b4 feat: 添加动态参数路径API路由支持
- 添加API路由文件以支持动态参数路径的请求处理
- 添加对 `/api/$` 路由的支持,包括路由配置、类型定义和路由树的完整集成。
2026-01-21 15:57:53 +08:00
168c160bd4 refactor: 升级核心依赖并清理无用依赖
- 移除未使用的开发依赖项 @biomejs/biome 和 turbo
- 升级 TanStack 生态相关依赖至最新版本,包括 react-query、react-router、react-start 等核心包,并同步更新其依赖项和构建工具 esbuild 的多平台支持版本。
- 升级 React Query、React Router 及其相关依赖至最新版本,并移除未使用的 TypeScript 依赖。
2026-01-21 15:54:34 +08:00
e1ab34b7ec fix: 修复环境变量文件路径问题
- 将 .env.example 文件重命名为 apps/server/.env.example
2026-01-21 15:50:39 +08:00
484ecd85a3 refactor: 重构数据库相关代码并更新依赖
- 添加本地数据库包依赖以支持项目数据库功能
- 导出 todoTable 以供数据库 schema 使用
- 移除对 todo 模式的导出
- 删除待办事项数据表的定义配置
- 重命名文件以更准确地反映其用途,将数据库相关工具函数集中到新的工具文件中。
- 将数据库创建函数的导入路径从 '@/db' 更新为 '@/db/utils'。
- 将数据库包版本更新为1.0.0并添加对工作区中数据库包的依赖。
2026-01-21 15:47:24 +08:00
28d0c9ad3d feat: 添加数据库包及 UUID 支持,初始化类型检查与字段工具
- 将 typecheck 脚本改为使用 tsc --noEmit,并添加 @furtherverse/tsconfig 工作区依赖。
- 添加新的本地包 @furtherverse/database 并更新 uuid 依赖至 13.0.0 版本,同时修复相关依赖引用。
- 添加 uuid 依赖以支持生成唯一标识符功能
- 初始化数据库包的配置,设置模块导入导出路径并配置类型检查与格式化脚本。
- 导出 todo 模式定义文件中的所有内容
- 添加待办事项表结构,包含自动生成字段、标题和完成状态字段。
- 添加用于生成主键、创建和更新时间戳字段的实用工具函数,并提供自动生成字段及其键的映射。
- 导出字段工具模块中的所有内容
- 添加 SQLite 数据库连接初始化功能
- 添加数据库包的 TypeScript 配置并继承基础配置文件
2026-01-21 15:42:49 +08:00
653a144736 refactor: 重命名项目应用模块为 desktop 和 server
- 重命名项目应用模块,将 demo-desktop 和 demo-server 重命名为 desktop 和 server,并同步更新相关依赖引用。
2026-01-21 15:05:17 +08:00
011c9211f5 rename 2026-01-21 15:00:11 +08:00
fd20ca2c52 feat: 添加桌面应用和服务器应用项目并更新依赖
- 添加新的桌面应用和服务器应用项目,并更新依赖和版本信息。
2026-01-21 14:53:58 +08:00
38db2ae6f2 mv tauri 2026-01-21 14:53:22 +08:00
babd0f5615 chore 2026-01-21 14:41:56 +08:00
a6125718f5 refactor: 重构演示应用目录结构与配置文件路径
- 重命名构建配置文件至指定应用目录
- 重命名 drizzle.config.ts 文件至 apps/demo-app/ 目录下
- 添加演示应用的完整包配置,包含构建、开发、数据库管理及依赖项,支持 Tauri 桌面应用开发。
- 重命名 robots.txt 文件至 demo-app 应用目录下
- 重命名 src-tauri/.gitignore 文件为 apps/demo-app/src-tauri/.gitignore
- 重命名 AGENTS.md 文件至指定目录路径
- 重命名构建脚本文件以匹配新项目路径结构
- 重命名默认能力配置文件以匹配新项目路径
- 重命名 Cargo.lock 文件以匹配新的项目路径结构
- 重命名 Cargo.toml 文件以正确反映其在项目中的路径位置
- 重命名图标文件以正确匹配新项目路径
- 重命名128x128.png图标文件至demo-app应用目录下
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以匹配新项目路径
- 重命名图标文件以正确反映其在项目中的路径位置
- 重命名图标文件以匹配新项目路径结构
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以匹配新项目路径结构
- 重命名图标文件以正确放置在演示应用的资源目录中
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名命令模块文件路径以匹配项目结构
- 重命名文件以正确反映其在项目中的位置
- 重命名主程序文件路径以匹配项目结构
- 重命名 sidecar.rs 文件至 demo-app 项目路径下
- 重命名 tauri.conf.json 文件至指定目录路径
- 重命名错误组件文件以正确反映其在项目中的位置
- 重命名文件 NotFount.tsx 到指定目录路径
- 重命名数据库入口文件路径以适应项目结构调整
- 重命名数据 schema 文件路径以匹配项目结构
- 重命名文件以正确反映其在项目中的位置路径
- 重命名环境配置文件以正确反映其在项目中的位置。
- 重命名文件以正确反映其在项目中的路径位置
- 重命名文件以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的路径位置
- 重命名文件以正确反映其在项目中的路径位置
- 重命名工具函数文件以正确反映其在项目中的位置
- 重命名客户端文件以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的位置
- 重命名文件路径以正确组织项目结构中的契约文件
- 重命名文件路径以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的位置
- 重命名数据库中间件文件以正确反映其在项目中的位置
- 重命名中间件文件路径以匹配项目结构
- 重命名路由文件以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的位置。
- 重命名类型文件以正确反映其在项目中的路径位置
- 重命名路由配置文件以匹配项目目录结构
- 重命名根路由文件以正确反映其在项目中的位置
- 重命名RPC路由文件至demo-app应用目录下
- 重命名路由文件路径以匹配项目结构调整
- 重命名路由树生成文件至 demo-app 应用目录下
- 重命名样式文件以正确反映其在项目中的位置
- 添加 TypeScript 配置以扩展 React 项目模板并设置路径别名。
- 重命名 vite.config.ts 文件至 apps/demo-app/ 目录下
- 移除 biome.json 中对 routeTree.gen.ts 文件的排除规则
- 更新依赖版本以统一使用 catalog 依赖管理,提升项目依赖一致性并升级关键包至最新稳定版本。
- 配置安装时的公共提升模式,包含类型包和特定命名空间的包。
- 删除空的 drizzle 目录占位文件
- 将 node 版本更新为最新版本,同时将 bun 和 rust 版本设置为最新。
- 将项目名称更新为 monorepo 并重构脚本与依赖配置以支持工作区结构和统一的 turbo 管理。
- 添加基础 TypeScript 配置文件,启用严格模式并配置模块解析与编译选项以支持现代 JavaScript 特性。
- 添加 Bun 专用的 TypeScript 配置,继承基础配置并引入 Bun 类型定义。
- 添加 tsconfig 包的配置文件并定义基础、Bun 和 React 的配置导出,同时引入 Bun 类型定义作为开发依赖。
- 添加React项目专用的TypeScript配置,指定JSX处理方式并扩展基础配置。
- 删除旧的 TypeScript 配置文件以移除过时的编译选项和路径别名设置。
2026-01-21 14:31:12 +08:00
100 changed files with 2499 additions and 1189 deletions

View File

@@ -37,7 +37,8 @@
"files.associations": { "files.associations": {
".env": "dotenv", ".env": "dotenv",
".env.*": "dotenv", ".env.*": "dotenv",
"**/tsconfig*.json": "jsonc", "**/tsconfig.json": "jsonc",
"**/tsconfig.*.json": "jsonc",
"**/biome.json": "jsonc", "**/biome.json": "jsonc",
"**/opencode.json": "jsonc" "**/opencode.json": "jsonc"
}, },

275
AGENTS.md
View File

@@ -1,193 +1,200 @@
# AGENTS.md - AI Coding Agent Guidelines # AGENTS.md - AI Coding Agent Guidelines
Guidelines for AI agents working in this Bun monorepo.
## Project Overview ## Project Overview
TanStack Start fullstack app with Tauri desktop shell. - **Monorepo**: Bun workspaces + Turborepo orchestration
- **Runtime**: Bun (see `mise.toml` for version)
- **Apps**:
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
- `apps/desktop` - Tauri v2 desktop shell, loads server via sidecar (see `apps/desktop/AGENTS.md`)
- **Packages**: `packages/utils`, `packages/tsconfig` (shared configs)
| Layer | Tech | ## Build / Lint / Test Commands
|-------|------|
| Framework | TanStack Start (React SSR, file-based routing) |
| Runtime | Bun |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS v4 |
| Database | PostgreSQL + Drizzle ORM |
| RPC | ORPC (contract-first, type-safe) |
| Build | Vite + Turbo |
| Linting | Biome |
| Desktop | Tauri v2 (optional, see `src-tauri/AGENTS.md`) |
## Commands
### Root Commands (via Turbo)
```bash ```bash
# Development bun dev # Start all apps in dev mode
bun dev # Start Tauri + Vite via Turbo bun build # Build all apps
bun dev:vite # Vite only (localhost:3000) bun fix # Lint + format (Biome auto-fix)
bun db:studio # Drizzle Studio bun typecheck # TypeScript check across monorepo
# Build
bun build # Full build (Vite → compile → Tauri)
bun build:vite # Vite only (outputs to .output/)
# Code Quality
bun typecheck # TypeScript check (tsc -b)
bun fix # Biome auto-fix (format + lint)
# Database
bun db:generate # Generate migrations from schema
bun db:migrate # Run migrations
bun db:push # Push schema changes (dev only)
# Testing (not configured yet)
# When adding tests, use Vitest or Bun test runner:
# bun test path/to/test.ts # Single file
# bun test -t "pattern" # By test name
``` ```
## Code Style ### Server App (`apps/server`)
```bash
bun dev # Vite dev server (localhost:3000)
bun build # Production build → .output/
bun fix # Biome auto-fix
bun typecheck # TypeScript check
### Formatting (Biome enforced) # Database (Drizzle)
bun db:generate # Generate migrations from schema
bun db:migrate # Run migrations
bun db:push # Push schema (dev only)
bun db:studio # Open Drizzle Studio
```
### Desktop App (`apps/desktop`)
```bash
bun dev # Copy sidecar + start Tauri dev
bun build # Copy sidecar + build installer
# Rust (from apps/desktop/src-tauri/)
cargo check # Compile check
cargo clippy # Linter
cargo fmt # Formatter
```
### Testing
No test framework configured yet. When adding tests:
```bash
bun test path/to/test.ts # Run single test file
bun test -t "pattern" # Run tests matching pattern
cargo test test_name -- --nocapture # Rust single test with output
```
## Code Style (TypeScript)
### Formatting (Biome)
- **Indent**: 2 spaces - **Indent**: 2 spaces
- **Line endings**: LF - **Line endings**: LF
- **Quotes**: Single `'string'` - **Quotes**: Single `'`
- **Semicolons**: As needed (ASI) - **Semicolons**: Omit (ASI)
- **Arrow parens**: Always `(x) => x` - **Arrow parentheses**: Always `(x) => x`
### Imports ### Imports
Biome auto-organizes. Order:
Biome auto-organizes. Order: external → internal (`@/*`) → type-only imports. 1. External packages
2. Internal `@/*` aliases
3. Type imports (`import type { ... }`)
```typescript ```typescript
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod' import { z } from 'zod'
import { db } from '@/db' import { db } from '@/server/db'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
``` ```
### TypeScript ### TypeScript Strictness
- `strict: true`
Strict mode with extra checks: - `noUncheckedIndexedAccess: true` - array/object access returns `T | undefined`
- `noUncheckedIndexedAccess: true` - array/object index returns `T | undefined`
- `noImplicitOverride: true` - `noImplicitOverride: true`
- `verbatimModuleSyntax: true` - `verbatimModuleSyntax: true`
- Use `@/*` path aliases (maps to `src/*`)
Path alias: `@/*``src/*` ### Naming Conventions
| Type | Convention | Example |
### Naming |------|------------|---------|
| Files (utils) | kebab-case | `auth-utils.ts` |
| Entity | Convention | Example | | Files (components) | PascalCase | `UserProfile.tsx` |
|--------|------------|---------| | Components | PascalCase arrow | `const Button = () => {}` |
| Files (utils) | kebab-case | `utils.ts`, `db-provider.ts` | | Functions | camelCase | `getUserById` |
| Files (components) | PascalCase | `NotFound.tsx` | | Constants | UPPER_SNAKE | `MAX_RETRIES` |
| Routes | TanStack conventions | `routes/index.tsx`, `routes/__root.tsx` | | Types/Interfaces | PascalCase | `UserProfile` |
| Components | PascalCase arrow functions | `const MyComponent = () => {}` |
| Functions | camelCase | `handleSubmit` |
| Constants | UPPER_SNAKE_CASE | `MAX_RETRIES` |
| Types/Interfaces | PascalCase | `TodoItem`, `RouterContext` |
### React Patterns ### React Patterns
```typescript ```typescript
// Components: arrow functions (Biome enforces) // Components: arrow functions (enforced by Biome)
const MyComponent = ({ title }: { title: string }) => { const MyComponent = ({ title }: { title: string }) => {
return <div>{title}</div> return <div>{title}</div>
} }
// Routes: createFileRoute // Routes: TanStack Router file conventions
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
component: Home, component: Home,
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions())
},
}) })
// Data fetching: TanStack Query // Data fetching: TanStack Query
const query = useSuspenseQuery(orpc.todo.list.queryOptions()) const { data } = useSuspenseQuery(orpc.todo.list.queryOptions())
const mutation = useMutation(orpc.todo.create.mutationOptions())
``` ```
### ORPC Pattern (Contract-First RPC) ### Error Handling
- Use `try-catch` for async operations
- Throw descriptive errors
- ORPC: Use `ORPCError` with proper codes (`NOT_FOUND`, `INPUT_VALIDATION_FAILED`)
- Never use empty catch blocks
1. **Define contract** (`src/orpc/contracts/my-feature.ts`): ## Code Style (Rust - Tauri)
```typescript
import { oc } from '@orpc/contract'
import { z } from 'zod'
export const get = oc.input(z.object({ id: z.uuid() })).output(schema) - **Indent**: 4 spaces
export const create = oc.input(insertSchema).output(schema) - **Naming**: snake_case (functions), PascalCase (types), SCREAMING_SNAKE (consts)
``` - Use `expect("中文消息")` over `unwrap()`
- Async: `tokio` runtime, `tauri::async_runtime::spawn`
- Run `cargo fmt` and `cargo clippy` before commit
2. **Implement handler** (`src/orpc/handlers/my-feature.ts`): ## Database (Drizzle ORM)
```typescript
import { os } from '@/orpc/server'
import { dbProvider } from '@/orpc/middlewares'
export const get = os.myFeature.get
.use(dbProvider)
.handler(async ({ context, input }) => {
return await context.db.query.myTable.findFirst(...)
})
```
3. **Register** in `contract.ts` and `router.ts`
4. **Use** in components via `orpc.myFeature.get.queryOptions()`
### Drizzle Schema
```typescript ```typescript
import { sql } from 'drizzle-orm'
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const myTable = pgTable('my_table', { export const myTable = pgTable('my_table', {
id: uuid('id').primaryKey().default(sql`uuidv7()`), id: uuid().primaryKey().default(sql`uuidv7()`),
name: text('name').notNull(), name: text().notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()), updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
}) })
``` ```
### Environment Variables ## Environment Variables
- Server: no prefix (e.g., `DATABASE_URL`) - Use `@t3-oss/env-core` with Zod validation in `src/env.ts`
- Client: `VITE_` prefix required - Server vars: no prefix
- Validated via `@t3-oss/env-core` in `src/env.ts` - Client vars: `VITE_` prefix required
- Never commit `.env` files
## Directory Structure
```
src/
├── components/ # Reusable React components
├── db/
│ ├── schema/ # Drizzle schema definitions
│ └── index.ts # Database instance
├── integrations/ # TanStack Query/Router setup
├── lib/ # Utility functions
├── orpc/
│ ├── contracts/ # Input/output schemas
│ ├── handlers/ # Server procedure implementations
│ ├── middlewares/ # Middleware (e.g., dbProvider)
│ ├── contract.ts # Contract aggregation
│ ├── router.ts # Router composition
│ └── client.ts # Isomorphic client
├── routes/ # File-based routes
│ ├── __root.tsx # Root layout
│ └── api/rpc.$.ts # ORPC HTTP endpoint
└── env.ts # Environment validation
```
## Critical Rules ## Critical Rules
- **DO NOT** edit `src/routeTree.gen.ts` (auto-generated) **DO:**
- **DO NOT** commit `.env` files - Run `bun fix` before committing
- **MUST** run `bun fix` before commits - Use `@/*` path aliases (not relative imports)
- **MUST** use `@/*` path alias (not relative imports) - Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
- **MUST** use React Compiler (no manual memoization needed) - Include `createdAt`/`updatedAt` on all tables
- **MUST** use `Readonly<T>` for immutable props
**DON'T:**
- Edit `src/routeTree.gen.ts` (auto-generated)
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
- Commit `.env` files
- Use empty catch blocks `catch(e) {}`
- Use `unwrap()` in Rust without `expect()`
## Git Workflow ## Git Workflow
1. Make changes following style guide 1. Make changes following style guide
2. Run `bun fix` (format + lint) 2. `bun fix` - auto-format and lint
3. Run `bun typecheck` (type safety) 3. `bun typecheck` - verify types
4. Test with `bun dev` 4. `bun dev` - test locally
5. Commit with descriptive message 5. Commit with descriptive message
## Directory Structure
```
.
├── apps/
│ ├── server/ # TanStack Start fullstack app
│ │ ├── src/
│ │ │ ├── client/ # ORPC client, Query client
│ │ │ ├── components/
│ │ │ ├── routes/ # File-based routing
│ │ │ └── server/ # API layer + database
│ │ │ ├── api/ # ORPC contracts, routers, middlewares
│ │ │ └── db/ # Drizzle schema
│ │ └── AGENTS.md
│ └── desktop/ # Tauri v2 shell (no frontend src)
│ ├── src-tauri/ # Rust Tauri code
│ │ ├── src/ # Rust source
│ │ └── binaries/ # Sidecar binaries
│ └── AGENTS.md
├── packages/
│ ├── tsconfig/ # Shared TS configs
│ └── utils/ # Shared utilities
├── biome.json # Linting/formatting config
├── turbo.json # Turbo task orchestration
└── package.json # Workspace root
```
## See Also
- `apps/server/AGENTS.md` - Detailed TanStack Start / ORPC patterns
- `apps/desktop/AGENTS.md` - Rust / Tauri development guide

24
apps/desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

171
apps/desktop/AGENTS.md Normal file
View File

@@ -0,0 +1,171 @@
# AGENTS.md - Desktop App Guidelines
Tauri v2 desktop shell - a lightweight wrapper that loads the server app via sidecar.
## Architecture
- **Type**: Tauri v2 desktop application (shell only)
- **Design**: Tauri provides native desktop APIs; all web logic handled by sidecar
- **Sidecar**: The compiled server binary runs as a child process
- **Dev mode**: Connects to `localhost:3000` (requires server dev running)
- **Prod mode**: Automatically starts sidecar binary
**This app has NO frontend src** - it loads the server app entirely.
## Commands
```bash
# Development (from apps/desktop/)
bun dev # Copy sidecar + start Tauri dev
# Build
bun build # Copy sidecar + build Tauri installer
# Rust Commands (from src-tauri/)
cargo check # Compile check
cargo clippy # Linter
cargo fmt # Formatter
cargo test # Run tests
cargo test test_name -- --nocapture # Single test with output
```
## Directory Structure
```
apps/desktop/
├── src-tauri/ # Rust Tauri code
│ ├── src/
│ │ ├── main.rs # Entry point (calls lib::run)
│ │ ├── lib.rs # Core app logic (plugins, commands, state)
│ │ ├── commands/
│ │ │ └── mod.rs # Native desktop commands
│ │ └── sidecar.rs # Sidecar process management
│ ├── binaries/ # Sidecar binaries (copied from server build)
│ ├── capabilities/ # Tauri v2 permission config
│ ├── icons/ # App icons
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri configuration
├── copy.ts # Script to copy server binary to binaries/
├── package.json
└── tsconfig.json
```
## Development Workflow
1. **Start server dev first**: `cd ../server && bun dev`
2. **Start Tauri**: `bun dev` (from apps/desktop/)
3. Tauri connects to localhost:3000 with HMR support
## Rust Code Style
### Formatting
- **Indent**: 4 spaces
- **Line width**: 100 chars
- Run `cargo fmt` before commit
### Naming
| Type | Convention | Example |
|------|------------|---------|
| Functions/variables | snake_case | `find_available_port` |
| Types/structs/enums | PascalCase | `SidecarProcess` |
| Constants | SCREAMING_SNAKE | `DEFAULT_PORT` |
### Imports
```rust
// Order: std → external crates → internal modules (separated by blank lines)
use std::sync::Mutex;
use tauri::Manager;
use tauri_plugin_shell::ShellExt;
use crate::sidecar::SidecarProcess;
```
### Error Handling
```rust
// Use expect() with Chinese error messages
let sidecar = app_handle
.shell()
.sidecar("server")
.expect("无法找到 server sidecar");
// Log with emoji for clear feedback
println!("✓ Sidecar 启动成功!");
eprintln!("✗ Sidecar 启动失败");
```
### Async Code
```rust
// Use Tauri's async runtime for spawning
tauri::async_runtime::spawn(async move {
let port = find_available_port(3000).await;
// ...
});
```
## Tauri Patterns
### Command Definition
```rust
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Register in Builder
.invoke_handler(tauri::generate_handler![commands::greet])
```
### State Management
```rust
struct SidecarProcess(Mutex<Option<CommandChild>>);
// Register state
app.manage(SidecarProcess(Mutex::new(None)));
// Access state
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
*state.0.lock().unwrap() = Some(child);
}
```
### Sidecar Lifecycle
```rust
// Start sidecar with environment
let sidecar = app_handle
.shell()
.sidecar("server")
.expect("无法找到 server sidecar")
.env("PORT", port.to_string());
// Cleanup on exit
match event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
if let Some(child) = process.take() {
let _ = child.kill();
}
}
_ => {}
}
```
## Critical Rules
**DO:**
- Run `cargo fmt` and `cargo clippy` before commit
- Use `expect("中文消息")` instead of `unwrap()`
- Always cleanup sidecar on app exit
- Declare sidecar in `tauri.conf.json``bundle.externalBin`
**DON'T:**
- Edit `gen/schemas/` (auto-generated)
- Use `unwrap()` in production code without context
- Block the async runtime (use `spawn_blocking`)
## Pre-commit Checklist
- [ ] `cargo fmt` - formatting
- [ ] `cargo clippy` - linting
- [ ] `cargo check` - compiles
- [ ] `cargo test` - tests pass
- [ ] Tauri app starts and exits cleanly

470
apps/desktop/copy.ts Normal file
View File

@@ -0,0 +1,470 @@
import * as path from 'node:path'
import { Schema } from '@effect/schema'
import { $ } from 'bun'
import { Console, Context, Data, Effect, Layer } from 'effect'
// ============================================================================
// Domain Models & Schema
// ============================================================================
/**
* Bun 构建目标后缀
*/
const BunTargetSuffixSchema = Schema.Literal(
'windows-x64',
'darwin-arm64',
'darwin-x64',
'linux-x64',
'linux-arm64',
)
/**
* Tauri sidecar 目标三元组
*/
const TauriTargetSchema = Schema.Literal(
'x86_64-pc-windows-msvc',
'aarch64-apple-darwin',
'x86_64-apple-darwin',
'x86_64-unknown-linux-gnu',
'aarch64-unknown-linux-gnu',
)
/**
* 目标映射配置
*/
const TargetMappingSchema = Schema.Struct({
bunSuffix: BunTargetSuffixSchema,
tauriTarget: TauriTargetSchema,
})
type TargetMapping = Schema.Schema.Type<typeof TargetMappingSchema>
/**
* 复制配置
*/
const CopyConfigSchema = Schema.Struct({
sourceDir: Schema.String.pipe(Schema.nonEmptyString()),
targetDir: Schema.String.pipe(Schema.nonEmptyString()),
baseName: Schema.String.pipe(Schema.nonEmptyString()),
mappings: Schema.Array(TargetMappingSchema).pipe(Schema.minItems(1)),
})
type CopyConfig = Schema.Schema.Type<typeof CopyConfigSchema>
/**
* 复制结果
*/
const CopyResultSchema = Schema.Struct({
bunSuffix: BunTargetSuffixSchema,
tauriTarget: TauriTargetSchema,
sourceFile: Schema.String,
targetFile: Schema.String,
success: Schema.Boolean,
})
type CopyResult = Schema.Schema.Type<typeof CopyResultSchema>
// ============================================================================
// Error Models
// ============================================================================
class ConfigError extends Data.TaggedError('ConfigError')<{
readonly message: string
readonly cause: unknown
}> {}
class FileSystemError extends Data.TaggedError('FileSystemError')<{
readonly operation: string
readonly path: string
readonly cause: unknown
}> {}
class CopyError extends Data.TaggedError('CopyError')<{
readonly source: string
readonly target: string
readonly cause: unknown
}> {}
// ============================================================================
// Services
// ============================================================================
/**
* 配置服务
*/
class CopyConfigService extends Context.Tag('CopyConfigService')<
CopyConfigService,
CopyConfig
>() {
/**
* 从原始数据创建并验证配置
*/
static fromRaw = (raw: unknown) =>
Effect.gen(function* () {
const decoded = yield* Schema.decodeUnknown(CopyConfigSchema)(raw)
return decoded
}).pipe(
Effect.catchAll((error) =>
Effect.fail(
new ConfigError({
message: '配置验证失败',
cause: error,
}),
),
),
)
/**
* 默认配置 Layer
*/
static readonly Live = Layer.effect(
CopyConfigService,
CopyConfigService.fromRaw({
sourceDir: path.join(__dirname, '..', 'server', 'out'),
targetDir: path.join(__dirname, 'src-tauri', 'binaries'),
baseName: 'server',
mappings: [
{
bunSuffix: 'windows-x64',
tauriTarget: 'x86_64-pc-windows-msvc',
},
{
bunSuffix: 'darwin-arm64',
tauriTarget: 'aarch64-apple-darwin',
},
{
bunSuffix: 'darwin-x64',
tauriTarget: 'x86_64-apple-darwin',
},
{
bunSuffix: 'linux-x64',
tauriTarget: 'x86_64-unknown-linux-gnu',
},
{
bunSuffix: 'linux-arm64',
tauriTarget: 'aarch64-unknown-linux-gnu',
},
],
} satisfies CopyConfig),
)
}
/**
* 文件系统服务
*/
class FileSystemService extends Context.Tag('FileSystemService')<
FileSystemService,
{
readonly ensureDir: (dir: string) => Effect.Effect<void, FileSystemError>
readonly fileExists: (
filePath: string,
) => Effect.Effect<boolean, FileSystemError>
readonly dirExists: (
dirPath: string,
) => Effect.Effect<boolean, FileSystemError>
readonly copyFile: (
source: string,
target: string,
) => Effect.Effect<void, CopyError>
}
>() {
static readonly Live = Layer.succeed(FileSystemService, {
ensureDir: (dir: string) =>
Effect.tryPromise({
try: async () => {
await $`mkdir -p ${dir}`
},
catch: (cause: unknown) =>
new FileSystemError({
operation: 'ensureDir',
path: dir,
cause,
}),
}),
fileExists: (filePath: string) =>
Effect.tryPromise({
try: async () => {
const file = Bun.file(filePath)
return await file.exists()
},
catch: (cause: unknown) =>
new FileSystemError({
operation: 'fileExists',
path: filePath,
cause,
}),
}),
dirExists: (dirPath: string) =>
Effect.tryPromise({
try: async () => {
const { default: fs } = await import('node:fs/promises')
try {
const stat = await fs.stat(dirPath)
return stat.isDirectory()
} catch {
return false
}
},
catch: (cause: unknown) =>
new FileSystemError({
operation: 'dirExists',
path: dirPath,
cause,
}),
}),
copyFile: (source: string, target: string) =>
Effect.tryPromise({
try: async () => {
await $`cp ${source} ${target}`
},
catch: (cause: unknown) =>
new CopyError({
source,
target,
cause,
}),
}),
})
}
/**
* 复制服务
*/
class CopyService extends Context.Tag('CopyService')<
CopyService,
{
readonly copyBinary: (
config: CopyConfig,
mapping: TargetMapping,
) => Effect.Effect<CopyResult, CopyError | FileSystemError>
readonly copyAllBinaries: (
config: CopyConfig,
) => Effect.Effect<ReadonlyArray<CopyResult>, CopyError | FileSystemError>
}
>() {
static readonly Live = Layer.effect(
CopyService,
Effect.gen(function* () {
const fs = yield* FileSystemService
return {
copyBinary: (config: CopyConfig, mapping: TargetMapping) =>
Effect.gen(function* () {
const { sourceDir, targetDir, baseName } = config
const { bunSuffix, tauriTarget } = mapping
// 确定文件扩展名Windows 需要 .exe
const ext = tauriTarget.includes('windows') ? '.exe' : ''
// 构建源文件和目标文件路径
const sourceFile = path.join(
sourceDir,
`${baseName}-${bunSuffix}${ext}`,
)
const targetFile = path.join(
targetDir,
`${baseName}-${tauriTarget}${ext}`,
)
// 检查源文件是否存在
const exists = yield* fs.fileExists(sourceFile)
if (!exists) {
yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`)
return {
bunSuffix,
tauriTarget,
sourceFile,
targetFile,
success: false,
} satisfies CopyResult
}
// 复制文件
yield* fs.copyFile(sourceFile, targetFile)
yield* Console.log(`${bunSuffix}${tauriTarget}`)
yield* Console.log(` ${sourceFile}`)
yield* Console.log(`${targetFile}\n`)
return {
bunSuffix,
tauriTarget,
sourceFile,
targetFile,
success: true,
} satisfies CopyResult
}),
copyAllBinaries: (config: CopyConfig) =>
Effect.gen(function* () {
const effects = config.mappings.map((mapping) =>
Effect.gen(function* () {
const { sourceDir, targetDir, baseName } = config
const { bunSuffix, tauriTarget } = mapping
const ext = tauriTarget.includes('windows') ? '.exe' : ''
const sourceFile = path.join(
sourceDir,
`${baseName}-${bunSuffix}${ext}`,
)
const targetFile = path.join(
targetDir,
`${baseName}-${tauriTarget}${ext}`,
)
const exists = yield* fs.fileExists(sourceFile)
if (!exists) {
yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`)
return {
bunSuffix,
tauriTarget,
sourceFile,
targetFile,
success: false,
} satisfies CopyResult
}
yield* fs.copyFile(sourceFile, targetFile)
yield* Console.log(`${bunSuffix}${tauriTarget}`)
yield* Console.log(` ${sourceFile}`)
yield* Console.log(`${targetFile}\n`)
return {
bunSuffix,
tauriTarget,
sourceFile,
targetFile,
success: true,
} satisfies CopyResult
}),
)
return yield* Effect.all(effects, { concurrency: 'unbounded' })
}),
}
}),
)
}
/**
* 报告服务
*/
class ReporterService extends Context.Tag('ReporterService')<
ReporterService,
{
readonly printSummary: (
results: ReadonlyArray<CopyResult>,
) => Effect.Effect<void>
}
>() {
static readonly Live = Layer.succeed(ReporterService, {
printSummary: (results: ReadonlyArray<CopyResult>) =>
Effect.gen(function* () {
const successful = results.filter((r) => r.success)
const failed = results.filter((r) => !r.success)
yield* Console.log('\n📦 复制摘要:')
yield* Console.log(` ✅ 成功: ${successful.length}`)
yield* Console.log(` ⚠️ 跳过: ${failed.length}`)
if (successful.length > 0) {
yield* Console.log('\n成功复制的文件:')
for (const result of successful) {
yield* Console.log(
`${result.bunSuffix}${result.tauriTarget}`,
)
}
}
if (failed.length > 0) {
yield* Console.log('\n跳过的文件:')
for (const result of failed) {
yield* Console.log(`${result.bunSuffix} (源文件不存在)`)
}
}
}),
})
}
// ============================================================================
// Main Program
// ============================================================================
const program = Effect.gen(function* () {
const config = yield* CopyConfigService
const fs = yield* FileSystemService
const copier = yield* CopyService
const reporter = yield* ReporterService
yield* Console.log('📦 开始复制二进制文件到 Tauri sidecar 目录...\n')
// 1. 检查源目录
const sourceExists = yield* fs.dirExists(config.sourceDir)
if (!sourceExists) {
yield* Console.error(`❌ 源目录不存在: ${config.sourceDir}`)
yield* Console.log(
'💡 提示: 请先在 apps/server 中运行 bun run compile 构建服务器二进制文件',
)
return yield* Effect.fail(
new FileSystemError({
operation: 'checkSourceDir',
path: config.sourceDir,
cause: '源目录不存在',
}),
)
}
// 2. 创建目标目录
yield* fs.ensureDir(config.targetDir)
yield* Console.log(`✓ 目标目录: ${config.targetDir}\n`)
// 3. 并行复制所有二进制文件
const results = yield* copier.copyAllBinaries(config)
// 4. 输出摘要
yield* reporter.printSummary(results)
return results
})
// ============================================================================
// Layer Composition
// ============================================================================
const MainLayer = Layer.mergeAll(
CopyConfigService.Live,
FileSystemService.Live,
CopyService.Live.pipe(Layer.provide(FileSystemService.Live)),
ReporterService.Live,
)
// ============================================================================
// Runner
// ============================================================================
const runnable = program.pipe(
Effect.provide(MainLayer),
Effect.catchTags({
ConfigError: (error) =>
Console.error(`❌ 配置错误: ${error.message}`, error.cause),
FileSystemError: (error) =>
Console.error(
`❌ 文件系统错误 [${error.operation}]: ${error.path}`,
error.cause,
),
CopyError: (error) =>
Console.error(
`❌ 复制失败: ${error.source}${error.target}`,
error.cause,
),
}),
Effect.tapErrorCause((cause) => Console.error('❌ 未预期的错误:', cause)),
)
Effect.runPromise(runnable).catch(() => {
process.exit(1)
})

19
apps/desktop/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "@furtherverse/desktop",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "bun run copy && tauri build",
"copy": "rm -rf binaries && bun --bun copy.ts",
"dev": "bun run copy && tauri dev"
},
"devDependencies": {
"@effect/schema": "catalog:",
"@furtherverse/tsconfig": "workspace:*",
"@tauri-apps/cli": "catalog:",
"@types/bun": "catalog:",
"effect": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -43,20 +43,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.101" version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "app-desktop"
version = "0.1.0"
dependencies = [
"serde",
"tauri",
"tauri-build",
"tauri-plugin-shell",
"tokio",
]
[[package]] [[package]]
name = "atk" name = "atk"
@@ -167,9 +156,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.25.0" version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -179,9 +168,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -240,7 +229,7 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.18", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -255,9 +244,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.55" version = "1.2.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -706,15 +695,15 @@ dependencies = [
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.9" version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
@@ -1248,13 +1237,14 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.20" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
@@ -1271,9 +1261,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.65" version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@@ -1295,9 +1285,9 @@ dependencies = [
[[package]] [[package]]
name = "ico" name = "ico"
version = "0.5.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"png", "png",
@@ -1757,7 +1747,7 @@ dependencies = [
"once_cell", "once_cell",
"png", "png",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.17",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -1805,9 +1795,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
@@ -2277,7 +2267,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [ dependencies = [
"siphasher 1.0.2", "siphasher 1.0.1",
] ]
[[package]] [[package]]
@@ -2415,9 +2405,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2433,9 +2423,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.44" version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -2550,7 +2540,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [ dependencies = [
"getrandom 0.2.17", "getrandom 0.2.17",
"libredox", "libredox",
"thiserror 2.0.18", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -2575,9 +2565,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2587,9 +2577,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.14" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2598,15 +2588,15 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.9" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.1" version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -2623,6 +2613,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -2651,6 +2642,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -2689,9 +2686,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "1.2.1" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
dependencies = [ dependencies = [
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
@@ -2840,6 +2837,18 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.16.1" version = "3.16.1"
@@ -2852,7 +2861,7 @@ dependencies = [
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.13.0", "indexmap 2.13.0",
"schemars 0.9.0", "schemars 0.9.0",
"schemars 1.2.1", "schemars 1.2.0",
"serde_core", "serde_core",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@@ -2893,6 +2902,17 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "server-desktop"
version = "0.1.0"
dependencies = [
"serde",
"tauri",
"tauri-build",
"tauri-plugin-shell",
"tokio",
]
[[package]] [[package]]
name = "servo_arc" name = "servo_arc"
version = "0.2.0" version = "0.2.0"
@@ -2976,15 +2996,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.2" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
@@ -2994,9 +3014,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.2" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.60.2", "windows-sys 0.60.2",
@@ -3212,9 +3232,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.10.2" version = "2.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -3251,7 +3271,7 @@ dependencies = [
"tauri-runtime", "tauri-runtime",
"tauri-runtime-wry", "tauri-runtime-wry",
"tauri-utils", "tauri-utils",
"thiserror 2.0.18", "thiserror 2.0.17",
"tokio", "tokio",
"tray-icon", "tray-icon",
"url", "url",
@@ -3263,9 +3283,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.5.5" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -3285,9 +3305,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.5.4" version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@@ -3303,7 +3323,7 @@ dependencies = [
"sha2", "sha2",
"syn 2.0.114", "syn 2.0.114",
"tauri-utils", "tauri-utils",
"thiserror 2.0.18", "thiserror 2.0.17",
"time", "time",
"url", "url",
"uuid", "uuid",
@@ -3312,9 +3332,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.5.4" version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -3326,9 +3346,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.5.3" version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@@ -3343,9 +3363,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.3.5" version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" checksum = "39b76f884a3937e04b631ffdc3be506088fa979369d25147361352f2f352e5ed"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@@ -3358,15 +3378,15 @@ dependencies = [
"shared_child", "shared_child",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.18", "thiserror 2.0.17",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.10.0" version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892"
dependencies = [ dependencies = [
"cookie", "cookie",
"dpi", "dpi",
@@ -3380,7 +3400,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"thiserror 2.0.18", "thiserror 2.0.17",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
@@ -3389,9 +3409,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.10.0" version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -3416,9 +3436,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.8.2" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli", "brotli",
@@ -3444,7 +3464,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror 2.0.18", "thiserror 2.0.17",
"toml 0.9.11+spec-1.1.0", "toml 0.9.11+spec-1.1.0",
"url", "url",
"urlpattern", "urlpattern",
@@ -3485,11 +3505,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [ dependencies = [
"thiserror-impl 2.0.18", "thiserror-impl 2.0.17",
] ]
[[package]] [[package]]
@@ -3505,9 +3525,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.18" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3516,9 +3536,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.47" version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
@@ -3531,15 +3551,15 @@ dependencies = [
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.27" version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@@ -3760,7 +3780,7 @@ dependencies = [
"once_cell", "once_cell",
"png", "png",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.17",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -3874,9 +3894,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.20.0" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"js-sys", "js-sys",
@@ -4040,9 +4060,9 @@ dependencies = [
[[package]] [[package]]
name = "webkit2gtk" name = "webkit2gtk"
version = "2.0.2" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"cairo-rs", "cairo-rs",
@@ -4064,9 +4084,9 @@ dependencies = [
[[package]] [[package]]
name = "webkit2gtk-sys" name = "webkit2gtk-sys"
version = "2.0.2" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"cairo-sys-rs", "cairo-sys-rs",
@@ -4113,7 +4133,7 @@ version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.17",
"windows", "windows",
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
@@ -4585,9 +4605,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.54.1" version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ed1a195b0375491dd15a7066a10251be217ce743cf4bbbbdcf5391d6473bee0" checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2", "block2",
@@ -4617,7 +4637,7 @@ dependencies = [
"sha2", "sha2",
"soup3", "soup3",
"tao-macros", "tao-macros",
"thiserror 2.0.18", "thiserror 2.0.17",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
@@ -4674,18 +4694,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.39" version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.39" version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4748,6 +4768,6 @@ dependencies = [
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.19" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "app-desktop" name = "server-desktop"
version = "0.1.0" version = "0.1.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["imbytecat"] authors = ["imbytecat"]
@@ -11,7 +11,7 @@ edition = "2021"
# The `_lib` suffix may seem redundant but it is necessary # The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name. # to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "app_desktop_lib" name = "server_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
app_desktop_lib::run() server_desktop_lib::run()
} }

View File

@@ -86,7 +86,7 @@ pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
// 启动 sidecar // 启动 sidecar
let sidecar = app_handle let sidecar = app_handle
.shell() .shell()
.sidecar("app") .sidecar("server")
.expect("无法找到 app") .expect("无法找到 app")
.env("PORT", port.to_string()); .env("PORT", port.to_string());

View File

@@ -1,8 +1,8 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "app-desktop", "productName": "server-desktop",
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.imbytecat.app-desktop", "identifier": "com.imbytecat.server-desktop",
"app": { "app": {
"withGlobalTauri": true, "withGlobalTauri": true,
"windows": [], "windows": [],
@@ -20,6 +20,6 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"externalBin": ["binaries/app"] "externalBin": ["binaries/server"]
} }
} }

View File

@@ -0,0 +1,4 @@
{
"extends": "@furtherverse/tsconfig/bun.json",
"exclude": ["node_modules", "src-tauri"]
}

14
apps/desktop/turbo.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["@furtherverse/server#compile"],
"outputs": ["src-tauri/target/release/**"]
},
"dev": {
"dependsOn": ["@furtherverse/server#compile"],
"with": ["@furtherverse/server#dev"]
}
}
}

201
apps/server/AGENTS.md Normal file
View File

@@ -0,0 +1,201 @@
# AGENTS.md - Server App Guidelines
TanStack Start fullstack web app with ORPC (contract-first RPC).
## Tech Stack
- **Framework**: TanStack Start (React 19 SSR, file-based routing)
- **Runtime**: Bun
- **Language**: TypeScript (strict mode)
- **Styling**: Tailwind CSS v4
- **Database**: PostgreSQL + Drizzle ORM
- **State**: TanStack Query v5
- **RPC**: ORPC (contract-first, type-safe)
- **Build**: Vite + Nitro
## Commands
```bash
# Development
bun dev # Vite dev server (localhost:3000)
bun db:studio # Drizzle Studio GUI
# Build
bun build # Production build → .output/
bun compile # Compile to standalone binary
# Code Quality
bun fix # Biome auto-fix
bun typecheck # TypeScript check
# Database
bun db:generate # Generate migrations from schema
bun db:migrate # Run migrations
bun db:push # Push schema directly (dev only)
# Testing (not yet configured)
bun test path/to/test.ts # Run single test
bun test -t "pattern" # Run tests matching pattern
```
## Directory Structure
```
src/
├── client/ # Client-side code
│ ├── orpc.client.ts # ORPC isomorphic client
│ └── query-client.ts # TanStack Query client
├── components/ # React components
├── routes/ # TanStack Router file routes
│ ├── __root.tsx # Root layout
│ ├── index.tsx # Home page
│ └── api/
│ └── rpc.$.ts # ORPC HTTP endpoint
├── server/ # Server-side code
│ ├── api/ # ORPC layer
│ │ ├── contracts/ # Input/output schemas (Zod)
│ │ ├── middlewares/ # Middleware (db provider, auth)
│ │ ├── routers/ # Handler implementations
│ │ ├── context.ts # Request context
│ │ ├── server.ts # ORPC server instance
│ │ └── types.ts # Type exports
│ └── db/
│ ├── schema/ # Drizzle table definitions
│ └── index.ts # Database instance
├── env.ts # Environment variable validation
├── router.tsx # Router configuration
├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
└── styles.css # Tailwind entry
```
## ORPC Pattern
### 1. Define Contract (`src/server/api/contracts/feature.contract.ts`)
```typescript
import { oc } from '@orpc/contract'
import { createSelectSchema } from 'drizzle-zod'
import { z } from 'zod'
import { featureTable } from '@/server/db/schema'
const selectSchema = createSelectSchema(featureTable)
export const list = oc.input(z.void()).output(z.array(selectSchema))
export const create = oc.input(insertSchema).output(selectSchema)
```
### 2. Implement Router (`src/server/api/routers/feature.router.ts`)
```typescript
import { ORPCError } from '@orpc/server'
import { db } from '../middlewares'
import { os } from '../server'
export const list = os.feature.list.use(db).handler(async ({ context }) => {
return await context.db.query.featureTable.findMany()
})
```
### 3. Register in Index Files
```typescript
// src/server/api/contracts/index.ts
import * as feature from './feature.contract'
export const contract = { feature }
// src/server/api/routers/index.ts
import * as feature from './feature.router'
export const router = os.router({ feature })
```
### 4. Use in Components
```typescript
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
import { orpc } from '@/client/orpc.client'
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
const mutation = useMutation(orpc.feature.create.mutationOptions())
```
## Database Schema (Drizzle)
```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const myTable = pgTable('my_table', {
id: uuid().primaryKey().default(sql`uuidv7()`),
name: text().notNull(),
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
})
```
## Code Style
### Formatting (Biome)
- **Indent**: 2 spaces
- **Quotes**: Single `'`
- **Semicolons**: Omit (ASI)
- **Arrow parens**: Always `(x) => x`
### Imports
Biome auto-organizes:
1. External packages
2. Internal `@/*` aliases
3. Type imports (`import type { ... }`)
```typescript
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { db } from '@/server/db'
import type { ReactNode } from 'react'
```
### TypeScript
- `strict: true`
- `noUncheckedIndexedAccess: true` - array access returns `T | undefined`
- Use `@/*` path aliases (maps to `src/*`)
### Naming
| Type | Convention | Example |
|------|------------|---------|
| Files (utils) | kebab-case | `auth-utils.ts` |
| Files (components) | PascalCase | `UserProfile.tsx` |
| Components | PascalCase arrow | `const Button = () => {}` |
| Functions | camelCase | `getUserById` |
| Types | PascalCase | `UserProfile` |
### React
- Use arrow functions for components (Biome enforced)
- Use `useSuspenseQuery` for guaranteed data
- Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
## Environment Variables
```typescript
// src/env.ts - using @t3-oss/env-core
import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod'
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
},
clientPrefix: 'VITE_',
client: {
VITE_API_URL: z.string().optional(),
},
})
```
## Critical Rules
**DO:**
- Run `bun fix` before committing
- Use `@/*` path aliases
- Include `createdAt`/`updatedAt` on all tables
- Use `ORPCError` with proper codes
**DON'T:**
- Edit `src/routeTree.gen.ts` (auto-generated)
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
- Commit `.env` files
- Use empty catch blocks

7
apps/server/biome.json Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": "//",
"files": {
"includes": ["**", "!**/routeTree.gen.ts"]
}
}

View File

@@ -6,14 +6,6 @@ import { Console, Context, Data, Effect, Layer } from 'effect'
// Domain Models & Schema // Domain Models & Schema
// ============================================================================ // ============================================================================
const targetMap = {
'bun-windows-x64': 'x86_64-pc-windows-msvc',
'bun-darwin-arm64': 'aarch64-apple-darwin',
'bun-darwin-x64': 'x86_64-apple-darwin',
'bun-linux-x64': 'x86_64-unknown-linux-gnu',
'bun-linux-arm64': 'aarch64-unknown-linux-gnu',
} as const
const BunTargetSchema = Schema.Literal( const BunTargetSchema = Schema.Literal(
'bun-windows-x64', 'bun-windows-x64',
'bun-darwin-arm64', 'bun-darwin-arm64',
@@ -22,11 +14,19 @@ const BunTargetSchema = Schema.Literal(
'bun-linux-arm64', 'bun-linux-arm64',
) )
/**
* bun target ( 'bun-' )
*/
const getTargetSuffix = (target: BunTarget): string => {
return target.replace('bun-', '')
}
type BunTarget = Schema.Schema.Type<typeof BunTargetSchema> type BunTarget = Schema.Schema.Type<typeof BunTargetSchema>
const BuildConfigSchema = Schema.Struct({ const BuildConfigSchema = Schema.Struct({
entrypoint: Schema.String.pipe(Schema.nonEmptyString()), entrypoint: Schema.String.pipe(Schema.nonEmptyString()),
outputDir: Schema.String.pipe(Schema.nonEmptyString()), outputDir: Schema.String.pipe(Schema.nonEmptyString()),
outfile: Schema.String.pipe(Schema.nonEmptyString()),
targets: Schema.Array(BunTargetSchema).pipe(Schema.minItems(1)), targets: Schema.Array(BunTargetSchema).pipe(Schema.minItems(1)),
}) })
@@ -94,10 +94,10 @@ class BuildConfigService extends Context.Tag('BuildConfigService')<
BuildConfigService, BuildConfigService,
BuildConfigService.fromRaw({ BuildConfigService.fromRaw({
entrypoint: '.output/server/index.mjs', entrypoint: '.output/server/index.mjs',
// outputDir: 'out', outputDir: 'out',
outputDir: 'src-tauri/binaries', outfile: 'server',
targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'], targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'],
}), } satisfies BuildConfig),
) )
} }
@@ -150,7 +150,7 @@ class BuildService extends Context.Tag('BuildService')<
Bun.build({ Bun.build({
entrypoints: [config.entrypoint], entrypoints: [config.entrypoint],
compile: { compile: {
outfile: `app-${targetMap[target]}`, outfile: `${config.outfile}-${getTargetSuffix(target)}`,
target: target, target: target,
}, },
outdir: config.outputDir, outdir: config.outputDir,
@@ -181,7 +181,7 @@ class BuildService extends Context.Tag('BuildService')<
Bun.build({ Bun.build({
entrypoints: [config.entrypoint], entrypoints: [config.entrypoint],
compile: { compile: {
outfile: `app-${targetMap[target]}`, outfile: `${config.outfile}-${getTargetSuffix(target)}`,
target: target, target: target,
}, },
outdir: config.outputDir, outdir: config.outputDir,

View File

@@ -3,7 +3,7 @@ import { env } from '@/env'
export default defineConfig({ export default defineConfig({
out: './drizzle', out: './drizzle',
schema: './src/db/schema/index.ts', schema: './src/server/db/schema/index.ts',
dialect: 'postgresql', dialect: 'postgresql',
dbCredentials: { dbCredentials: {
url: env.DATABASE_URL, url: env.DATABASE_URL,

59
apps/server/package.json Normal file
View File

@@ -0,0 +1,59 @@
{
"name": "@furtherverse/server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"compile": "bun build.ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "vite dev",
"fix": "biome check --write",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@furtherverse/utils": "workspace:*",
"@orpc/client": "catalog:",
"@orpc/contract": "catalog:",
"@orpc/openapi": "catalog:",
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "catalog:",
"@orpc/zod": "catalog:",
"@t3-oss/env-core": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
"@tanstack/react-router-ssr-query": "catalog:",
"@tanstack/react-start": "catalog:",
"@tauri-apps/api": "catalog:",
"drizzle-orm": "catalog:",
"drizzle-zod": "catalog:",
"postgres": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"uuid": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@effect/platform": "catalog:",
"@effect/schema": "catalog:",
"@furtherverse/tsconfig": "workspace:*",
"@tailwindcss/vite": "catalog:",
"@tanstack/devtools-vite": "catalog:",
"@tanstack/react-devtools": "catalog:",
"@tanstack/react-query-devtools": "catalog:",
"@tanstack/react-router-devtools": "catalog:",
"@types/bun": "catalog:",
"@vitejs/plugin-react": "catalog:",
"babel-plugin-react-compiler": "catalog:",
"drizzle-kit": "catalog:",
"effect": "catalog:",
"nitro": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:"
}
}

View File

@@ -0,0 +1,24 @@
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { createRouterClient } from '@orpc/server'
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { router } from '@/server/api/routers'
import type { RouterClient } from '@/server/api/types'
const getORPCClient = createIsomorphicFn()
.server(() =>
createRouterClient(router, {
context: () => ({
headers: getRequestHeaders(),
}),
}),
)
.client(() => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
})
return createORPCClient<RouterClient>(link)
})
export const orpc: RouterClient = getORPCClient()

View File

@@ -0,0 +1,30 @@
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
import { orpc as orpcClient } from './orpc.client'
export const orpc = createTanstackQueryUtils(orpcClient, {
experimental_defaults: {
todo: {
create: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
update: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
remove: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
},
},
})

View File

@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiSplatRouteImport } from './routes/api/$'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
@@ -17,6 +18,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ApiSplatRoute = ApiSplatRouteImport.update({
id: '/api/$',
path: '/api/$',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
id: '/api/rpc/$', id: '/api/rpc/$',
path: '/api/rpc/$', path: '/api/rpc/$',
@@ -25,27 +31,31 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/api/$': typeof ApiSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/api/$': typeof ApiSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/api/$': typeof ApiSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/api/rpc/$' fullPaths: '/' | '/api/$' | '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/api/rpc/$' to: '/' | '/api/$' | '/api/rpc/$'
id: '__root__' | '/' | '/api/rpc/$' id: '__root__' | '/' | '/api/$' | '/api/rpc/$'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
ApiSplatRoute: typeof ApiSplatRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute
} }
@@ -58,6 +68,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/api/$': {
id: '/api/$'
path: '/api/$'
fullPath: '/api/$'
preLoaderRoute: typeof ApiSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/api/rpc/$': { '/api/rpc/$': {
id: '/api/rpc/$' id: '/api/rpc/$'
path: '/api/rpc/$' path: '/api/rpc/$'
@@ -70,6 +87,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
ApiSplatRoute: ApiSplatRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute, ApiRpcSplatRoute: ApiRpcSplatRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -1,15 +1,15 @@
import { TanStackDevtools } from '@tanstack/react-devtools' import { TanStackDevtools } from '@tanstack/react-devtools'
import type { QueryClient } from '@tanstack/react-query' import type { QueryClient } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { import {
createRootRouteWithContext, createRootRouteWithContext,
HeadContent, HeadContent,
Scripts, Scripts,
} from '@tanstack/react-router' } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { ErrorComponent } from '@/components/Error' import { ErrorComponent } from '@/components/Error'
import { NotFoundComponent } from '@/components/NotFound' import { NotFoundComponent } from '@/components/NotFount'
import { devtools as queryDevtools } from '@/integrations/tanstack-query'
import { devtools as routerDevtools } from '@/integrations/tanstack-router'
import appCss from '@/styles.css?url' import appCss from '@/styles.css?url'
export interface RouterContext { export interface RouterContext {
@@ -27,7 +27,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
content: 'width=device-width, initial-scale=1', content: 'width=device-width, initial-scale=1',
}, },
{ {
title: 'Fullstack Starter', title: 'Furtherverse',
}, },
], ],
links: [ links: [
@@ -54,7 +54,16 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
config={{ config={{
position: 'bottom-right', position: 'bottom-right',
}} }}
plugins={[routerDevtools, queryDevtools]} plugins={[
{
name: 'TanStack Router',
render: <TanStackRouterDevtoolsPanel />,
},
{
name: 'TanStack Query',
render: <ReactQueryDevtoolsPanel />,
},
]}
/> />
<Scripts /> <Scripts />
</body> </body>

View File

@@ -0,0 +1,86 @@
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
import { ORPCError, onError, ValidationError } from '@orpc/server'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { name, version } from '@/../package.json'
import { router } from '@/server/api/routers'
const handler = new OpenAPIHandler(router, {
plugins: [
new OpenAPIReferencePlugin({
docsProvider: 'scalar',
schemaConverters: [new ZodToJsonSchemaConverter()],
specGenerateOptions: {
info: {
title: name,
version,
},
// components: {
// securitySchemes: {
// bearerAuth: {
// type: 'http',
// scheme: 'bearer',
// },
// },
// },
},
docsPath: '/docs',
specPath: '/spec.json',
}),
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
clientInterceptors: [
onError((error) => {
if (
error instanceof ORPCError &&
error.code === 'BAD_REQUEST' &&
error.cause instanceof ValidationError
) {
// If you only use Zod you can safely cast to ZodIssue[]
const zodError = new z.ZodError(
error.cause.issues as z.core.$ZodIssue[],
)
throw new ORPCError('INPUT_VALIDATION_FAILED', {
status: 422,
message: z.prettifyError(zodError),
data: z.flattenError(zodError),
cause: error.cause,
})
}
if (
error instanceof ORPCError &&
error.code === 'INTERNAL_SERVER_ERROR' &&
error.cause instanceof ValidationError
) {
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
cause: error.cause,
})
}
}),
],
})
export const Route = createFileRoute('/api/$')({
server: {
handlers: {
ANY: async ({ request }) => {
const { response } = await handler.handle(request, {
prefix: '/api',
context: {
headers: request.headers,
},
})
return response ?? new Response('Not Found', { status: 404 })
},
},
},
})

View File

@@ -2,7 +2,7 @@ import { ORPCError, onError, ValidationError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch' import { RPCHandler } from '@orpc/server/fetch'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod' import { z } from 'zod'
import { router } from '@/orpc/router' import { router } from '@/server/api/routers'
const handler = new RPCHandler(router, { const handler = new RPCHandler(router, {
interceptors: [ interceptors: [
@@ -49,7 +49,9 @@ export const Route = createFileRoute('/api/rpc/$')({
ANY: async ({ request }) => { ANY: async ({ request }) => {
const { response } = await handler.handle(request, { const { response } = await handler.handle(request, {
prefix: '/api/rpc', prefix: '/api/rpc',
context: {}, context: {
headers: request.headers,
},
}) })
return response ?? new Response('Not Found', { status: 404 }) return response ?? new Response('Not Found', { status: 404 })

View File

@@ -4,7 +4,7 @@ import { isTauri } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import type { ChangeEventHandler, FormEventHandler } from 'react' import type { ChangeEventHandler, FormEventHandler } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { orpc } from '@/orpc' import { orpc } from '@/client/query-client'
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
component: Todos, component: Todos,

View File

@@ -0,0 +1,25 @@
import type { DB } from '@/server/db'
/**
* 基础 Context - 所有请求都包含的上下文
*/
export interface BaseContext {
headers: Headers
}
/**
* 数据库 Context - 通过 db middleware 扩展
*/
export interface DBContext extends BaseContext {
db: DB
}
/**
* 认证 Context - 通过 auth middleware 扩展(未来使用)
*
* @example
* export interface AuthContext extends DBContext {
* userId: string
* user: User
* }
*/

View File

@@ -0,0 +1,7 @@
import * as todo from './todo.contract'
export const contract = {
todo,
}
export type Contract = typeof contract

View File

@@ -5,7 +5,7 @@ import {
createUpdateSchema, createUpdateSchema,
} from 'drizzle-zod' } from 'drizzle-zod'
import { z } from 'zod' import { z } from 'zod'
import { todoTable } from '@/db/schema' import { todoTable } from '@/server/db/schema'
const selectSchema = createSelectSchema(todoTable) const selectSchema = createSelectSchema(todoTable)

View File

@@ -0,0 +1,11 @@
import { os } from '@orpc/server'
import { getDB } from '@/server/db'
export const db = os.middleware(async ({ context, next }) => {
return next({
context: {
...context,
db: getDB(),
},
})
})

View File

@@ -0,0 +1 @@
export * from './db.middleware'

View File

@@ -0,0 +1,6 @@
import { os } from '../server'
import * as todo from './todo.router'
export const router = os.router({
todo,
})

View File

@@ -1,17 +1,19 @@
import { ORPCError } from '@orpc/server' import { ORPCError } from '@orpc/server'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { todoTable } from '@/db/schema' import { todoTable } from '@/server/db/schema'
import { baseProcedure } from '@/orpc/procedures' import { db } from '../middlewares'
import { os } from '../server'
export const list = baseProcedure.todo.list.handler(async ({ context }) => { export const list = os.todo.list.use(db).handler(async ({ context }) => {
const todos = await context.db.query.todoTable.findMany({ const todos = await context.db.query.todoTable.findMany({
orderBy: (todos, { desc }) => [desc(todos.createdAt)], orderBy: (todos, { desc }) => [desc(todos.createdAt)],
}) })
return todos return todos
}) })
export const create = baseProcedure.todo.create.handler( export const create = os.todo.create
async ({ context, input }) => { .use(db)
.handler(async ({ context, input }) => {
const [newTodo] = await context.db const [newTodo] = await context.db
.insert(todoTable) .insert(todoTable)
.values(input) .values(input)
@@ -22,11 +24,11 @@ export const create = baseProcedure.todo.create.handler(
} }
return newTodo return newTodo
}, })
)
export const update = baseProcedure.todo.update.handler( export const update = os.todo.update
async ({ context, input }) => { .use(db)
.handler(async ({ context, input }) => {
const [updatedTodo] = await context.db const [updatedTodo] = await context.db
.update(todoTable) .update(todoTable)
.set(input.data) .set(input.data)
@@ -38,11 +40,10 @@ export const update = baseProcedure.todo.update.handler(
} }
return updatedTodo return updatedTodo
}, })
)
export const remove = baseProcedure.todo.remove.handler( export const remove = os.todo.remove
async ({ context, input }) => { .use(db)
.handler(async ({ context, input }) => {
await context.db.delete(todoTable).where(eq(todoTable.id, input.id)) await context.db.delete(todoTable).where(eq(todoTable.id, input.id))
}, })
)

View File

@@ -0,0 +1,5 @@
import { implement } from '@orpc/server'
import type { BaseContext } from './context'
import { contract } from './contracts'
export const os = implement(contract).$context<BaseContext>()

View File

@@ -3,9 +3,8 @@ import type {
InferContractRouterInputs, InferContractRouterInputs,
InferContractRouterOutputs, InferContractRouterOutputs,
} from '@orpc/contract' } from '@orpc/contract'
import type { contract } from './contract' import type { Contract } from './contracts'
export type Contract = typeof contract
export type RouterClient = ContractRouterClient<Contract> export type RouterClient = ContractRouterClient<Contract>
export type RouterInputs = InferContractRouterInputs<Contract> export type RouterInputs = InferContractRouterInputs<Contract>
export type RouterOutputs = InferContractRouterOutputs<Contract> export type RouterOutputs = InferContractRouterOutputs<Contract>

View File

@@ -0,0 +1,27 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import { env } from '@/env'
import * as schema from '@/server/db/schema'
export const createDB = () =>
drizzle({
connection: {
url: env.DATABASE_URL,
prepare: true,
},
schema,
})
export type DB = ReturnType<typeof createDB>
export const getDB = (() => {
let db: DB | null = null
return (singleton = true): DB => {
if (!singleton) {
return createDB()
}
db ??= createDB()
return db
}
})()

View File

@@ -0,0 +1,8 @@
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
import { generatedFields } from './utils/field'
export const todoTable = pgTable('todo', {
...generatedFields,
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
})

View File

@@ -0,0 +1,58 @@
import { sql } from 'drizzle-orm'
import { timestamp, uuid } from 'drizzle-orm/pg-core'
import { v7 as uuidv7 } from 'uuid'
// id
export const id = (name: string) => uuid(name)
export const pk = (name: string, strategy?: 'native' | 'extension') => {
switch (strategy) {
// PG 18+
case 'native':
return id(name).primaryKey().default(sql`uuidv7()`)
// PG 13+ with extension
case 'extension':
return id(name).primaryKey().default(sql`uuid_generate_v7()`)
// Any PG version
default:
return id(name)
.primaryKey()
.$defaultFn(() => uuidv7())
}
}
// timestamp
export const createdAt = (name = 'created_at') =>
timestamp(name, { withTimezone: true }).notNull().defaultNow()
export const updatedAt = (name = 'updated_at') =>
timestamp(name, { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date())
// generated fields
export const generatedFields = {
id: pk('id'),
createdAt: createdAt('created_at'),
updatedAt: updatedAt('updated_at'),
}
// Helper to create omit keys from generatedFields
const createGeneratedFieldKeys = <T extends Record<string, unknown>>(
fields: T,
): Record<keyof T, true> => {
return Object.keys(fields).reduce(
(acc, key) => {
acc[key as keyof T] = true
return acc
},
{} as Record<keyof T, true>,
)
}
export const generatedFieldKeys = createGeneratedFieldKeys(generatedFields)

View File

@@ -0,0 +1,9 @@
{
"extends": "@furtherverse/tsconfig/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

10
apps/server/turbo.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"compile": {
"dependsOn": ["build"],
"outputs": ["out/**"]
}
}
}

View File

@@ -6,7 +6,6 @@
"useIgnoreFile": true "useIgnoreFile": true
}, },
"files": { "files": {
"includes": ["**", "!**/routeTree.gen.ts"],
"ignoreUnknown": false "ignoreUnknown": false
}, },
"formatter": { "formatter": {

1005
bun.lock

File diff suppressed because it is too large Load Diff

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[install]
publicHoistPattern = ["@types/*", "bun-types", "nitro*"]

View File

View File

@@ -1,4 +1,4 @@
[tools] [tools]
node = "24" node = "latest"
bun = "1" bun = "1"
rust = 'latest' rust = 'latest'

View File

@@ -1,64 +1,65 @@
{ {
"name": "fullstack-starter", "name": "@furtherverse/monorepo",
"version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": { "scripts": {
"build": "turbo build:tauri", "build": "turbo run build",
"build:compile": "bun build.ts", "compile": "turbo run compile",
"build:tauri": "NO_STRIP=1 tauri build", "deploy": "turbo run deploy",
"build:vite": "vite build", "dev": "turbo run dev",
"db:generate": "drizzle-kit generate", "fix": "turbo run fix",
"db:migrate": "drizzle-kit migrate", "typecheck": "turbo run typecheck"
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "turbo dev:tauri",
"dev:tauri": "tauri dev",
"dev:vite": "vite dev",
"fix": "biome check --write",
"typecheck": "tsc -b"
}, },
"dependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11",
"turbo": "^2.7.5"
},
"catalog": {
"@biomejs/biome": "^2.3.11",
"@effect/platform": "^0.94.2",
"@effect/schema": "^0.75.5",
"@orpc/client": "^1.13.4", "@orpc/client": "^1.13.4",
"@orpc/contract": "^1.13.4", "@orpc/contract": "^1.13.4",
"@orpc/openapi": "^1.13.4",
"@orpc/server": "^1.13.4", "@orpc/server": "^1.13.4",
"@orpc/tanstack-query": "^1.13.4", "@orpc/tanstack-query": "^1.13.4",
"@orpc/zod": "^1.13.4", "@orpc/zod": "^1.13.4",
"@t3-oss/env-core": "^0.13.10", "@t3-oss/env-core": "^0.13.10",
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-router": "^1.158.1",
"@tanstack/react-router-ssr-query": "^1.158.1",
"@tanstack/react-start": "^1.158.3",
"@tauri-apps/api": "^2.10.1",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"postgres": "^3.4.8",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^2.3.14",
"@effect/platform": "^0.94.3",
"@effect/schema": "^0.75.5",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/devtools-vite": "^0.4.1", "@tanstack/devtools-vite": "^0.4.1",
"@tanstack/react-devtools": "^0.9.4", "@tanstack/react-devtools": "^0.9.2",
"@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-query": "^5.90.19",
"@tanstack/react-router-devtools": "^1.158.1", "@tanstack/react-query-devtools": "^5.91.2",
"@tauri-apps/cli": "^2.10.0", "@tanstack/react-router": "^1.154.12",
"@types/bun": "^1.3.8", "@tanstack/react-router-devtools": "^1.154.12",
"@vitejs/plugin-react": "^5.1.3", "@tanstack/react-router-ssr-query": "^1.154.12",
"@tanstack/react-start": "^1.154.12",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/cli": "^2.9.6",
"@types/bun": "^1.3.6",
"@vitejs/plugin-react": "^5.1.2",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"effect": "^3.19.16", "drizzle-orm": "^0.45.1",
"nitro": "npm:nitro-nightly@latest", "drizzle-zod": "^0.8.3",
"effect": "^3.19.15",
"nitro": "npm:nitro-nightly@3.0.1-20260122-201913-dfdff9e9",
"ohash": "^2.0.11",
"postgres": "^3.4.8",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"turbo": "^2.8.3", "turbo": "^2.7.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.0-beta.13", "uuid": "^13.0.0",
"vite-tsconfig-paths": "^6.0.5" "systeminformation": "^5.30.5",
}, "vite": "^8.0.0-beta.9",
"overrides": { "vite-tsconfig-paths": "^6.0.4",
"@tanstack/query-core": "^5.90.20" "zod": "^4.3.6"
} }
} }

View File

@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base",
"compilerOptions": {
"target": "esnext",
"lib": ["ESNext"],
"module": "preserve",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
},
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Bun",
"extends": "./base.json",
"compilerOptions": {
"types": ["bun-types"]
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@furtherverse/tsconfig",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
"./base.json": "./base.json",
"./bun.json": "./bun.json",
"./react.json": "./react.json"
}
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "@furtherverse/utils",
"version": "1.0.0",
"private": true,
"type": "module",
"imports": {
"#*": "./src/*"
},
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"dependencies": {
"ohash": "catalog:",
"systeminformation": "catalog:"
},
"devDependencies": {
"@furtherverse/tsconfig": "workspace:*",
"typescript": "catalog:"
}
}

View File

@@ -0,0 +1,29 @@
import { hash } from 'ohash'
import si from 'systeminformation'
async function getSystemInfo() {
const [uuid, baseboard, bios, system, diskLayout, networkInterfaces] =
await Promise.all([
si.uuid(),
si.baseboard(),
si.bios(),
si.system(),
si.diskLayout(),
si.networkInterfaces(),
])
return {
uuid,
baseboard,
bios,
system,
diskLayout,
networkInterfaces,
}
}
export async function getHardwareFingerprint() {
const systemInfo = await getSystemInfo()
return hash(systemInfo)
}

View File

@@ -0,0 +1 @@
export * from './fingerprint'

View File

@@ -1,357 +0,0 @@
# AGENTS.md - Tauri Shell 项目开发指南
本文档为 AI 编程助手和开发者提供项目规范、构建命令和代码风格指南。
## 项目概览
- **项目类型**: Tauri v2 桌面应用(轻量级壳子)
- **后端**: Rust (Edition 2021)
- **架构**: Sidecar 模式 - Sidecar App 承载主要业务逻辑
- **设计理念**: Tauri 仅提供原生桌面能力文件对话框、系统通知等Web 逻辑全部由 Sidecar App 处理
- **开发模式**: 使用 localhost:3000需手动启动开发服务器
- **生产模式**: 自动启动 Sidecar 二进制
- **异步运行时**: Tokio
- **Rust 版本**: 1.92.0+
- **工具管理**: 使用 mise 管理 Rust 和 Tauri CLI 版本(见 `mise.toml`
## 构建、测试、运行命令
### 开发运行
```bash
# 开发模式运行 (需要先启动开发服务器)
# 终端 1: 启动前端开发服务器
bun run dev
# 终端 2: 启动 Tauri 应用
tauri dev
# 或者使用单命令并行启动(需要配置 package.json
bun run dev:tauri
```
**开发模式说明**
- 开发模式下Tauri 直接连接到 `localhost:3000`(不启动 sidecar 二进制)
- 需要手动运行 `bun run dev` 来启动开发服务器
- 支持热重载HMR无需重启 Tauri 应用
### 构建
```bash
# 开发构建 (debug mode)
cargo build
# 生产构建
cargo build --release
# Tauri 应用打包 (生成安装程序)
tauri build
```
### 代码检查
```bash
# 编译检查 (不生成二进制)
cargo check
# Clippy 代码质量检查
cargo clippy
# Clippy 严格模式 (所有警告视为错误)
cargo clippy -- -D warnings
# 代码格式化检查
cargo fmt -- --check
# 自动格式化代码
cargo fmt
```
### 测试
```bash
# 运行所有测试
cargo test
# 运行单个测试 (按名称过滤)
cargo test test_function_name
# 运行特定模块的测试
cargo test module_name::
# 显示测试输出 (包括 println!)
cargo test -- --nocapture
# 运行单个测试并显示输出
cargo test test_name -- --nocapture
```
### 清理
```bash
# 清理构建产物
cargo clean
```
## 项目结构
```
app-desktop/
├── src/
│ ├── main.rs # 入口文件 (仅调用 lib::run)
│ ├── lib.rs # 核心应用逻辑 (注册插件、命令、状态)
│ ├── commands/
│ │ └── mod.rs # 原生桌面功能命令 (文件对话框、通知等)
│ └── sidecar.rs # Sidecar 进程管理 (启动、端口扫描、清理)
├── binaries/ # Sidecar 二进制文件
│ └── app-* # Sidecar App 可执行文件 (示例: app)
├── capabilities/ # Tauri v2 权限配置
│ └── default.json
├── icons/ # 应用图标资源
├── gen/schemas/ # 自动生成的 Schema (不要手动编辑)
├── Cargo.toml # Rust 项目配置
├── tauri.conf.json # Tauri 应用配置
├── build.rs # Rust 构建脚本
└── mise.toml # 开发工具版本管理
```
## Rust 代码风格指南
### 导入 (Imports)
- 使用标准库、外部 crate、当前 crate 的顺序,用空行分隔
- 按字母顺序排列
- 优先使用具体导入而非通配符 `*`
```rust
// ✅ 推荐
use std::sync::Mutex;
use std::time::Duration;
use tauri::Manager;
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::{CommandEvent, CommandChild};
// ❌ 避免
use tauri::*;
```
### 命名规范
- **函数和变量**: `snake_case`
- **类型、结构体、枚举、Trait**: `PascalCase`
- **常量和静态变量**: `SCREAMING_SNAKE_CASE`
- **生命周期参数**: 简短小写字母,如 `'a`, `'b`
```rust
// ✅ 推荐
struct SidecarProcess(Mutex<Option<CommandChild>>);
const DEFAULT_PORT: u16 = 3000;
async fn find_available_port(start: u16) -> u16 { }
// ❌ 避免
struct sidecar_process { }
const defaultPort: u16 = 3000;
```
### 类型注解
- 函数参数必须有类型注解
- 函数返回值必须明确声明 (除非返回 `()`)
- 优先使用具体类型而非 `impl Trait` (除非必要)
- 使用 `&str` 而非 `String` 作为只读字符串参数
```rust
// ✅ 推荐
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
async fn is_port_available(port: u16) -> bool {
tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.is_ok()
}
```
### 错误处理
- 使用 `Result<T, E>` 返回可能失败的操作
- 使用 `expect()` 时提供有意义的错误消息 (中文)
- 避免 `unwrap()` 在生产代码中,除非逻辑上保证不会 panic
- 使用 `?` 操作符传播错误
- 记录关键错误信息到控制台
```rust
// ✅ 推荐
let sidecar = app_handle
.shell()
.sidecar("app")
.expect("无法找到 app sidecar");
let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败");
// 日志记录
eprintln!("✗ Sidecar App 启动失败");
println!("✓ Sidecar App 启动成功!");
// ❌ 避免
let data = read_file().unwrap(); // 无上下文信息
```
### 异步代码
- 使用 `async/await` 而非手动创建 Future
- Tauri 内部使用 `tauri::async_runtime::spawn` 启动异步任务
- 使用 Tokio 的异步 API (如 `tokio::net::TcpListener`)
- 避免阻塞异步运行时 (使用 `tokio::task::spawn_blocking`)
```rust
// ✅ 推荐
tauri::async_runtime::spawn(async move {
let port = find_available_port(3000).await;
// ...
});
```
### 格式化
- 使用 `cargo fmt` 自动格式化
- 缩进: 4 空格
- 行宽: 100 字符 (rustfmt 默认)
- 结构体和枚举的字段每行一个 (如果超过一定长度)
- 链式调用适当换行提高可读性
### 注释
- 使用中文注释说明复杂逻辑
- 代码块前添加简短说明注释
- 避免显而易见的注释
```rust
// ✅ 推荐
// 全局状态:存储 Sidecar App 进程句柄
struct SidecarProcess(Mutex<Option<CommandChild>>);
// 检查端口是否可用
async fn is_port_available(port: u16) -> bool { }
```
## Tauri 特定规范
### 模块组织
- **`lib.rs`**: 主入口,负责注册插件、命令、状态管理
- **`commands/mod.rs`**: 所有 Tauri 命令集中定义,命令必须是 `pub fn`
- **`sidecar.rs`**: Sidecar 进程管理逻辑,导出公共 API`spawn_sidecar`, `cleanup_sidecar_process`
```rust
// lib.rs - 模块声明
mod commands;
mod sidecar;
use sidecar::SidecarProcess;
// 注册命令时使用模块路径
.invoke_handler(tauri::generate_handler![commands::greet])
```
### 命令定义
- 使用 `#[tauri::command]` 宏标记命令
- 命令函数必须是公开的或在 `invoke_handler` 中注册
- 参数类型必须实现 `serde::Deserialize`
- 返回类型必须实现 `serde::Serialize`
```rust
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// 在 Builder 中注册
.invoke_handler(tauri::generate_handler![greet])
```
### 状态管理
- 使用 `app.manage()` 注册全局状态
- 状态必须实现 `Send + Sync`
- 使用 `Mutex``RwLock` 保证线程安全
```rust
struct SidecarProcess(Mutex<Option<CommandChild>>);
// 注册状态
app.manage(SidecarProcess(Mutex::new(None)));
// 访问状态
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
*state.0.lock().unwrap() = Some(child);
}
```
### Sidecar 进程管理
- Sidecar 二进制必须在 `tauri.conf.json``bundle.externalBin` 中声明
- 使用 `app.shell().sidecar()` 启动 sidecar
- 在应用退出时清理子进程 (监听 `RunEvent::ExitRequested`)
```rust
// 启动 sidecar
let sidecar = app_handle
.shell()
.sidecar("app")
.expect("无法找到 app sidecar")
.env("PORT", port.to_string());
// 清理进程
match event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
if let Some(child) = process.take() {
let _ = child.kill();
}
}
_ => {}
}
```
## 依赖管理
-`Cargo.toml` 中明确声明依赖版本
- 使用语义化版本 (如 `"2"` 表示兼容 2.x.x)
- 仅启用需要的 feature 以减少编译时间和二进制大小
```toml
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["net"] }
```
## 开发工具
推荐安装以下 VSCode 扩展:
- `tauri-apps.tauri-vscode` - Tauri 官方支持
- `rust-lang.rust-analyzer` - Rust 语言服务器
## 最佳实践
1. **开发环境配置**:
- 开发模式下需先启动前端开发服务器(`bun run dev`),再启动 Tauri`tauri dev`
- 生产构建自动打包 sidecar 二进制,无需额外配置
2. **进程生命周期**: 始终在应用退出时清理子进程和资源
3. **端口管理**:
- 开发模式固定使用 3000 端口(与开发服务器匹配)
- 生产模式使用端口扫描避免硬编码端口冲突
4. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒)
5. **日志**: 使用表情符号 (✓/✗/🔧/🚀) 和中文消息提供清晰的状态反馈
6. **错误退出**: 关键错误时调用 `std::process::exit(1)`
7. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口
## 提交代码前检查清单
- [ ] `cargo fmt` 格式化通过
- [ ] `cargo clippy` 无警告
- [ ] `cargo check` 编译通过
- [ ] `cargo test` 测试通过
- [ ] 更新相关注释和文档
- [ ] 检查是否有 `unwrap()` 需要替换为 `expect()`
- [ ] 验证 Tauri 应用正常启动和退出

View File

@@ -1,13 +0,0 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import * as schema from '@/db/schema'
import { env } from '@/env'
export function createDb() {
return drizzle({
connection: {
url: env.DATABASE_URL,
prepare: true,
},
schema,
})
}

View File

@@ -1,15 +0,0 @@
import { sql } from 'drizzle-orm'
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
export const todoTable = pgTable('todo', {
id: uuid('id').primaryKey().default(sql`uuidv7()`),
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date()),
})

View File

@@ -1,7 +0,0 @@
import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
export const devtools = {
name: 'TanStack Query',
render: <ReactQueryDevtoolsPanel />,
} satisfies TanStackDevtoolsReactPlugin

View File

@@ -1 +0,0 @@
export * from './devtools'

View File

@@ -1,7 +0,0 @@
import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
export const devtools = {
name: 'TanStack Router',
render: <TanStackRouterDevtoolsPanel />,
} satisfies TanStackDevtoolsReactPlugin

View File

@@ -1 +0,0 @@
export * from './devtools'

View File

View File

@@ -1,53 +0,0 @@
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { createRouterClient } from '@orpc/server'
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { router } from './router'
import type { RouterClient } from './types'
const getORPCClient = createIsomorphicFn()
.server(() =>
createRouterClient(router, {
context: () => ({
headers: getRequestHeaders(),
}),
}),
)
.client(() => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
})
return createORPCClient<RouterClient>(link)
})
const client: RouterClient = getORPCClient()
export const orpc = createTanstackQueryUtils(client, {
experimental_defaults: {
todo: {
create: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
update: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
remove: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
},
},
})

View File

@@ -1,5 +0,0 @@
import * as todo from './contracts/todo'
export const contract = {
todo,
}

View File

@@ -1,2 +0,0 @@
export { orpc } from './client'
export * from './types'

View File

@@ -1,17 +0,0 @@
import { createDb } from '@/db'
import { os } from '@/orpc/server'
export type Database = ReturnType<typeof createDb>
const globalForDb = globalThis as { __db?: Database }
function getDb(): Database {
globalForDb.__db ??= createDb()
return globalForDb.__db
}
export const dbProvider = os.middleware(async ({ context, next }) => {
return next({
context: { ...context, db: getDb() },
})
})

View File

@@ -1 +0,0 @@
export * from './db'

View File

@@ -1,4 +0,0 @@
import { dbProvider } from './middlewares/db'
import { os } from './server'
export const baseProcedure = os.use(dbProvider)

View File

@@ -1,6 +0,0 @@
import * as todo from './handlers/todo'
import { os } from './server'
export const router = os.router({
todo,
})

View File

@@ -1,8 +0,0 @@
import { implement } from '@orpc/server'
import { contract } from './contract'
export type ORPCContext = {
headers?: Headers
}
export const os = implement(contract).$context<ORPCContext>()

View File

@@ -1,34 +0,0 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -1,31 +1,31 @@
{ {
"$schema": "./node_modules/turbo/schema.json", "$schema": "./node_modules/turbo/schema.json",
"dangerouslyDisablePackageManagerCheck": true, "dangerouslyDisablePackageManagerCheck": true,
"globalDependencies": [
"package.json",
"bun.lock",
"turbo.json",
"biome.json"
],
"tasks": { "tasks": {
"build:compile": { "build": {
"dependsOn": ["build:vite"], "outputs": ["dist/**", ".output/**"]
"outputs": ["out/**", "src-tauri/binaries/**"]
}, },
"build:tauri": { "compile": {
"dependsOn": ["build:compile"], "dependsOn": ["build"],
"outputs": ["src-tauri/target/release/bundle/**"] "outputs": ["out/**"]
},
"build:vite": {
"outputs": [".output/**"]
}, },
"dev": { "dev": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"dev:tauri": { "fix": {
"cache": false, "outputs": []
"dependsOn": ["build:compile"],
"persistent": true,
"with": ["dev:vite"]
}, },
"dev:vite": { "typecheck": {
"cache": false, "dependsOn": ["^typecheck"],
"persistent": true "inputs": ["tsconfig.json", "tsconfig.*.json", "**/*.{ts,tsx}"],
"outputs": []
} }
}, },
"ui": "tui" "ui": "tui"