From 493b31cff0c0bded4f0ec61622ccd62744773b81 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Sun, 9 Aug 2026 13:31:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=B0=8F=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E4=B8=8E=E5=B0=8F=E7=A8=8B=E5=BA=8F=E9=A1=B9=E7=9B=AE=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tasks/20260809-project-types-impl-3f7a.md | 71 + README.md | 2 +- electron/api/routes/opencode.ts | 16 +- electron/opencode/project-config.ts | 24 +- .../project-directory-initialization.ts | 7 +- electron/opencode/project-template.ts | 139 ++ .../project-templates/vite/package-lock.json | 1164 +++++++++++++++++ .../project-templates/vite/package.json | 15 + electron/services/project-packager.ts | 47 +- shared/project-config.ts | 14 +- src/components/layout/Sidebar.tsx | 45 + src/components/works/ProjectPublishAction.tsx | 6 +- src/lib/works-project-publish.ts | 9 +- src/pages/ProjectConfiguration/index.tsx | 12 +- src/stores/opencode.ts | 3 + tests/e2e/project-superpowers-toggle.spec.ts | 6 + tests/unit/opencode-routes.test.ts | 47 + tests/unit/opencode-store.test.ts | 4 + tests/unit/project-config.test.ts | 28 + .../unit/project-configuration-page.test.tsx | 25 +- .../project-directory-initialization.test.ts | 93 ++ tests/unit/project-packager.test.ts | 47 +- tests/unit/project-publish-action.test.tsx | 19 +- tests/unit/sidebar-opencode-projects.test.tsx | 13 +- tests/unit/works-project-publish.test.ts | 3 +- tests/unit/works-routes.test.ts | 7 + 26 files changed, 1827 insertions(+), 39 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260809-project-types-impl-3f7a.md create mode 100644 electron/opencode/project-template.ts create mode 100644 electron/opencode/project-templates/vite/package-lock.json create mode 100644 electron/opencode/project-templates/vite/package.json diff --git a/.project-docs/30-worklog/tasks/20260809-project-types-impl-3f7a.md b/.project-docs/30-worklog/tasks/20260809-project-types-impl-3f7a.md new file mode 100644 index 0000000..2fb9a9f --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260809-project-types-impl-3f7a.md @@ -0,0 +1,71 @@ +# Task: 实现小游戏小程序和自定义项目类型 + +## Identity + +- Task ID: 20260809-project-types-impl-3f7a +- Mode: Feature +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: a6fe14a8d60ae5bfd83529b36bead34b8b8c3348 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- 为 AI 编程项目引入创建时选择且不可变的 `ProjectType`:`mini_game`、`mini_program`、`custom`。 +- 在核心配置、目录初始化和安全打包层贯通项目类型;为小游戏和小程序生成可真实 `npm ci`、Vite build 的固定版本模板。 +- 旧配置缺少类型时归一为 `custom`;自定义项目拒绝进入受控发布打包。 +- 更新配置、初始化和打包聚焦测试;Renderer、Host API 路由和 README 由同任务根代理负责。 + +## Intent And Constraints + +- `ProjectType` 是不可变产品类型,`BuildPreset` 是平台内部构建实现,两者不得混用;第一期两个可发布类型都使用受控 Vite preset。 +- 直接复用已有项目时,显式请求的类型必须与已有配置一致;未传类型的旧 API 调用保留既有复用行为。 +- 模板在 staging 目录完整生成后沿用现有排他复制与回滚机制,不覆盖用户文件。 +- `custom` 和旧配置不得被打包为可发布作品;安全扫描、敏感文件排除、符号链接和包体限制不得削弱。 +- 只修改任务记录,不写 canonical `.project-docs` 文件;不提交代码。 + +## Plan + +1. 固定配置类型、旧配置归一和保存/复用不可变规则,并用单元测试锁定。 +2. 增加两个受控 Vite 项目模板和真实 npm 锁文件,接入原子目录初始化。 +3. 按项目类型生成带 `project_type` 的发布清单,并拒绝 `custom`。 +4. 运行聚焦测试、模板 `npm ci`/build、typecheck、scoped lint、build 和文档漂移检查。 + +## Outcome + +- 新增不可变 `ProjectType` 契约:`mini_game`、`mini_program`、`custom`。新配置明确保存类型;旧配置仅在字段缺失时归一为 `custom`,显式未知值会判为无效配置。 +- 项目配置保存不能改变已有类型;直接复用所选文件夹时,显式类型必须与已有配置一致,未传类型的旧 API 调用仍可按原行为复用。 +- 小游戏和小程序都在 staging 中生成固定 Vite 7.3.1、真实 npm v3 锁文件、相对资源构建配置和可运行示例。小游戏包含 `game.json`/`game.js` 与 Canvas 示例;小程序包含 `app.json`/`app.js`/`pages/index` 与交互首页。自定义项目保持原最小结构。 +- 模板继续使用排他复制和失败回滚;目标文件冲突时不会在用户文件夹留下 `.niancode` 或其他半成品。 +- 安全打包先读取项目类型:`custom` 返回 `PROJECT_TYPE_UNPUBLISHABLE`;两种可发布类型继续使用内部受控 Vite preset,并在生成的 `niancode.yml` 顶层写入 `project_type`。既有敏感文件排除、符号链接、稳定读取和包体限制未放宽。 +- 同任务根代理已贯通创建对话框、Renderer store、Host API、发布入口/分类和 README;本记录仅描述已观察到的共享任务结果,不改 canonical 项目文档。 +- 新建弹窗默认选择“小游戏”,同时提供“小程序”和“自定义项目”两种明确选项;自定义项目的配置页不显示“一键提交审核”,而是说明需新建小游戏或小程序项目。 +- 一键提交的作品分类随项目类型发送;模板文件缺失时不再要求非专业用户手工补三个文件,而是提示让开发助手修复项目模板。 + +## Verification + +- `pnpm exec vitest run tests/unit/project-config.test.ts tests/unit/project-directory-initialization.test.ts tests/unit/project-packager.test.ts`:3 files / 24 tests passed。 +- `pnpm exec vitest run tests/unit/works-routes.test.ts`:1 file / 27 tests passed;可发布路由 fixture 已明确写入 `mini_game` 配置,避免旧的隐式“任意 Vite 项目都可发布”前提。 +- 使用真实 `initializeProjectDirectory` 生成临时 `mini_game` 与 `mini_program` 项目后,分别执行 `npm ci --ignore-scripts --no-audit --no-fund` 和 `npm run build`:两者均安装 14 个锁定依赖并由 Vite 7.3.1 构建成功;验证结束已清理临时项目与临时测试工具。 +- `pnpm run typecheck`:通过。 +- 对核心配置、初始化、模板、打包及三份聚焦测试执行 scoped ESLint:通过。 +- `pnpm run build:vite`:Renderer、Electron Main、Preload 均构建成功;仅有仓库既有 chunk/dynamic-import 提示。 +- `git diff --check`:通过;仅有 Git 的 LF/CRLF 工作区提示。 +- `pnpm exec vitest run tests/unit/project-config.test.ts tests/unit/project-directory-initialization.test.ts tests/unit/project-packager.test.ts tests/unit/opencode-routes.test.ts tests/unit/opencode-store.test.ts tests/unit/sidebar-opencode-projects.test.tsx tests/unit/project-configuration-page.test.tsx tests/unit/project-publish-action.test.tsx tests/unit/works-project-publish.test.ts tests/unit/works-routes.test.ts`:10 files / 310 tests passed。 +- `pnpm exec playwright test tests/e2e/project-superpowers-toggle.spec.ts --reporter=line`:1 passed;真实 Electron 新建弹窗验证三种项目类型、默认小游戏及小游戏/小程序切换。 +- `pnpm test` 的全仓尝试结果为 1471 passed / 20 failed:14 项因本机缺少 `zip` 可执行文件、1 项因仓库缺少 `.opencode/agent` 夹具;其余为未修改的图片认证/项目进度同步用例和 1 项时序用例(该时序用例单独重跑通过)。这些失败不位于本任务变更链路,已保留为基线信息,未扩张本任务去修改无关模块。 + +## Follow-ups + +- Works Square 服务端已在兄弟任务 `20260809-project-types-server-8d2c` 接受并校验顶层 `project_type`,并继续映射到内部受控 Vite preset;两仓变更需成组集成后才能进行真实发布验收。 +- 完成服务端类型分流后,使用真实账号执行“小程序/小游戏创建 → 一键提交 → 构建 → 运营审核 → 发布”的生产整链验收。 + +## Promotion Candidates + +- Target: `.project-docs/40-domain/business-rules.md` 与 README 当前产品状态。 + - Proposal: 记录 `ProjectType` 是创建时选择且不可变的产品类型;小游戏和小程序创建即具备平台受控发布骨架,自定义及旧项目默认不可一键发布。 + - Evidence: 本任务配置/初始化/打包实现,24 项核心回归,两类真实 `npm ci` + Vite build,以及同任务 Renderer/Host API 回归。 + - Future impact: 后续不得把 `custom` 当成构建 preset,也不得通过任意 Shell 命令绕过平台受控构建边界。 + - Semantic conflicts: 需要服务端同步接受 `project_type`;与现有 Main-owned 安全打包和单入口发布边界一致。 + - Human confirmation required: 是;用户已确认三种创建类型和“参照微信、自有平台实现”的方向,canonical promotion 仍由 Integration Gate 执行。 diff --git a/README.md b/README.md index d71181f..e166572 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Makelore 是一个面向软件与视觉创作的 AI 桌面工作台。当前版 - `Makelore Canvas|AI 绘画`:以设计项目(Workspace)组织 Agent 对话、方向确认、图片/视频生成任务和私有结果。 - `Makelore Learning|AI 学习`:数学与知识宇宙主题的学习入口,当前暂未开通;开屏页和模块切换菜单保留置灰入口,不能进入。 -作品广场、素材广场、独立发布上传和云部署页面不属于 Makelore 2.0 工作台。AI 编程项目只在现有项目配置底部提供“一键提交审核”:Main 自动预检、安全打包并提交,构建通过后进入运营审核,审核通过即直接发布。项目成果预览 `/deliverables` 继续保留。 +作品广场、素材广场、独立发布上传和云部署页面不属于 Makelore 2.0 工作台。新建项目可选择“小游戏”“小程序”或“自定义项目”:小游戏和小程序会创建完整的平台发布模板,项目配置底部提供“一键提交审核”;Main 自动预检、安全打包并提交,构建通过后进入运营审核,审核通过即直接发布。自定义项目只创建工作空间,不配置默认发布方式。项目成果预览 `/deliverables` 继续保留。 ## 当前产品状态 diff --git a/electron/api/routes/opencode.ts b/electron/api/routes/opencode.ts index 7561915..ae90f7e 100644 --- a/electron/api/routes/opencode.ts +++ b/electron/api/routes/opencode.ts @@ -48,7 +48,11 @@ import { upsertProjectSessionMetadata, type ProjectConversationState, } from '../../../shared/project-conversations'; -import type { ProjectConfig } from '../../../shared/project-config'; +import { + isProjectType, + type ProjectConfig, + type ProjectType, +} from '../../../shared/project-config'; import { initializeProjectDirectory } from '../../opencode/project-directory-initialization'; const bundledCourseSkillIds = new Set(BUNDLED_COURSE_SKILL_IDS); @@ -82,6 +86,13 @@ const COMMAND_IMAGE_DATA_URL_PATTERN = /^data:(image\/[A-Za-z0-9.+-]+);base64,([A-Za-z0-9+/]*={0,2})$/iu; const STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u; +function parseOptionalProjectType(value: unknown): ProjectType | undefined { + if (value === undefined) return undefined; + if (!isProjectType(value)) { + throw new Error('Invalid project type'); + } + return value; +} function asRecord(value: unknown): Record | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; @@ -956,8 +967,10 @@ export async function handleOpencodeRoutes( projectPath?: string; parentPath?: string; projectName?: string; + projectType?: unknown; defaultModel?: unknown; }>(req); + const projectType = parseOptionalProjectType(body.projectType); const defaultModel = body.defaultModel === undefined || body.defaultModel === null ? null : typeof body.defaultModel === 'string' && body.defaultModel.trim() @@ -972,6 +985,7 @@ export async function handleOpencodeRoutes( : buildNewProjectPath(body.parentPath, body.projectName); const { config } = await initializeProjectDirectory({ projectPath, + projectType, defaultModel, allowExistingDirectory: useSelectedDirectory, }); diff --git a/electron/opencode/project-config.ts b/electron/opencode/project-config.ts index 196c47c..ef0c8dd 100644 --- a/electron/opencode/project-config.ts +++ b/electron/opencode/project-config.ts @@ -2,11 +2,14 @@ import path from 'node:path'; import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import { createProjectConfig, + isProjectType, validateAgentConfigs, validateAgentNames, type ProjectAgentConfig, type ProjectConfig, + type ProjectType, } from '../../shared/project-config'; +import { createPublishableProjectTemplate } from './project-template'; export const PROJECT_CONFIG_PATH = '.niancode/project.json'; @@ -22,6 +25,12 @@ function normalizeStringList(value: unknown): string[] { .filter(Boolean))]; } +function normalizeProjectType(value: unknown): ProjectType { + if (value === undefined) return 'custom'; + if (isProjectType(value)) return value; + throw new Error('Invalid project type'); +} + function normalizeAgent(value: unknown): ProjectAgentConfig | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const raw = value as Partial; @@ -86,6 +95,7 @@ export function normalizeProjectConfig(value: unknown): ProjectConfig { } return { schemaVersion: 1, + projectType: normalizeProjectType(raw.projectType), initialized, superpowersEnabled: raw.superpowersEnabled === true, defaultModel: typeof raw.defaultModel === 'string' && raw.defaultModel.trim() @@ -231,9 +241,9 @@ Last Updated By: user export async function createInitialProjectConfig( projectPath: string, - options?: { defaultModel?: string | null }, + options?: { defaultModel?: string | null; projectType?: ProjectType }, ): Promise { - const config = createProjectConfig(); + const config = createProjectConfig(new Date().toISOString(), options?.projectType ?? 'custom'); if (typeof options?.defaultModel === 'string' && options.defaultModel.trim()) { config.defaultModel = options.defaultModel.trim(); } @@ -242,15 +252,25 @@ export async function createInitialProjectConfig( await writeFile(configPath(projectPath), `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' }); await writeFile(path.join(projectPath, 'VERSION.md'), initialVersionDocument(config.createdAt), { encoding: 'utf8', flag: 'wx' }); await writeFile(path.join(projectPath, 'TASKS.md'), initialTaskDocument(), { encoding: 'utf8', flag: 'wx' }); + if (config.projectType !== 'custom') { + await createPublishableProjectTemplate(projectPath, config.projectType); + } return config; } export async function writeProjectConfig(projectPath: string, value: unknown): Promise { const previous = await readProjectConfig(projectPath); if (previous.status !== 'valid') throw new Error('Project configuration is missing or invalid'); + const requestedProjectType = value && typeof value === 'object' && !Array.isArray(value) + ? (value as { projectType?: unknown }).projectType + : undefined; + if (requestedProjectType !== undefined && requestedProjectType !== previous.config.projectType) { + throw new Error('已有项目类型不可更改'); + } const config = normalizeProjectConfig({ ...(value as object), schemaVersion: 1, + projectType: previous.config.projectType, createdAt: previous.config.createdAt, updatedAt: new Date().toISOString(), }); diff --git a/electron/opencode/project-directory-initialization.ts b/electron/opencode/project-directory-initialization.ts index 049c49a..5fb19ee 100644 --- a/electron/opencode/project-directory-initialization.ts +++ b/electron/opencode/project-directory-initialization.ts @@ -11,7 +11,7 @@ import { } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, sep } from 'node:path'; -import type { ProjectConfig } from '../../shared/project-config'; +import type { ProjectConfig, ProjectType } from '../../shared/project-config'; import { createInitialProjectConfig, readProjectConfig } from './project-config'; type StagedProjectEntries = { @@ -27,6 +27,7 @@ export type ProjectDirectoryInitializationResult = { export type ProjectDirectoryInitializationInput = { projectPath: string; defaultModel?: string | null; + projectType?: ProjectType; allowExistingDirectory: boolean; }; @@ -210,6 +211,9 @@ export async function initializeProjectDirectory( await requireExistingDirectory(input.projectPath); const existing = await readProjectConfig(input.projectPath); if (existing.status === 'valid') { + if (input.projectType !== undefined && input.projectType !== existing.config.projectType) { + throw new Error('已有项目类型不可更改'); + } return { config: existing.config, reusedExistingConfig: true }; } if (existing.status === 'invalid') { @@ -228,6 +232,7 @@ export async function initializeProjectDirectory( try { config = await createInitialProjectConfig(stagingPath, { defaultModel: input.defaultModel, + projectType: input.projectType, }); } catch (error) { throw new Error( diff --git a/electron/opencode/project-template.ts b/electron/opencode/project-template.ts new file mode 100644 index 0000000..0473835 --- /dev/null +++ b/electron/opencode/project-template.ts @@ -0,0 +1,139 @@ +import { dirname, join } from 'node:path'; +import { mkdir, writeFile } from 'node:fs/promises'; +import type { ProjectType } from '../../shared/project-config'; +import vitePackageJson from './project-templates/vite/package.json'; +import vitePackageLock from './project-templates/vite/package-lock.json'; + +const VITE_CONFIG = `import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', +}); +`; + +const MINI_GAME_FILES: Record = { + 'index.html': ` + + + + + Makelore 小游戏 + + + + + + + +`, + 'game.json': `${JSON.stringify({ deviceOrientation: 'portrait', showStatusBar: false }, null, 2)}\n`, + 'game.js': `const canvas = document.querySelector('#game'); +const context = canvas.getContext('2d'); + +function render() { + const ratio = window.devicePixelRatio || 1; + const width = window.innerWidth; + const height = window.innerHeight; + canvas.width = Math.round(width * ratio); + canvas.height = Math.round(height * ratio); + context.setTransform(ratio, 0, 0, ratio, 0, 0); + + const gradient = context.createLinearGradient(0, 0, 0, height); + gradient.addColorStop(0, '#10233d'); + gradient.addColorStop(1, '#3a5578'); + context.fillStyle = gradient; + context.fillRect(0, 0, width, height); + + context.textAlign = 'center'; + context.fillStyle = '#ffffff'; + context.font = '700 28px system-ui, sans-serif'; + context.fillText('Makelore 小游戏', width / 2, height / 2 - 10); + context.fillStyle = '#f6b45f'; + context.font = '16px system-ui, sans-serif'; + context.fillText('点击画布,开始创造', width / 2, height / 2 + 26); +} + +canvas.addEventListener('pointerdown', (event) => { + context.beginPath(); + context.fillStyle = '#f26a3d'; + context.arc(event.clientX, event.clientY, 18, 0, Math.PI * 2); + context.fill(); +}); +window.addEventListener('resize', render); +render(); +`, +}; + +const MINI_PROGRAM_FILES: Record = { + 'index.html': ` + + + + + Makelore 小程序 + + +
+ + + +`, + 'app.json': `${JSON.stringify({ pages: ['pages/index/index'], window: { navigationBarTitleText: 'Makelore 小程序' } }, null, 2)}\n`, + 'app.js': `export const app = { + globalData: { + platform: 'makelore', + }, +}; +`, + 'pages/index/index.json': `${JSON.stringify({ navigationBarTitleText: '首页' }, null, 2)}\n`, + 'pages/index/index.css': `:root { + color: #24364b; + background: #f4f7fb; + font-family: system-ui, sans-serif; +} + +body { margin: 0; } +.page { min-height: 100vh; display: grid; place-items: center; padding: 24px; } +.card { width: min(420px, 100%); border: 1px solid #d8e1ec; border-radius: 20px; background: #fff; padding: 28px; text-align: center; box-shadow: 0 16px 48px rgb(36 54 75 / 10%); } +button { border: 0; border-radius: 12px; background: #3a5578; color: #fff; padding: 12px 18px; font: inherit; font-weight: 700; cursor: pointer; } +`, + 'pages/index/index.js': `import { app } from '../../app.js'; +import './index.css'; + +const root = document.querySelector('#app'); +let count = 0; +root.innerHTML = \`

运行于 \${app.globalData.platform}

Makelore 小程序

已点击 0 次

\`; +root.querySelector('button').addEventListener('click', () => { + count += 1; + root.querySelector('#count').textContent = \`已点击 \${count} 次\`; +}); +`, +}; + +function serializeJson(value: unknown): string { + return `${JSON.stringify(value, null, 2)}\n`; +} + +export async function createPublishableProjectTemplate( + projectPath: string, + projectType: Exclude, +): Promise { + const files = projectType === 'mini_game' ? MINI_GAME_FILES : MINI_PROGRAM_FILES; + const templateFiles: Record = { + 'package.json': serializeJson(vitePackageJson), + 'package-lock.json': serializeJson(vitePackageLock), + 'vite.config.js': VITE_CONFIG, + ...files, + }; + + for (const [relativePath, content] of Object.entries(templateFiles)) { + const filePath = join(projectPath, relativePath); + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, content, { encoding: 'utf8', flag: 'wx' }); + } +} diff --git a/electron/opencode/project-templates/vite/package-lock.json b/electron/opencode/project-templates/vite/package-lock.json new file mode 100644 index 0000000..fbe4b83 --- /dev/null +++ b/electron/opencode/project-templates/vite/package-lock.json @@ -0,0 +1,1164 @@ +{ + "name": "makelore-project", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "makelore-project", + "version": "0.1.0", + "devDependencies": { + "vite": "7.3.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/electron/opencode/project-templates/vite/package.json b/electron/opencode/project-templates/vite/package.json new file mode 100644 index 0000000..11e793e --- /dev/null +++ b/electron/opencode/project-templates/vite/package.json @@ -0,0 +1,15 @@ +{ + "name": "makelore-project", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@10.9.2", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "devDependencies": { + "vite": "7.3.1" + } +} diff --git a/electron/services/project-packager.ts b/electron/services/project-packager.ts index 320da91..a45ee70 100644 --- a/electron/services/project-packager.ts +++ b/electron/services/project-packager.ts @@ -10,19 +10,26 @@ import { writeFile, } from 'node:fs/promises'; import { dirname, isAbsolute, join, relative, sep } from 'node:path'; +import type { ProjectType } from '../../shared/project-config'; +import { readProjectConfig } from '../opencode/project-config'; const require = createRequire(import.meta.url); const AdmZip = require('adm-zip') as typeof import('adm-zip'); -const GENERATED_MANIFEST = ( - 'schema_version: 1\n' - + 'kind: web\n' - + 'runtime: static\n' - + 'build:\n' - + ' preset: vite\n' - + ' package_manager: npm\n' - + ' entry: index.html\n' -); +type PublishableProjectType = Exclude; + +function generatedManifest(projectType: PublishableProjectType): string { + return ( + 'schema_version: 1\n' + + `project_type: ${projectType}\n` + + 'kind: web\n' + + 'runtime: static\n' + + 'build:\n' + + ' preset: vite\n' + + ' package_manager: npm\n' + + ' entry: index.html\n' + ); +} const FIXED_ZIP_TIME = new Date(1980, 0, 1, 0, 0, 0, 0); const FILE_MODE = 0o100644; const DEFAULT_LIMITS: ProjectPackageLimits = { @@ -125,6 +132,7 @@ export type StaticProjectPackageSummary = { excludedPaths: string[]; manifest: { schema_version: 1; + project_type: PublishableProjectType; kind: 'web'; runtime: 'static'; build: { @@ -177,9 +185,10 @@ export async function createStaticProjectPackage(input: { }): Promise { const limits = input.limits ?? DEFAULT_LIMITS; const projectPath = await resolveProjectDirectory(input.projectPath); + const projectType = await readPublishableProjectType(projectPath); await validateViteProject(projectPath, limits); const scan = await scanProject(projectPath, limits); - const manifestBytes = Buffer.from(GENERATED_MANIFEST, 'utf8'); + const manifestBytes = Buffer.from(generatedManifest(projectType), 'utf8'); const sourceBytes = scan.sourceBytes + manifestBytes.length; if (sourceBytes > limits.maxSourceBytes) { throw new ProjectPackageError( @@ -228,6 +237,7 @@ export async function createStaticProjectPackage(input: { excludedPaths: scan.excludedPaths, manifest: { schema_version: 1, + project_type: projectType, kind: 'web', runtime: 'static', build: { @@ -239,6 +249,23 @@ export async function createStaticProjectPackage(input: { }; } +async function readPublishableProjectType(projectPath: string): Promise { + const result = await readProjectConfig(projectPath); + if (result.status === 'invalid') { + throw new ProjectPackageError( + 'PROJECT_CONFIG_INVALID', + '项目配置无效,无法确认发布类型', + ); + } + if (result.status === 'missing' || result.config.projectType === 'custom') { + throw new ProjectPackageError( + 'PROJECT_TYPE_UNPUBLISHABLE', + '自定义项目暂未配置发布方式,请新建小游戏或小程序项目', + ); + } + return result.config.projectType; +} + async function resolveProjectDirectory(projectPath: string): Promise { try { const resolved = await realpath(projectPath); diff --git a/shared/project-config.ts b/shared/project-config.ts index 45bc3f7..fa054f8 100644 --- a/shared/project-config.ts +++ b/shared/project-config.ts @@ -1,6 +1,13 @@ export const PRODUCT_OVERVIEW_FILENAME = 'PRODUCT_OVERVIEW.md'; export const LEGACY_PROMOTION_PLAN_FILENAME = 'PROMOTION_PLAN.md'; +export const PROJECT_TYPES = ['mini_game', 'mini_program', 'custom'] as const; +export type ProjectType = (typeof PROJECT_TYPES)[number]; + +export function isProjectType(value: unknown): value is ProjectType { + return typeof value === 'string' && PROJECT_TYPES.includes(value as ProjectType); +} + export type ProjectAgentResponsibility = { mission: string; owns: string[]; @@ -28,6 +35,7 @@ export type ProjectAgentConfig = { export type ProjectConfig = { schemaVersion: 1; + projectType: ProjectType; initialized: boolean; superpowersEnabled: boolean; defaultModel: string | null; @@ -37,9 +45,13 @@ export type ProjectConfig = { updatedAt: string; }; -export function createProjectConfig(now = new Date().toISOString()): ProjectConfig { +export function createProjectConfig( + now = new Date().toISOString(), + projectType: ProjectType = 'custom', +): ProjectConfig { return { schemaVersion: 1, + projectType, initialized: false, superpowersEnabled: false, defaultModel: null, diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index f1ae36d..fe44bc8 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -39,6 +39,7 @@ import type { OpencodeProject } from '@/types/opencode'; import { buildConfiguredModelOptions } from '@/lib/model-options'; import { useProjectConfigStore } from '@/stores/project-config'; import { EMPTY_AGENT_USER_PROFILE, getProfileAccountKey, isUserProfileComplete, useUserProfileStore, type UserGender } from '@/stores/user-profile'; +import type { ProjectType } from '../../../shared/project-config'; import { toast } from 'sonner'; type OpenDialogResult = { @@ -162,6 +163,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba const [newProjectName, setNewProjectName] = useState(''); const [newProjectSelectedPath, setNewProjectSelectedPath] = useState(''); const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState('use-selected-directory'); + const [newProjectType, setNewProjectType] = useState('mini_game'); const [newProjectDefaultModel, setNewProjectDefaultModel] = useState(''); const [createProjectError, setCreateProjectError] = useState(null); const [creatingProject, setCreatingProject] = useState(false); @@ -391,6 +393,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba setNewProjectName(''); setNewProjectSelectedPath(''); setNewProjectDirectoryMode('use-selected-directory'); + setNewProjectType('mini_game'); setNewProjectDefaultModel(''); setCreateProjectError(null); setCreateDialogOpen(true); @@ -399,6 +402,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba const closeCreateProject = () => { if (creatingProject) return; setCreateDialogOpen(false); + setNewProjectType('mini_game'); setNewProjectDefaultModel(''); setCreateProjectError(null); }; @@ -460,6 +464,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba const project = await createProject({ directoryMode: newProjectDirectoryMode, selectedPath, + projectType: newProjectType, ...(newProjectDirectoryMode === 'create-child-directory' ? { projectName } : {}), ...(newProjectDefaultModel ? { defaultModel: newProjectDefaultModel } : {}), }); @@ -666,6 +671,46 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba void confirmCreateProject(); }} > +
+ 项目类型 +
+ {([ + ['mini_game', '小游戏', '适合实时互动、Canvas 和关卡玩法;创建后可直接提交审核。'], + ['mini_program', '小程序', '适合页面、表单和轻量工具;创建后可直接提交审核。'], + ['custom', '自定义项目', '只创建项目空间,不配置默认发布方式。'], + ] as const).map(([value, title, description]) => ( + + ))} +
+
+
项目目录方式
diff --git a/src/components/works/ProjectPublishAction.tsx b/src/components/works/ProjectPublishAction.tsx index 886cdcc..4b19d9c 100644 --- a/src/components/works/ProjectPublishAction.tsx +++ b/src/components/works/ProjectPublishAction.tsx @@ -12,6 +12,7 @@ import { type WorksProjectMetadataInput, } from '@/lib/works-square'; import type { OpencodeProject } from '@/types/opencode'; +import type { ProjectType } from '../../../shared/project-config'; type PublishPhase = 'idle' | 'submitting' | 'polling' | 'succeeded' | 'failed' | 'uncertain'; @@ -23,6 +24,7 @@ type BuildVersion = { type ProjectPublishActionProps = { project: OpencodeProject; + projectType: Exclude; }; const BUILD_POLL_INTERVAL_MS = 2_000; @@ -63,7 +65,7 @@ function buttonLabel(phase: PublishPhase): string { return '一键提交审核'; } -export function ProjectPublishAction({ project }: ProjectPublishActionProps) { +export function ProjectPublishAction({ project, projectType }: ProjectPublishActionProps) { const [phase, setPhase] = useState('idle'); const [failure, setFailure] = useState(null); const pollGeneration = useRef(0); @@ -79,7 +81,7 @@ export function ProjectPublishAction({ project }: ProjectPublishActionProps) { title, summary: `${title},由 Makelore 创建的作品。`, cover_url: null, - category: 'web', + category: projectType, age_band: null, difficulty: null, }; diff --git a/src/lib/works-project-publish.ts b/src/lib/works-project-publish.ts index 5c94e3f..7183a65 100644 --- a/src/lib/works-project-publish.ts +++ b/src/lib/works-project-publish.ts @@ -19,8 +19,13 @@ const PUBLISH_FAILURES: Record = { }, PROJECT_FILE_MISSING: { title: '项目文件不完整', - reason: '发布所需的基础文件缺失。', - nextStep: '请让开发助手补齐 package.json、package-lock.json 和 index.html 后重试。', + reason: '项目模板中的发布文件被删除或损坏。', + nextStep: '请让开发助手修复当前项目模板后重试。', + }, + PROJECT_TYPE_UNPUBLISHABLE: { + title: '这个项目没有配置发布方式', + reason: '自定义项目只创建工作空间,不会自动生成平台发布模板。', + nextStep: '如需一键提交,请新建小游戏或小程序项目。', }, PROJECT_FILE_INVALID: { title: '项目配置无法读取', diff --git a/src/pages/ProjectConfiguration/index.tsx b/src/pages/ProjectConfiguration/index.tsx index b757af1..4feb48d 100644 --- a/src/pages/ProjectConfiguration/index.tsx +++ b/src/pages/ProjectConfiguration/index.tsx @@ -324,7 +324,17 @@ export function ProjectConfiguration() {
- + {draft.projectType === 'mini_game' || draft.projectType === 'mini_program' ? ( + + ) : ( +

+ 自定义项目暂未配置发布方式。如需一键提交,请新建小游戏或小程序项目。 +

+ )}
diff --git a/src/stores/opencode.ts b/src/stores/opencode.ts index ac9fede..62f17bd 100644 --- a/src/stores/opencode.ts +++ b/src/stores/opencode.ts @@ -60,6 +60,7 @@ import { getOpencodeErrorKind, type OpencodeErrorKind, } from '../../shared/opencode-error-kind'; +import type { ProjectType } from '../../shared/project-config'; export type { OpencodeErrorKind } from '../../shared/opencode-error-kind'; @@ -80,6 +81,7 @@ export type ProjectDirectoryMode = 'use-selected-directory' | 'create-child-dire export interface ProjectCreationInput { directoryMode: ProjectDirectoryMode; selectedPath: string; + projectType: ProjectType; projectName?: string; defaultModel?: string | null; } @@ -3956,6 +3958,7 @@ export const useOpencodeStore = create((set, get) => ({ ...(input.directoryMode === 'use-selected-directory' ? { projectPath: input.selectedPath } : { parentPath: input.selectedPath, projectName: input.projectName }), + projectType: input.projectType, ...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel } : {}), }), }); diff --git a/tests/e2e/project-superpowers-toggle.spec.ts b/tests/e2e/project-superpowers-toggle.spec.ts index 094a4d7..8d337bd 100644 --- a/tests/e2e/project-superpowers-toggle.spec.ts +++ b/tests/e2e/project-superpowers-toggle.spec.ts @@ -16,6 +16,12 @@ test.describe('Project-level Superpowers setting', () => { const page = await getStableWindow(app); await page.getByTestId('sidebar-create-project').click(); + await expect(page.getByRole('radio', { name: '小游戏' })).toBeChecked(); + await expect(page.getByRole('radio', { name: '小程序' })).not.toBeChecked(); + await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked(); + await page.getByRole('radio', { name: '小程序' }).click(); + await expect(page.getByRole('radio', { name: '小程序' })).toBeChecked(); + await page.getByRole('radio', { name: '小游戏' }).click(); await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked(); await page.getByRole('button', { name: '选择路径' }).click(); await expect(page.getByLabel('项目路径')).toHaveValue(parentPath); diff --git a/tests/unit/opencode-routes.test.ts b/tests/unit/opencode-routes.test.ts index f476a8b..9a3d3cc 100644 --- a/tests/unit/opencode-routes.test.ts +++ b/tests/unit/opencode-routes.test.ts @@ -515,6 +515,53 @@ describe('opencode host api routes', () => { } }); + it.each([ + ['mini_game', ['game.json', 'game.js']], + ['mini_program', ['app.json', 'pages/index/index.js']], + ] as const)('creates a complete %s project template', async (projectType, expectedTypeFiles) => { + const parentPath = await mkdtemp(join(tmpdir(), `niancode-create-${projectType}-`)); + try { + const response = createResponse(); + const expectedProjectPath = join(parentPath, '新作品'); + const project = { + id: `prj_${projectType}`, + path: expectedProjectPath, + name: '新作品', + }; + const rememberProject = vi.fn(async () => project); + + const handled = await handleOpencodeRoutes( + createRequest('POST', { parentPath, projectName: '新作品', projectType }), + response.res, + new URL('http://127.0.0.1/api/opencode/projects/create'), + { + opencodeProjectStore: { + rememberProject, + listProjects: vi.fn(async () => [project]), + getActiveProject: vi.fn(async () => null), + }, + } as never, + ); + + expect(handled).toBe(true); + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + success: true, + config: { projectType }, + }); + const config = JSON.parse(await readFile(join(expectedProjectPath, '.niancode', 'project.json'), 'utf8')) as Record; + expect(config.projectType).toBe(projectType); + await expect(stat(join(expectedProjectPath, 'package.json'))).resolves.toBeTruthy(); + await expect(stat(join(expectedProjectPath, 'package-lock.json'))).resolves.toBeTruthy(); + await expect(stat(join(expectedProjectPath, 'index.html'))).resolves.toBeTruthy(); + for (const relativePath of expectedTypeFiles) { + await expect(stat(join(expectedProjectPath, relativePath))).resolves.toBeTruthy(); + } + } finally { + await rm(parentPath, { recursive: true, force: true }); + } + }); + it('initializes the selected non-empty folder in place without changing existing files', async () => { const projectPath = await mkdtemp(join(tmpdir(), 'niancode-direct-project-')); try { diff --git a/tests/unit/opencode-store.test.ts b/tests/unit/opencode-store.test.ts index e353a8b..b5e3437 100644 --- a/tests/unit/opencode-store.test.ts +++ b/tests/unit/opencode-store.test.ts @@ -3650,6 +3650,7 @@ describe('opencode store', () => { directoryMode: 'create-child-directory', selectedPath: '/Users/kid/NianCode', projectName: '星星收集器', + projectType: 'mini_game', }); expect(opened).toEqual(project); @@ -3658,6 +3659,7 @@ describe('opencode store', () => { body: JSON.stringify({ parentPath: '/Users/kid/NianCode', projectName: '星星收集器', + projectType: 'mini_game', }), }); expect(useOpencodeStore.getState().activeProject).toBeNull(); @@ -3676,6 +3678,7 @@ describe('opencode store', () => { const opened = await useOpencodeStore.getState().createProject({ directoryMode: 'use-selected-directory', selectedPath: '/Users/kid/NianCode/星星收集器', + projectType: 'custom', }); expect(opened).toEqual(project); @@ -3683,6 +3686,7 @@ describe('opencode store', () => { method: 'POST', body: JSON.stringify({ projectPath: '/Users/kid/NianCode/星星收集器', + projectType: 'custom', }), }); expect(useOpencodeStore.getState().activeProject).toBeNull(); diff --git a/tests/unit/project-config.test.ts b/tests/unit/project-config.test.ts index f74b707..ec3c9e9 100644 --- a/tests/unit/project-config.test.ts +++ b/tests/unit/project-config.test.ts @@ -45,6 +45,7 @@ describe('project-owned contact configuration', () => { it('creates an empty, template-free project config', () => { const config = createProjectConfig('2026-07-11T00:00:00.000Z'); expect(config).toMatchObject({ + projectType: 'custom', initialized: false, superpowersEnabled: false, agents: [], @@ -53,6 +54,19 @@ describe('project-owned contact configuration', () => { expect('templateId' in config).toBe(false); }); + it('normalizes legacy configs without a project type as custom', () => { + const legacyConfig = { ...createProjectConfig(), projectType: undefined }; + expect(normalizeProjectConfig(legacyConfig).projectType).toBe('custom'); + expect(normalizeProjectConfig({ + ...legacyConfig, + projectType: 'mini_game', + }).projectType).toBe('mini_game'); + expect(() => normalizeProjectConfig({ + ...legacyConfig, + projectType: 'future_project', + })).toThrow('Invalid project type'); + }); + it('requires a unique name, preset avatar, model, and responsibility for every contact', () => { expect(validateAgentConfigs([])).toEqual([]); expect(validateAgentConfigs([createContact({ name: '' })])).toContain('agent-test:name-required'); @@ -130,4 +144,18 @@ describe('project-owned contact configuration', () => { agents: [createContact({ model: null })], })).rejects.toThrow('model-required'); }); + + it('does not allow the persisted project type to change', async () => { + const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-type-')); + const initial = await createInitialProjectConfig(projectPath, { projectType: 'mini_game' }); + + await expect(writeProjectConfig(projectPath, { + ...initial, + projectType: 'mini_program', + })).rejects.toThrow('已有项目类型不可更改'); + await expect(readProjectConfig(projectPath)).resolves.toMatchObject({ + status: 'valid', + config: { projectType: 'mini_game' }, + }); + }); }); diff --git a/tests/unit/project-configuration-page.test.tsx b/tests/unit/project-configuration-page.test.tsx index 0dbd973..d86e5d4 100644 --- a/tests/unit/project-configuration-page.test.tsx +++ b/tests/unit/project-configuration-page.test.tsx @@ -52,7 +52,7 @@ describe('ProjectConfiguration', () => { dispatchEvent: vi.fn(), })), }); - const config = createProjectConfig(); + const config = createProjectConfig(undefined, 'mini_game'); useOpencodeStore.setState({ activeProject: project, projects: [project] }); useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null }); useProviderStore.setState({ @@ -85,6 +85,29 @@ describe('ProjectConfiguration', () => { expect(screen.queryAllByTestId(/^project-agent-/)).toHaveLength(0); }); + it('does not offer one-click submission for custom projects', async () => { + const customConfig = createProjectConfig(); + useProjectConfigStore.setState({ + configsByProjectId: { [project.id]: customConfig }, + knowledgeByProjectId: { [project.id]: [] }, + errorsByProjectId: {}, + loadingProjectId: null, + }); + const fallback = hostApiFetchMock.getMockImplementation(); + hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => { + if (path === `/api/opencode/projects/config?projectId=${project.id}`) { + return { status: 'valid', config: customConfig, knowledgeFiles: [] }; + } + if (!fallback) throw new Error(`Unexpected path ${path}`); + return fallback(path, init); + }); + + render(); + + expect(await screen.findByText('自定义项目暂未配置发布方式。如需一键提交,请新建小游戏或小程序项目。')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '一键提交审核' })).not.toBeInTheDocument(); + }); + it('creates a contact with the required basics and leaves advanced settings empty', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: '新增伙伴' })); diff --git a/tests/unit/project-directory-initialization.test.ts b/tests/unit/project-directory-initialization.test.ts index 3a319b7..0a15114 100644 --- a/tests/unit/project-directory-initialization.test.ts +++ b/tests/unit/project-directory-initialization.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { initializeProjectDirectory } from '@electron/opencode/project-directory-initialization'; +import type { ProjectType } from '../../shared/project-config'; const temporaryDirectories: string[] = []; @@ -13,6 +14,98 @@ afterEach(async () => { }); describe('project directory initialization', () => { + it.each([ + ['mini_game', ['game.json', 'game.js']], + ['mini_program', ['app.json', 'app.js', join('pages', 'index', 'index.js')]], + ] satisfies Array<[ProjectType, string[]]>)('creates a complete %s Vite project template', async (projectType, typeFiles) => { + const parentPath = await mkdtemp(join(tmpdir(), 'makelore-project-parent-')); + temporaryDirectories.push(parentPath); + const projectPath = join(parentPath, projectType); + + const result = await initializeProjectDirectory({ + projectPath, + projectType, + allowExistingDirectory: false, + }); + + expect(result).toMatchObject({ + reusedExistingConfig: false, + config: { projectType }, + }); + const packageJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf8')) as { + scripts: Record; + devDependencies: Record; + }; + const packageLock = JSON.parse(await readFile(join(projectPath, 'package-lock.json'), 'utf8')) as { + lockfileVersion: number; + packages: Record; + }; + expect(packageJson.scripts.build).toBe('vite build'); + expect(packageJson.devDependencies.vite).toBe('7.3.1'); + expect(packageLock.lockfileVersion).toBe(3); + expect(packageLock.packages['node_modules/vite']).toBeTruthy(); + await expect(readFile(join(projectPath, 'index.html'), 'utf8')).resolves.toContain('type="module"'); + for (const relativePath of typeFiles) { + await expect(readFile(join(projectPath, relativePath), 'utf8')).resolves.not.toBe(''); + } + }); + + it('keeps custom projects on the existing minimal project structure', async () => { + const parentPath = await mkdtemp(join(tmpdir(), 'makelore-custom-parent-')); + temporaryDirectories.push(parentPath); + const projectPath = join(parentPath, 'custom-project'); + + const result = await initializeProjectDirectory({ + projectPath, + projectType: 'custom', + allowExistingDirectory: false, + }); + + expect(result.config.projectType).toBe('custom'); + expect((await readdir(projectPath)).sort()).toEqual(['.niancode', 'TASKS.md', 'VERSION.md', 'knowledge']); + }); + + it('reuses an existing project only when an explicitly selected type matches', async () => { + const parentPath = await mkdtemp(join(tmpdir(), 'makelore-existing-parent-')); + temporaryDirectories.push(parentPath); + const projectPath = join(parentPath, 'existing-project'); + await initializeProjectDirectory({ + projectPath, + projectType: 'mini_game', + allowExistingDirectory: false, + }); + + await expect(initializeProjectDirectory({ + projectPath, + projectType: 'mini_game', + allowExistingDirectory: true, + })).resolves.toMatchObject({ reusedExistingConfig: true, config: { projectType: 'mini_game' } }); + await expect(initializeProjectDirectory({ + projectPath, + projectType: 'mini_program', + allowExistingDirectory: true, + })).rejects.toThrow('已有项目类型不可更改'); + await expect(initializeProjectDirectory({ + projectPath, + allowExistingDirectory: true, + })).resolves.toMatchObject({ reusedExistingConfig: true, config: { projectType: 'mini_game' } }); + }); + + it('does not change an existing folder when a publishable template file conflicts', async () => { + const projectPath = await mkdtemp(join(tmpdir(), 'makelore-template-conflict-')); + temporaryDirectories.push(projectPath); + await writeFile(join(projectPath, 'package.json'), '{"name":"user-project"}\n', 'utf8'); + + await expect(initializeProjectDirectory({ + projectPath, + projectType: 'mini_game', + allowExistingDirectory: true, + })).rejects.toThrow('Project initialization conflict: package.json already exists'); + + expect((await readdir(projectPath)).sort()).toEqual(['package.json']); + await expect(readFile(join(projectPath, 'package.json'), 'utf8')).resolves.toBe('{"name":"user-project"}\n'); + }); + it('rolls back only files and directories created by a failed in-place merge', async () => { const projectPath = await mkdtemp(join(tmpdir(), 'niancode-in-place-rollback-')); temporaryDirectories.push(projectPath); diff --git a/tests/unit/project-packager.test.ts b/tests/unit/project-packager.test.ts index 2beffa4..2c68ed6 100644 --- a/tests/unit/project-packager.test.ts +++ b/tests/unit/project-packager.test.ts @@ -18,6 +18,7 @@ import { createStaticProjectPackage, ProjectPackageError, } from '@electron/services/project-packager'; +import { createProjectConfig, type ProjectType } from '../../shared/project-config'; const openRaceHook = vi.hoisted(() => ({ beforeOpen: null as null | ((path: string) => Promise), @@ -39,11 +40,17 @@ const AdmZip = require('adm-zip') as typeof import('adm-zip'); const tempDirectories: string[] = []; -async function createProject(): Promise { +async function createProject(projectType: ProjectType = 'mini_game'): Promise { const projectPath = await mkdtemp(join(tmpdir(), 'makelore-project-packager-')); tempDirectories.push(projectPath); await mkdir(join(projectPath, 'src'), { recursive: true }); await mkdir(join(projectPath, 'public'), { recursive: true }); + await mkdir(join(projectPath, '.niancode'), { recursive: true }); + await writeFile( + join(projectPath, '.niancode', 'project.json'), + JSON.stringify(createProjectConfig('2026-08-09T00:00:00.000Z', projectType)), + 'utf8', + ); await writeFile( join(projectPath, 'package.json'), JSON.stringify({ @@ -83,7 +90,6 @@ describe('project packager', () => { await mkdir(join(projectPath, 'node_modules', 'vite'), { recursive: true }); await mkdir(join(projectPath, '.git'), { recursive: true }); await mkdir(join(projectPath, 'dist'), { recursive: true }); - await mkdir(join(projectPath, '.niancode'), { recursive: true }); for (const directory of ['.ssh', '.aws', '.gnupg', '.kube', '.docker', 'secrets']) { await mkdir(join(projectPath, directory), { recursive: true }); await writeFile(join(projectPath, directory, 'credential'), 'private', 'utf8'); @@ -91,7 +97,7 @@ describe('project packager', () => { await writeFile(join(projectPath, 'node_modules', 'vite', 'index.js'), 'generated', 'utf8'); await writeFile(join(projectPath, '.git', 'config'), 'private', 'utf8'); await writeFile(join(projectPath, 'dist', 'index.html'), 'built', 'utf8'); - await writeFile(join(projectPath, '.niancode', 'project.json'), '{}', 'utf8'); + await writeFile(join(projectPath, '.niancode', 'local-state.json'), '{}', 'utf8'); await writeFile(join(projectPath, '.env'), 'TOKEN=secret', 'utf8'); await writeFile(join(projectPath, '.env.example'), 'TOKEN=replace-me', 'utf8'); await writeFile(join(projectPath, '.npmrc'), '//registry/:_authToken=secret', 'utf8'); @@ -170,6 +176,7 @@ describe('project packager', () => { ]); expect(archive.readAsText('niancode.yml')).toBe( 'schema_version: 1\n' + + 'project_type: mini_game\n' + 'kind: web\n' + 'runtime: static\n' + 'build:\n' @@ -177,10 +184,44 @@ describe('project packager', () => { + ' package_manager: npm\n' + ' entry: index.html\n', ); + expect(first.manifest.project_type).toBe('mini_game'); expect(entries.every((entry) => entry.header.time.getFullYear() === 1980)).toBe(true); await expect(readFile(join(projectPath, 'niancode.yml'), 'utf8')).resolves.toBe('runtime: compose\n'); }); + it('writes the selected publishable project type into the generated manifest', async () => { + const projectPath = await createProject('mini_program'); + const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-')); + tempDirectories.push(outputDirectory); + const archivePath = join(outputDirectory, 'project.zip'); + + const summary = await createStaticProjectPackage({ projectPath, archivePath }); + + expect(summary.manifest.project_type).toBe('mini_program'); + expect(new AdmZip(archivePath).readAsText('niancode.yml')).toContain('project_type: mini_program\n'); + }); + + it('rejects custom projects before validating Vite source files', async () => { + const projectPath = await mkdtemp(join(tmpdir(), 'makelore-custom-packager-')); + tempDirectories.push(projectPath); + await mkdir(join(projectPath, '.niancode'), { recursive: true }); + await writeFile( + join(projectPath, '.niancode', 'project.json'), + JSON.stringify(createProjectConfig('2026-08-09T00:00:00.000Z', 'custom')), + 'utf8', + ); + const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-')); + tempDirectories.push(outputDirectory); + + await expect(createStaticProjectPackage({ + projectPath, + archivePath: join(outputDirectory, 'project.zip'), + })).rejects.toMatchObject({ + code: 'PROJECT_TYPE_UNPUBLISHABLE', + message: '自定义项目暂未配置发布方式,请新建小游戏或小程序项目', + }); + }); + it('rejects a project that is not a locked npm Vite project', async () => { const projectPath = await createProject(); await writeFile( diff --git a/tests/unit/project-publish-action.test.tsx b/tests/unit/project-publish-action.test.tsx index 2ea9a08..bafda84 100644 --- a/tests/unit/project-publish-action.test.tsx +++ b/tests/unit/project-publish-action.test.tsx @@ -76,7 +76,7 @@ describe('ProjectPublishAction', () => { it('submits automatic metadata and locks after Builder succeeds', async () => { fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded')); - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); expect(screen.getByRole('button', { name: '正在检查并提交…' })).toBeDisabled(); @@ -89,7 +89,7 @@ describe('ProjectPublishAction', () => { title: 'space-cleaner', summary: 'space-cleaner,由 Makelore 创建的作品。', cover_url: null, - category: 'web', + category: 'mini_game', age_band: null, difficulty: null, }, @@ -109,7 +109,7 @@ describe('ProjectPublishAction', () => { fetchCurrentWorksProjectStatusMock.mockResolvedValue( projectStatus('failed', 'BROWSER_SMOKE_FAILED'), ); - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await flushSubmission(); @@ -128,7 +128,7 @@ describe('ProjectPublishAction', () => { fetchCurrentWorksProjectStatusMock.mockRejectedValue( new Error('socket closed at /srv/private/status.py'), ); - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await flushSubmission(); @@ -145,7 +145,7 @@ describe('ProjectPublishAction', () => { const status = projectStatus('queued'); status.latest_version.id = 'another-version'; fetchCurrentWorksProjectStatusMock.mockResolvedValue(status); - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await flushSubmission(); @@ -165,7 +165,7 @@ describe('ProjectPublishAction', () => { new Error('Traceback: failed at /srv/private/package.ts token=secret'), { code: 'INTERNAL_DATABASE_FAILURE', statusCode: 503 }, )); - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await flushSubmission(); @@ -182,14 +182,14 @@ describe('ProjectPublishAction', () => { new Error('D:/repo/space-cleaner/package-lock.json does not exist'), { code: 'PROJECT_FILE_MISSING' }, )); - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await flushSubmission(); const failure = screen.getByTestId('project-publish-failure'); expect(failure).toHaveTextContent('项目文件不完整'); - expect(failure).toHaveTextContent('请让开发助手补齐 package.json、package-lock.json 和 index.html'); + expect(failure).toHaveTextContent('请让开发助手修复当前项目模板'); expect(failure).not.toHaveTextContent('D:/repo'); expect(failure).not.toHaveTextContent('PROJECT_FILE_MISSING'); expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled(); @@ -197,7 +197,7 @@ describe('ProjectPublishAction', () => { it('keeps generated metadata within the server limits', async () => { const longName = `作品-${'x'.repeat(250)}`; - render(); + render(); fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await flushSubmission(); @@ -206,5 +206,6 @@ describe('ProjectPublishAction', () => { expect(input.project.app_id).toBe('makelore-prj-space-cleaner'); expect(input.project.title).toHaveLength(160); expect(input.project.summary).toBe(`${input.project.title},由 Makelore 创建的作品。`); + expect(input.project.category).toBe('mini_program'); }); }); diff --git a/tests/unit/sidebar-opencode-projects.test.tsx b/tests/unit/sidebar-opencode-projects.test.tsx index 278c75f..46e4aa4 100644 --- a/tests/unit/sidebar-opencode-projects.test.tsx +++ b/tests/unit/sidebar-opencode-projects.test.tsx @@ -232,9 +232,9 @@ describe('Sidebar project initialization flow', () => { expect(update).toHaveAccessibleName('下载新版本 0.9.2'); }); - it('creates a path-backed project without selecting a template and opens configuration', async () => { + it('defaults to a publishable mini-game project and opens configuration', async () => { const project = { id: 'prj_game', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '2026-07-11T00:00:00.000Z', updatedAt: '2026-07-11T00:00:00.000Z', lastOpenedAt: '2026-07-11T00:00:00.000Z' }; - const config = createProjectConfig(); + const config = createProjectConfig(undefined, 'mini_game'); invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo/星星收集器'] }); hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => { if (path === '/api/opencode/projects') return { projects: [], activeProject: null }; @@ -245,19 +245,22 @@ describe('Sidebar project initialization flow', () => { }); render(); fireEvent.click(await screen.findByRole('button', { name: '新建项目' })); + expect(screen.getByRole('radio', { name: '小游戏' })).toBeChecked(); + expect(screen.getByRole('radio', { name: '小程序' })).not.toBeChecked(); + expect(screen.getByRole('radio', { name: '自定义项目' })).not.toBeChecked(); expect(screen.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked(); expect(screen.queryByLabelText('项目名称')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '选择路径' })); await waitFor(() => expect(screen.getByLabelText('项目路径')).toHaveValue('D:/repo/星星收集器')); expect(screen.getByText('将直接接入此文件夹,项目名称使用“星星收集器”,不会重命名目录。')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '确认创建' })); - await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', { method: 'POST', body: JSON.stringify({ projectPath: 'D:/repo/星星收集器' }) })); + await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', { method: 'POST', body: JSON.stringify({ projectPath: 'D:/repo/星星收集器', projectType: 'mini_game' }) })); await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/project-config')); }); it('keeps creating a named child folder when that directory mode is selected', async () => { const project = { id: 'prj_child', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '', updatedAt: '', lastOpenedAt: '' }; - const config = createProjectConfig(); + const config = createProjectConfig(undefined, 'mini_program'); invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo'] }); hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => { if (path === '/api/opencode/projects') return { projects: [], activeProject: null }; @@ -269,6 +272,7 @@ describe('Sidebar project initialization flow', () => { render(); fireEvent.click(await screen.findByRole('button', { name: '新建项目' })); + fireEvent.click(screen.getByRole('radio', { name: '小程序' })); fireEvent.click(screen.getByRole('radio', { name: '在所选位置新建下级文件夹' })); fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '星星收集器' } }); fireEvent.click(screen.getByRole('button', { name: '选择路径' })); @@ -280,6 +284,7 @@ describe('Sidebar project initialization flow', () => { body: JSON.stringify({ parentPath: 'D:/repo', projectName: '星星收集器', + projectType: 'mini_program', }), })); }); diff --git a/tests/unit/works-project-publish.test.ts b/tests/unit/works-project-publish.test.ts index 0b906f7..ff71edb 100644 --- a/tests/unit/works-project-publish.test.ts +++ b/tests/unit/works-project-publish.test.ts @@ -6,7 +6,8 @@ import { describe('works project publish guidance', () => { it.each([ - ['PROJECT_FILE_MISSING', '项目文件不完整', 'package-lock.json'], + ['PROJECT_FILE_MISSING', '项目文件不完整', '修复当前项目模板'], + ['PROJECT_TYPE_UNPUBLISHABLE', '这个项目没有配置发布方式', '新建小游戏或小程序项目'], ['DEPENDENCY_PREFETCH_FAILED', '暂时无法下载项目依赖', 'package-lock.json'], ['OUTPUT_MISSING', '没有生成可运行页面', 'index.html'], ['RELEASE_STORE_FAILED', '平台暂时无法保存发布文件', '无需修改项目'], diff --git a/tests/unit/works-routes.test.ts b/tests/unit/works-routes.test.ts index 94f3326..5dc6cc8 100644 --- a/tests/unit/works-routes.test.ts +++ b/tests/unit/works-routes.test.ts @@ -5,6 +5,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { handleWorksRoutes } from '@electron/api/routes/works'; +import { createProjectConfig } from '../../shared/project-config'; const readWorksPublishFileMock = vi.hoisted(() => vi.fn()); const readWorksDeployCheckMock = vi.hoisted(() => vi.fn()); @@ -57,6 +58,12 @@ function createRequest(method: string, body?: unknown, headers: Record { await mkdir(join(projectPath, 'src'), { recursive: true }); + await mkdir(join(projectPath, '.niancode'), { recursive: true }); + await writeFile( + join(projectPath, '.niancode', 'project.json'), + JSON.stringify(createProjectConfig('2026-08-09T00:00:00.000Z', 'mini_game')), + 'utf8', + ); await writeFile(join(projectPath, 'package.json'), JSON.stringify({ name: 'space-cleaner', private: true,