diff --git a/.project-docs/10-decisions/proposals/20260904-project-scaffold-implementation-7e4c2a91__interactive-ai-app-type.md b/.project-docs/10-decisions/proposals/20260904-project-scaffold-implementation-7e4c2a91__interactive-ai-app-type.md new file mode 100644 index 0000000..ebaf5b5 --- /dev/null +++ b/.project-docs/10-decisions/proposals/20260904-project-scaffold-implementation-7e4c2a91__interactive-ai-app-type.md @@ -0,0 +1,45 @@ +# 交互式 AI 应用统一类型提案 + +## 文档信息 + +- 状态:Proposed,等待 Integration Gate 提升 +- 所有者:`20260904-project-scaffold-implementation-7e4c2a91` +- 日期:2026-09-04 +- 影响范围:项目创建、项目配置读取、脚手架、发布打包与作品元数据 + +## 决策 + +新建项目的唯一可发布类型改为: + +- 稳定标识:`interactive_ai_app` +- 展示名称:`交互式 AI 应用` + +`mini_game` 与 `mini_program` 不再出现在新建入口,也不再由新客户端写入。两者作为历史只读别名,在项目配置读取边界统一归一化为 `interactive_ai_app`,不批量改写用户已有的 `.makelore/project.json`。归一化后的打包清单和后续提交元数据只输出规范值。 + +`custom` 仍是独立的工作空间类型,并继续禁止进入平台的一键发布流程。 + +## 脚手架与发布合同 + +- 脚手架只维护一套通用 Vite 静态应用模板,不再按小游戏/小程序维护两套业务模板。 +- 历史项目调用脚手架时使用相同的统一模板,脚本返回规范类型,但保留原始项目元数据。 +- 固定 npm/Vite 构建、静态产物合同、浏览器预检、上传和审批要求不因类型合并而放宽。 +- 平台服务应先兼容接收三个值并统一输出 `interactive_ai_app`,再发布只写规范值的新客户端,以支持滚动升级。 + +## 兼容与迁移边界 + +- 不执行数据库或本地配置的批量历史迁移。 +- 不增加版本开关、双写、模板迁移框架或类型特有构建分支。 +- 旧客户端上传 `mini_game` / `mini_program` 时,服务端在校验边界接受并归一化;`custom`、未知值仍拒绝。 +- 历史发布记录保持不可变;公开读取和信息流投影使用规范类型。 + +## 被取代的语义 + +本提案在集成后取代既有文档中“小游戏与小程序是两个可发布项目类型”以及“二者分别生成不同模板”的部分。既有关于静态 Vite 发布、安全预检、制品不可变与审批流程的约束继续有效。 + +## 验收 + +- 新建界面只显示“交互式 AI 应用”和“自定义项目”。 +- 新建可发布项目落盘 `interactive_ai_app`,脚手架生成统一模板。 +- 两种历史配置均可打开和发布,原配置文件不会仅因读取而被改写。 +- 客户端清单、上传元数据和平台公开投影均使用 `interactive_ai_app`。 +- `custom` 与未知类型仍被拒绝发布。 diff --git a/.project-docs/30-worklog/tasks/20260904-project-scaffold-implementation-7e4c2a91.md b/.project-docs/30-worklog/tasks/20260904-project-scaffold-implementation-7e4c2a91.md new file mode 100644 index 0000000..90e9837 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260904-project-scaffold-implementation-7e4c2a91.md @@ -0,0 +1,105 @@ +# Task: Implement project scaffold Skill plugin + +## Identity + +- Task ID: 20260904-project-scaffold-implementation-7e4c2a91 +- Mode: Feature +- Branch: codex/20260904-diagnose-design-stream-6a4e9c21-diagnose-design-stream +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-diagnose-design-stream-6a4e9c21 +- Base commit: 336e0bb0caf24537b7b0f350aba3e04a37f5544c +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Preserve the current project-creation boundary and add regression coverage proving that no mini-game, mini-program, or custom business scaffold is created. +- Correct the new-project product copy so users are directed to the explicit project-scaffold Skill after creation. +- Expose the application-owned Node executable to direct Pi workers and the shared Agent Server. +- Treat non-empty standard Skill `scripts/` directories as executable code in Device Package preview, confirmation, and details copy. +- Add an independently installable `plugins/makelore-project-scaffold/` package containing one Skill, deterministic scaffold script, fixed templates, submission requirements, and tests. +- Make the Skill itself route publication/readiness requests and carry the current platform contract as four distinct classes: required input, direct blocker, source-package exclusion, and runtime/platform-only confirmation. +- Add regression coverage that keeps the documented source-package exclusion set aligned with the current packager without introducing a second build or publication implementation. +- Verify the unified generated project through the current project packager and local npm/Vite release seam, plus both historical input aliases at the compatibility boundary. +- Replace the two publishable project types with canonical `interactive_ai_app` / “交互式 AI 应用” across creation, configuration, packaging, the scaffold Skill, and Works Square-facing metadata while retaining read compatibility for existing `mini_game` and `mini_program` projects. + +## Intent And Constraints + +- Implement accepted proposal `ML-PROJECT-SCAFFOLD-001` without restoring project-creation side effects. +- Keep immutable `.makelore/project.json.projectType` as the only scaffold selector; do not add scaffold state, a template migration layer, `--force`, or a dedicated Pi extension/tool. +- Execute scaffolding only through the existing Pi `bash` project write lease and `MAKELORE_NODE_EXECUTABLE`; do not fall back to system Node or Python. +- Never overwrite target files. Preflight the full fixed target set and roll back only files/directories created by the current handled invocation. +- Preserve Main-owned fixed npm/Vite build, source/built artifact contract, browser preflight, and Works Square operational review as the release authority. +- Keep feature-task writes within product code, tests, this task record, and task-prefixed supporting records; canonical project memory changes remain promotion candidates for Integration Gate. +- Stage the plugin source under `plugins/makelore-project-scaffold/` for local Device Package installation. Immutable Git tagging, publication, and cross-platform packaged smoke remain release follow-ups and are not claimed by this task. + +## Outcome + +- Confirmed the request's original deletion premise was already true on base `336e0bb`: Project Service creates only `.makelore/project.json` and `knowledge/`. Added creation regressions for the canonical interactive type and `custom`, and removed the Sidebar promise that project creation itself makes a publishable scaffold. +- Added the non-overridable `MAKELORE_NODE_EXECUTABLE` host contract to both direct Pi workers and the shared Agent Server process. A worker overlay cannot spoof the application-selected executable path. +- Extended Device Package inspection so a declared Skill with a non-empty standard `scripts/` subtree is executable code. Skill-only script packages retain `kind: skill-only`, require the existing cross-turn executable confirmation, and show Skill-specific file/network/process authority copy in preview and plugin details. Existing Pi-extension and mixed packages retain executable warnings. +- Added `plugins/makelore-project-scaffold/` as a standard, independently versioned `0.1.0` plugin with one Skill and no Pi extension. Its package and plugin manifests are same-name/same-version and have no install lifecycle scripts. +- Added a deterministic Node-only `scaffold.mjs` selected exclusively by `.makelore/project.json.projectType`. It accepts only `--project-root`, creates one fixed six-file interactive AI application target set, reads all templates and preflights every target before writing, uses exclusive creation, preserves unrelated files, returns the flat version-1 JSON protocol, and rolls back only current-invocation files/directories on handled write failure. It provides no `--force`, type override, migration state, dependency install, network request, or publisher replacement. +- Regenerated the Vite 7.3.1 lockfile with the repository's fixed npm 11.6.2 and verified the unified template through source packaging and a real locked npm/Vite build. Added an approval-readiness reference that distinguishes static preparation from authoritative Main build/preflight/upload and Works Square operational review. +- Expanded the Skill's model-invocation routing and package-facing copy so requests about publishing, one-click submission, forbidden content, failure causes, or release readiness enter an explicit read-only readiness workflow. Project-specific reports now separate confirmed blockers, excluded-package impact, runtime/platform checks, and statically proven requirements, cite project-relative evidence, and use bounded conclusions that never claim approval. +- Reconciled the bundled publication reference against the current project packager, fixed npm/Vite release builder, static-artifact browser preflight, and Works submission route. It now records the full schema-v2 project prerequisite, root build inputs, exact size/count limits, direct path/link/output blockers, root `release.json` prohibition, both viewports and runtime failures, first-submit metadata/cover contract, and the exact case-insensitive excluded directory/file/suffix sets plus dynamic `.env`, credential JSON, Terraform, and `works-` patterns. +- Kept exclusion semantics explicit: excluded paths may remain in a project and are not blockers by existence; the Skill reports their effect only when required source or assets depend on them. The reference also states that an HTTP(S) literal is only a risk until the authoritative preflight makes an actual cross-origin request. +- Did not add a release-check script, duplicate packager, local dependency install, Vite invocation, browser launch, upload, or submission path to the plugin. Main and Works Square remain the only execution and approval authorities. +- Verified the real plugin through Device Package prepare, later-turn confirmation, commit, enabled-resource resolution, and installed script/template/reference reads. +- Replaced the two new-project values with canonical `interactive_ai_app` / “交互式 AI 应用”. New creation, generated `niancode.yml`, submission metadata, and publish UI use only the canonical value. Existing `mini_game` and `mini_program` configs normalize at the read boundary; a read or scaffold invocation does not rewrite the original config, while a later ordinary config mutation may persist the normalized value. +- Replaced the two business-specific starter trees with one generic interactive Vite starter. The scaffold continues to accept both historical values solely as compatibility aliases and reports the canonical type. +- Added the task-owned interactive-type proposal instead of modifying accepted ADRs or canonical project memory in feature mode. +- No existing user project files were removed or migrated. No plugin tag was published and the plugin was not silently installed into the running application. + +## Verification + +- `node --test plugins/makelore-project-scaffold/tests/scaffold.test.mjs`: 12 passed. Covers the canonical value, both historical aliases, exact unified output tree, metadata preservation, conflicts, invalid/missing/custom configs, invalid CLI, missing template, Chinese/space paths, and handled-write rollback. +- Focused type/scaffold/release/UI regressions: 7 Vitest files and 107 tests passed. +- `pnpm typecheck` and scoped ESLint over the changed type, configuration, packaging, UI, route, and test files passed. +- Desktop project-configuration Playwright E2E: 1 passed; the new-project dialog exposed only “交互式 AI 应用” and `custom`, and the publish action remained available for the canonical type. +- Real unified-template smoke: the actual scaffold ran in a verified temporary directory, fixed npm 11.6.2 completed `ci --ignore-scripts`, Vite 7.3.1 built successfully, and non-empty `dist/index.html` was produced; the temporary directory was removed. +- Plugin creator validation passed; Skill creator `quick_validate.py` returned `Skill is valid!` under explicit Python UTF-8 mode. +- `pnpm run build:vite` passed for Renderer, Main, Preload, and utility worker. Only existing Browserslist, mixed-import, and large-chunk warnings remained. +- Full serial main suite: 225/226 files passed; 1886 tests passed and 2 skipped. The only failure was the existing Pi Agent Server two-Bash-call wall-clock assertion (`2657ms < 2000ms`). An immediate isolated rerun again exceeded the fixed threshold (`2170ms`) while its other 5 tests passed; no project-type/scaffold path is involved and this task did not relax the threshold. The separately excluded pressure suite passed 1/1. +- Earlier focused Device Package, Pi environment, Plugin Details, release-builder, and browser-preflight suites recorded by this same task remained green; the type follow-up did not modify those implementations. +- Final `git diff --check` and task-aware project-doc drift check passed; only the task record and task-prefixed proposal changed under `.project-docs/`. +- Merge handoff re-ran `git diff --check` and the task-aware drift check after the explicit integration request; both remained green and no product files changed during handoff. + +## Follow-ups + +- Release `SCF-060`: move or mirror the staged plugin source to the chosen independent distribution repository, create an immutable SemVer tag, document the Device Package install source, and complete a small real-user prepare → confirm → install → initialize trial. Distribution location remains a product/release decision; this task does not invent it. +- Before claiming packaged support, run the Skill through an installed MakeLore build with the actual packaged executable and Git Bash on Windows. Run equivalent packaged smoke on macOS and native Linux; only then claim three-platform support. +- Verify enable/disable, upgrade to a second immutable version, downgrade/reinstall of a known version, and uninstall semantics. Uninstall must not delete files already created in user projects. +- Official Marketplace Skill artifacts currently reject executable `.mjs` assets. Continue using Device Package/Git distribution unless a separately approved Marketplace artifact policy adds executable Skill support. +- Track the existing `pi-agent-server-process-real` `<2000ms` assertion as a separate performance-test stability issue; it currently exceeds the threshold both under the full serial suite and in one isolated rerun, while its functional assertions pass. +- After code/plugin integration and release acceptance, run an Integration Gate task to promote the canonical project-creation and Device Package facts listed below. + +## Promotion Candidates + +- **Canonical interactive AI application type** + - Target canonical documents: `.project-docs/10-decisions/`, `.project-docs/30-worklog/current-state.md`, `.project-docs/40-domain/glossary.md`, `.project-docs/40-domain/business-rules.md`, and relevant architecture/data-flow summaries. + - Proposal: replace the distinct new-project `mini_game` / `mini_program` values and templates with `interactive_ai_app` / “交互式 AI 应用” and one generic interactive Vite scaffold; retain both old values only as historical read aliases. + - Evidence: shared/config normalizer and regressions, new-project E2E, canonical package/submission tests, all three scaffold input tests, real unified-template build, and task proposal `20260904-project-scaffold-implementation-7e4c2a91__interactive-ai-app-type.md`. + - Future impact: future templates and publishing rules evolve against one stable product type while existing local projects continue to open and publish without a batch rewrite. + - Semantic conflicts: supersedes the canonical two-publishable-type classification and any separate mini-game/mini-program template promises; it preserves fixed npm/Vite build, approval, and immutable release rules. + - Human confirmation required: no additional product decision is required because the user explicitly requested the merge; Integration Gate review is still required before canonical writes. +- **Project creation / scaffold ownership** + - Target canonical documents: `.project-docs/30-worklog/current-state.md`, `20-architecture/system-overview.md`, `20-architecture/data-flow.md`, `40-domain/glossary.md`, `40-domain/business-rules.md`; add a new integration entry to `30-worklog/task-history.md` rather than rewriting historical task rows. + - Proposal: replace current-state claims that mini-game/program creation atomically generates Vite templates with the accepted three-boundary model: Project Service owns only project metadata and `knowledge/`; explicit `makelore-project-scaffold` Skill owns starter files; Main/Works release code owns build, preflight, packaging, upload, and review state. + - Evidence: `electron/coding-projects/project-config.ts`, the canonical/custom creation regression, plugin script/tests, unified-template build smoke, and accepted proposal `ML-PROJECT-SCAFFOLD-001`. + - Future impact: prevents UI/docs/agents from assuming a newly created project is immediately publishable and keeps template evolution independent from project identity. + - Semantic conflicts: supersedes the integrated 2026-08-09 current-state claim; old task records remain historical evidence and must not be edited. + - Human confirmation required: no for factual promotion after the implementation commit is integrated; yes if product wants automatic/bundled initialization instead of the accepted explicit Skill boundary. +- **Executable Skill scripts and host runtime** + - Target canonical documents: `.project-docs/30-worklog/current-state.md`, `20-architecture/system-overview.md`, `20-architecture/module-map.md`, `20-architecture/data-flow.md`, and `40-domain/business-rules.md`. + - Proposal: state that a Device Package Skill with a non-empty standard `scripts/` subtree is executable code requiring preview disclosure and later-turn confirmation even when `kind` remains `skill-only`; parent worker/Agent Server environments expose the non-overridable application Node path through `MAKELORE_NODE_EXECUTABLE`. + - Evidence: `device-package-format.ts`, `device-package-manager.ts`, Plugin Details UI/tests, both Pi process tests, real package prepare/commit test, and Windows Git Bash quoting smoke. + - Future impact: keeps permission messaging accurate for script-backed Skills and gives portable Skills an application-owned runtime without PATH fallback. + - Semantic conflicts: current canonical wording mentions executable Pi extensions only. Existing rule that child workers receive no Device Package resources remains unchanged. + - Human confirmation required: no for promotion after integration; actual packaged platform claims still require the release smokes in Follow-ups. +- **Release obligation** + - Target canonical document: `.project-docs/80-commitments/commitments.md`. + - Proposal: fold this plugin's actual packaged Windows/macOS/Linux execution and Device Package lifecycle trial into the existing client release commitment instead of creating a parallel release gate. + - Evidence: workspace Windows Git Bash and real release-builder smokes are green; immutable tag, installed-app execution, macOS, and Linux evidence are absent. + - Future impact: prevents workspace-only evidence from being mistaken for a distributable cross-platform release. + - Semantic conflicts: the existing commitment already warns that workspace prepare tests do not replace final packaged proof; preserve that stricter rule. + - Human confirmation required: release owner confirmation is required before marking the commitment complete or choosing the public distribution location. diff --git a/electron/coding-packages/device-package-format.ts b/electron/coding-packages/device-package-format.ts index 7475001..7089bba 100644 --- a/electron/coding-packages/device-package-format.ts +++ b/electron/coding-packages/device-package-format.ts @@ -24,6 +24,7 @@ export interface InspectedDevicePackage { resolvedVersion: string | null; skillEntries: DevicePackageSkillEntry[]; extensionEntries: string[]; + hasSkillScripts: boolean; ignoredLifecycleScripts: string[]; } @@ -143,6 +144,25 @@ async function collectSkillFiles(root: string): Promise { return result; } +async function directoryContainsFile(directory: string): Promise { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.isFile() || entry.isSymbolicLink()) return true; + if (entry.isDirectory() && await directoryContainsFile(path.join(directory, entry.name))) { + return true; + } + } + return false; +} + +async function skillHasScripts(skillPath: string): Promise { + const scriptsPath = path.join(path.dirname(skillPath), 'scripts'); + const metadata = await stat(scriptsPath).catch((error: NodeJS.ErrnoException) => { + if (error.code === 'ENOENT') return null; + throw error; + }); + return metadata?.isDirectory() === true && await directoryContainsFile(scriptsPath); +} + function manifestEntries(value: unknown, field: string): string[] { if (value === undefined) return []; const values = typeof value === 'string' ? [value] : value; @@ -195,7 +215,8 @@ export async function inspectDevicePackage( if (await existsFile(path.join(root, 'SKILL.md'))) skillFiles.push(path.join(root, 'SKILL.md')); const uniqueSkills = new Map(); - for (const skillPath of [...new Set(skillFiles.map((entry) => path.resolve(entry)))]) { + const uniqueSkillPaths = [...new Set(skillFiles.map((entry) => path.resolve(entry)))]; + for (const skillPath of uniqueSkillPaths) { const id = await skillId(skillPath); if (uniqueSkills.has(id)) throw new Error(`Duplicate Skill id: ${id}`); uniqueSkills.set(id, { id, entryPath: relativeContainedPath(root, skillPath) }); @@ -214,6 +235,7 @@ export async function inspectDevicePackage( resolvedVersion: nonempty(packageJson?.version, 128) ?? nonempty(codexManifest?.version, 128), skillEntries: [...uniqueSkills.values()].sort((left, right) => left.id.localeCompare(right.id)), extensionEntries: extensionEntries.sort(), + hasSkillScripts: (await Promise.all(uniqueSkillPaths.map(skillHasScripts))).some(Boolean), ignoredLifecycleScripts: LIFECYCLE_SCRIPT_NAMES.filter((name) => typeof scripts[name] === 'string'), }; } diff --git a/electron/coding-packages/device-package-manager.ts b/electron/coding-packages/device-package-manager.ts index 4e92596..6ac70df 100644 --- a/electron/coding-packages/device-package-manager.ts +++ b/electron/coding-packages/device-package-manager.ts @@ -21,7 +21,13 @@ const PLAN_TTL_MS = 10 * 60 * 1000; const INSTALL_OUTPUT_BYTES = 64 * 1024; const INSTALL_TIMEOUT_MS = 2 * 60 * 1000; const INDEX_SCHEMA_VERSION = 1; -const EXECUTABLE_WARNING = '此包包含可执行 Pi extension,将获得完整桌面权限,包括当前进程可用的文件、网络和进程权限。'; +const EXECUTABLE_WARNING = '此包包含可执行代码,将获得完整桌面权限,包括当前进程可用的文件、网络和进程权限。'; +const SKILL_SCRIPT_WARNING = '此包包含可执行 Skill 脚本;Skill 被调用时,脚本可能使用当前桌面用户可用的文件、网络和进程权限。'; + +function executableWarning(kind: InstallPreviewV1['kind'], includesExecutableCode: boolean): string | null { + if (!includesExecutableCode) return null; + return kind === 'skill-only' ? SKILL_SCRIPT_WARNING : EXECUTABLE_WARNING; +} export class DevicePackageError extends Error { constructor( @@ -375,7 +381,10 @@ export class DevicePackageManager { resolvedSource: string; inspected: Awaited>; }): Promise { - const includesExecutableCode = input.inspected.extensionEntries.length > 0; + const kind = devicePackageKind(input.inspected); + const includesExecutableCode = input.inspected.extensionEntries.length > 0 + || input.inspected.hasSkillScripts; + const warning = executableWarning(kind, includesExecutableCode); const resolvedVersion = input.inspected.resolvedVersion ?? `local-${new Date(this.now()).toISOString().replace(/[-:.]/gu, '')}`; safeStorageSegment(resolvedVersion); @@ -388,12 +397,12 @@ export class DevicePackageManager { packageId: input.inspected.packageId, displayName: input.inspected.displayName, resolvedVersion, - kind: devicePackageKind(input.inspected), + kind, skillEntries: input.inspected.skillEntries, extensionEntries: input.inspected.extensionEntries, includesExecutableCode, ignoredLifecycleScripts: input.inspected.ignoredLifecycleScripts, - warnings: includesExecutableCode ? [EXECUTABLE_WARNING] : [], + warnings: warning ? [warning] : [], scope: 'device-parent-workers', }; const record: DevicePackageRecordV1 = { @@ -435,7 +444,8 @@ export class DevicePackageManager { if (Date.parse(plan.preview.expiresAt) <= this.now()) { throw new DevicePackageError('local_package_plan_expired', 'Install preview has expired'); } - if (plan.preview.includesExecutableCode && !plan.preview.warnings.includes(EXECUTABLE_WARNING)) { + const warning = executableWarning(plan.preview.kind, plan.preview.includesExecutableCode); + if (warning && !plan.preview.warnings.includes(warning)) { throw new DevicePackageError('local_package_confirmation_required', 'Executable permission warning is missing'); } const stagedPackage = path.join(this.stagingDir, planId, 'package'); diff --git a/electron/coding-projects/project-config.ts b/electron/coding-projects/project-config.ts index c53de72..95aeafe 100644 --- a/electron/coding-projects/project-config.ts +++ b/electron/coding-projects/project-config.ts @@ -2,7 +2,7 @@ import { mkdir, stat } from 'node:fs/promises'; import path from 'node:path'; import { isProjectAgentAvatarDataUrl, - isProjectType, + normalizeProjectType, type ProjectAgentResponsibility, type ProjectType, } from '../../shared/project-config'; @@ -199,7 +199,8 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon } const record = value as Partial; if (record.schemaVersion !== 2) throw new Error('Unsupported coding project config schema'); - if (!isProjectType(record.projectType)) throw new Error('Invalid project type'); + const projectType = normalizeProjectType(record.projectType); + if (!projectType) throw new Error('Invalid project type'); if (record.projectId !== undefined && !isCanonicalCodingProjectId(record.projectId)) { throw new Error('Project identity is invalid'); } @@ -216,7 +217,7 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon validateAgentNames(agents); return { schemaVersion: 2, - projectType: record.projectType, + projectType, ...(record.projectId !== undefined ? { projectId: record.projectId } : {}), initialized: record.initialized === true, agents, diff --git a/electron/coding-runtime/pi/agent-server-process.ts b/electron/coding-runtime/pi/agent-server-process.ts index 41ed00b..a049dc5 100644 --- a/electron/coding-runtime/pi/agent-server-process.ts +++ b/electron/coding-runtime/pi/agent-server-process.ts @@ -524,7 +524,7 @@ export class PiAgentServerProcess { ], { cwd: this.options.runtimeRoot, - env: buildPiWorkerEnvironment(this.options.configDir), + env: buildPiWorkerEnvironment(this.options.configDir, this.options.executablePath), stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, detached: platform() !== 'win32', diff --git a/electron/coding-runtime/pi/worker-process.ts b/electron/coding-runtime/pi/worker-process.ts index 250167d..4dfd9bb 100644 --- a/electron/coding-runtime/pi/worker-process.ts +++ b/electron/coding-runtime/pi/worker-process.ts @@ -180,6 +180,7 @@ function diagnosticPathAliases(value: string | undefined): string[] { export function buildPiWorkerEnvironment( configDir: string, + executablePath: string, overlay: NodeJS.ProcessEnv = {}, inherited: NodeJS.ProcessEnv = process.env, ): NodeJS.ProcessEnv { @@ -196,6 +197,7 @@ export function buildPiWorkerEnvironment( return { ...env, ...overlay, + MAKELORE_NODE_EXECUTABLE: executablePath, ELECTRON_RUN_AS_NODE: '1', PI_CODING_AGENT_DIR: configDir, PI_OFFLINE: '1', @@ -335,7 +337,11 @@ export class PiWorkerProcess { [this.options.cliPath, ...args], { cwd: this.options.cwd, - env: buildPiWorkerEnvironment(this.options.configDir, this.options.env), + env: buildPiWorkerEnvironment( + this.options.configDir, + this.options.executablePath, + this.options.env, + ), stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, detached: platform() !== 'win32', diff --git a/electron/services/project-packager.ts b/electron/services/project-packager.ts index 7e053c7..8043ce9 100644 --- a/electron/services/project-packager.ts +++ b/electron/services/project-packager.ts @@ -279,7 +279,7 @@ async function readPublishableProjectType(projectPath: string): Promise/scripts/scaffold.mjs" --project-root "$PWD" + ``` + +5. Parse the command's single version-1 JSON result. Success is written only to stdout and includes `status: created`; failure is written only to stderr and includes top-level `code`, `message`, and optional project-relative `paths`. On success, report only the returned `createdFiles`. On failure, report the returned code, message, and paths when present. + +The script performs a complete conflict preflight before writing. Never add `--force`, delete or rename existing user files, retry by overwriting conflicts, or manually fill gaps after a script failure. Resolve the reported conflict with the user before running again. + +## Release-readiness workflow + +Keep this workflow read-only unless the user also asks to fix a reported issue or explicitly starts the product's existing one-click submission flow. + +1. Read `references/submission-requirements.md` in full. Its four categories are semantically different: required input, direct blocker, source-package exclusion, and authoritative runtime/platform check. +2. For a general requirements question, explain those categories and do not claim to have inspected a project. +3. For a project-specific check, gather project-relative evidence for: + - the validity and publishable type of `.makelore/project.json`; + - the required root files and the relevant `package.json` and `package-lock.json` fields; + - source file count, byte limits, path shapes, links or special files, and paths the source packager excludes; + - project-authored source or assets that appear to exist only under an excluded path; + - statically visible build-output risks such as configuration that emits a root `release.json`. +4. Classify every finding instead of flattening it into a blacklist: + - **阻断项**: current evidence proves the one-click flow will reject or cannot accept the project; + - **打包排除影响**: a path will not enter the source archive; its mere presence is not a blocker, but required content there is a release risk; + - **待权威流程验证**: npm installation, Vite execution, built output, both browser viewports, account/ownership, upload, cloud build, and review state; + - **已通过静态项**: a requirement supported by concrete file evidence. +5. Report the evidence path and observed value for every blocker. Do not infer an external-network blocker merely because source text contains an HTTP(S) URL; only an actual cross-origin request during authoritative preflight proves that failure. You may report the literal as a runtime risk. +6. Use exactly one of these conclusions: + - `不能进入一键提交` when at least one confirmed blocker exists; + - `可进入一键提交,仍需运行期验证` when static blockers are absent but the product flow has not run; + - `本地构建与预检已通过,仍待平台结果` only when the current one-click flow actually returned that evidence. + +Never say that a project is approved based on this Skill's inspection. Do not run `npm ci`, Vite configuration, project scripts, upload, or submission merely to answer a readiness question: those steps execute code or change external state and belong to the explicit product flow. Do not delete excluded files just to make the report look clean. + +## Error handling + +Treat these codes as stable outcomes: + +- `USAGE_INVALID`: the script invocation is malformed. +- `PROJECT_CONFIG_MISSING`: `.makelore/project.json` is absent. +- `PROJECT_CONFIG_INVALID`: project metadata is unreadable or unsupported. +- `PROJECT_TYPE_UNSUPPORTED`: the project is neither `interactive_ai_app` nor an existing legacy `mini_game` / `mini_program` project. +- `TARGET_CONFLICT`: at least one target path already exists or cannot be used; show every returned conflict. +- `TEMPLATE_UNAVAILABLE`: the installed plugin is incomplete. +- `WRITE_FAILED`: file creation failed and the script attempted to roll back only files and directories it created in this run. + +## Ownership boundaries + +- MakeLore owns project creation and `.makelore/project.json`. +- This skill owns only the fixed starter files listed by the scaffold script and never edits project metadata. +- New projects use `interactive_ai_app`. Existing `mini_game` and `mini_program` metadata are read-only compatibility aliases and receive the same unified scaffold; the script reports the canonical type without rewriting metadata. +- MakeLore's existing release builder and publisher remain authoritative for build, preflight, packaging, upload, and approval submission. +- The requirements reference is this plugin version's release contract snapshot. If it differs from runtime validation, report the mismatch, follow the runtime result, and do not silently reinterpret an exclusion as a prohibition. diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/agents/openai.yaml b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/agents/openai.yaml new file mode 100644 index 0000000..4b4b022 --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "MakeLore 项目初始化与发布检查" + short_description: "创建交互式 AI 应用骨架并检查发布要求" + default_prompt: "使用 $makelore-project-scaffold 初始化当前 MakeLore 交互式 AI 应用项目,并检查是否满足一键提交要求。" diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/package-lock.json b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/package-lock.json new file mode 100644 index 0000000..701d913 --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/package-lock.json @@ -0,0 +1,1117 @@ +{ + "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://mirrors.cloud.tencent.com/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/@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://mirrors.cloud.tencent.com/npm/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://mirrors.cloud.tencent.com/npm/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://mirrors.cloud.tencent.com/npm/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://mirrors.cloud.tencent.com/npm/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://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "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.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/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://mirrors.cloud.tencent.com/npm/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "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://mirrors.cloud.tencent.com/npm/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/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/package.json b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/package.json new file mode 100644 index 0000000..939fba1 --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/package.json @@ -0,0 +1,15 @@ +{ + "name": "makelore-project", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "npm@11.6.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/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/vite.config.js b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/vite.config.js new file mode 100644 index 0000000..c5257bb --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/common/vite.config.js @@ -0,0 +1,5 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', +}); diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/index.html b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/index.html new file mode 100644 index 0000000..eb354da --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/index.html @@ -0,0 +1,20 @@ + + + + + + MakeLore 交互式 AI 应用 + + +
+
+

MAKELORE

+

交互式 AI 应用

+

这是你的创作起点。修改页面、加入交互,让想法真正运行起来。

+ +

已经准备好了

+
+
+ + + diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/main.js b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/main.js new file mode 100644 index 0000000..6303f22 --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/main.js @@ -0,0 +1,10 @@ +import './style.css'; + +const button = document.querySelector('button'); +const status = document.querySelector('.status'); +let interactions = 0; + +button.addEventListener('click', () => { + interactions += 1; + status.textContent = `已完成 ${interactions} 次互动`; +}); diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/style.css b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/style.css new file mode 100644 index 0000000..f56b57e --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/assets/templates/interactive_ai_app/style.css @@ -0,0 +1,16 @@ +:root { + color: #192033; + background: #eef1ff; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +body { margin: 0; } +.page { min-height: 100vh; display: grid; place-items: center; padding: 24px; } +.card { width: min(520px, 100%); border: 1px solid rgb(86 91 150 / 18%); border-radius: 28px; background: rgb(255 255 255 / 88%); padding: clamp(28px, 7vw, 52px); box-shadow: 0 24px 80px rgb(62 68 128 / 16%); } +.eyebrow { margin: 0 0 12px; color: #6558d9; font-size: 12px; font-weight: 800; letter-spacing: 0.18em; } +h1 { margin: 0; font-size: clamp(34px, 8vw, 58px); line-height: 1.05; letter-spacing: -0.04em; } +.card > p:not(.eyebrow) { line-height: 1.7; } +button { min-height: 44px; margin-top: 12px; border: 0; border-radius: 14px; background: #6558d9; color: #fff; padding: 12px 20px; font: inherit; font-weight: 750; cursor: pointer; } +button:hover { background: #5145c5; } +.status { color: #596078; font-size: 14px; } diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/references/submission-requirements.md b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/references/submission-requirements.md new file mode 100644 index 0000000..0a98626 --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/references/submission-requirements.md @@ -0,0 +1,141 @@ +# MakeLore 发布与提交审批要求 + +这份文件是本插件版本对 MakeLore 一键提交合同的说明,供发布前检查和失败归因使用。MakeLore 客户端实际执行的源码打包、固定构建、双视口预检、上传以及平台返回结果始终是唯一权威。若本说明与当前客户端不一致,必须报告差异并以客户端结果为准。 + +## 正确区分四类结果 + +- **必须具备**:缺失或格式不符会直接阻断发布。 +- **直接阻断**:项目当前存在已知会被本地发布链拒绝的内容或状态。 +- **打包排除**:文件可以留在项目里,但不会进入源码归档。存在本身不等于违规;只有把构建或运行必需内容放在这些位置才会造成问题。 +- **运行时或平台确认**:仅凭静态文件不能下结论,必须由 MakeLore 一键提交实际运行后确认。 + +静态检查没有阻断项时,只能得出“可进入一键提交,仍需运行期验证”,不能称为“发布通过”或“审批通过”。 + +## 必须具备的发布输入 + +### 项目身份 + +- 项目目录必须存在且可读。 +- `.makelore/project.json` 必须是 MakeLore 可读取的完整 schema v2 项目配置;不仅 `projectType`,配置中的版本、项目身份、初始化状态、知识目录、Agent 和时间字段也必须保持有效。不要手工缩减为只有项目类型的 JSON。 +- 新项目的 `projectType` 必须是 `interactive_ai_app`。已有项目中的 `mini_game` 或 `mini_program` 会在读取时归一化为同一个“交互式 AI 应用”类型;`custom`、缺失配置和无效配置都不能发布。 + +### 根目录构建输入 + +- `package.json`、`package-lock.json` 和 `index.html` 必须位于项目根目录,且都是普通文件而非符号链接。 +- `package.json` 和 `package-lock.json` 必须分别是有效 JSON 对象,不能是数组、字符串或其他 JSON 值。 +- `package.json` 的 `dependencies.vite` 或 `devDependencies.vite` 必须是非空字符串。 +- `packageManager` 可以省略;若存在,必须是以 `npm@` 开头的字符串。 +- `package-lock.json.lockfileVersion` 必须是整数 `2` 或 `3`。 +- 源 `index.html` 去除空白后必须非空。 +- 当前插件脚手架以 npm 11.6.2 生成锁文件;实际发布仍以当前 MakeLore 客户端随附的固定 npm 运行时为准。 + +### 固定构建合同 + +- 发布器在隔离的源码快照中执行 `npm ci --ignore-scripts --no-audit --no-fund --prefer-offline`,不会运行依赖安装生命周期脚本。项目不得依赖 `preinstall`、`install`、`postinstall` 等脚本生成发布必需文件。 +- 随后使用锁文件实际安装出的项目 Vite 执行构建,并强制 `--base ./`、独立输出目录和清空输出目录。项目根目录已有的 `dist/` 或 `build/` 不会被直接发布。 +- Vite 配置及其插件会以当前桌面用户权限执行;这个构建步骤不是代码沙箱,也不是可信性证明。 +- 最终构建产物根目录必须包含普通、非空、有效 UTF-8 的 `index.html`。 + +### 数量与大小上限 + +- 源码归档最多 2,000 个文件,其中包括发布器生成的 `niancode.yml`,因此用户源码最多占其余名额。 +- 纳入源码包的文件总量最多 200 MiB;生成的源码 ZIP 最多 50 MiB。 +- `package.json` 最大 2 MiB,`package-lock.json` 最大 50 MiB,根 `index.html` 最大 10 MiB。 +- 构建产物最多 2,000 个文件,总量最多 50 MiB。 + +## 会直接阻断发布的内容或状态 + +### 源项目与路径 + +- 项目配置缺失、无效、类型为 `custom`,或项目目录不存在/不可读。 +- 必需根文件缺失、不是普通文件、超过单文件上限、JSON 结构无效、未声明 Vite、声明了非 npm `packageManager`、锁文件版本不受支持,或 `index.html` 为空。 +- 未被排除的任意符号链接、目录链接、socket、设备文件等非普通文件类型。 +- 相对归档路径为空,含反斜杠、空段、`.`、`..`、冒号,或任一路径段以空格或点结尾。 +- 任一路径段是 Windows 保留名 `CON`、`PRN`、`AUX`、`NUL`、`COM1`–`COM9`、`LPT1`–`LPT9`,包括这些名称带扩展名的形式。 +- 在大小写不敏感比较下重复的归档路径,例如同时存在 `Logo.png` 与 `logo.png`。 +- 源码扫描、读取或写入归档期间文件或目录发生变化。 +- 超出源码文件数、源码字节数或源码 ZIP 上限。 + +### 构建与产物 + +- 固定 npm 运行时不可用、`npm ci` 失败、Vite 不可用、Vite 构建失败、构建超时或被取消。 +- 构建输出包含符号链接或其他非普通文件。 +- 构建输出超过 2,000 个文件或 50 MiB。 +- 构建产物根目录缺少 `index.html`,该文件为空或不是有效 UTF-8。 +- 构建产物根目录包含大小写不限的 `release.json`。这里禁止的是**最终构建产物的根文件**;源码中同名文件只有在最终被输出到该位置时才会阻断。 + +### 页面运行预检 + +- 构建首页未能在 1280×720 和 390×844 两个视口中全部加载完成并显示可见文字、背景、有效图片/视频、SVG 或尺寸合理的 Canvas。 +- 未捕获异常、`console.error`、Chromium Log error、非取消的资源加载失败、渲染进程故障或任意 HTTP 响应状态大于等于 400。 +- 预检期间实际发起到本地预览 origin 以外的 HTTP/HTTPS 请求,或页面最终跳转离开精确预览 URL。源码里仅出现 URL 字符串还不能证明会触发该阻断。 +- 两个视口共用的 30 秒预检时限耗尽。每个视口加载后只等待短暂稳定时间,因此首屏不能依赖长时间后台任务才出现。 + +新窗口会被预检环境拒绝打开;项目不应依赖弹窗才能展示首屏或完成启动。 + +## 会从源码归档排除、但存在本身不阻断的内容 + +以下匹配均不区分大小写。目录规则按任意层级的目录名匹配;文件名规则按任意层级的 basename 匹配。 + +### 排除目录名 + +`.cache`、`.git`、`.gnupg`、`.idea`、`.kube`、`.next`、`.makelore`、`.niancode`、`.opencode`、`.project-docs`、`.turbo`、`.vite`、`.vscode`、`.aws`、`.docker`、`.ssh`、`build`、`coverage`、`deploy`、`dist`、`knowledge`、`node_modules`、`secrets`。 + +### 排除文件名 + +`.dockerignore`、`.ds_store`、`.git-credentials`、`.netrc`、`.npmrc`、`art_guide.md`、`asset_plan.md`、`credentials.json`、`debug.log`、`dockerfile`、`docker-compose.yaml`、`docker-compose.yml`、`findings.md`、`gdd.md`、`nginx.conf`、`niancode.yml`、`_netrc`、`id_dsa`、`id_ecdsa`、`id_ed25519`、`id_rsa`、`product_overview.md`、`progress.md`、`release.md`、`task_plan.md`、`tasks.md`、`thumbs.db`、`version.md`、`works-cloud-deploy.json`、`works-deploy-check.json`、`works-publish.json`、`部署报告.md`。 + +### 排除模式 + +- 文件名以 `.env` 开头。 +- 文件名以 `.crt`、`.jks`、`.key`、`.keystore`、`.log`、`.p12`、`.pem`、`.pfx`、`.p8`、`.ppk`、`.tfstate` 或 `.tfvars` 结尾。 +- 文件名任意位置包含 `.tfstate` 或 `.tfvars`。 +- JSON 文件名去掉非字母数字字符后包含 `serviceaccount`、`firebaseadmin` 或 `applicationdefaultcredentials`。 +- 项目根相对路径以 `works-` 开头的文件,包括这类顶层目录中的文件。 + +排除规则的后果: + +- 不要把项目自己编写的源代码、首屏素材或构建配置只放在上述位置;发布快照看不到它们。 +- `node_modules/` 被排除是正常行为,发布器会根据根 `package-lock.json` 重新安装依赖。 +- `.npmrc` 被排除,发布器还会显式使用空的临时 npm 配置;项目不能依赖本机或仓库 `.npmrc` 才能安装。 +- 项目里的 `niancode.yml` 被排除,发布器会生成自己的固定清单。 +- `VERSION.md` 不是必需文件。若它是 64 KiB 以内的稳定普通文件,`Current:` 后不超过 80 字符的值可用于版本名;否则发布器自动生成版本名。无论哪种情况,该文件都不进入源码归档。 +- 源码中已有 `dist/` 和 `build/` 不会被上传;最终发布的是固定流程本次生成的构建产物。 + +不要因为检查发现排除项就要求删除它们。应报告哪些路径被排除,以及是否有证据表明构建或运行依赖这些路径。 + +## 首次提交必须提供的平台资料 + +当前 MakeLore 界面合同要求: + +- 项目名称:必填,最多 160 个字符。 +- 项目简介:必填,最多 2,000 个字符。 +- 发布者姓名:必填,最多 160 个字符。 +- 发布者年龄:必填,为 1–150 的整数。 +- 项目封面:首次提交必填,仅支持 PNG、JPEG 或 WebP,必须非空、内容签名与声明格式相符,最大 10 MiB。 +- 详细介绍可选,最多 10,000 个字符。 +- 适龄范围和难度可选,各最多 40 个字符。 + +作品 `app_id` 和类别由当前项目生成,不要求用户另造。若平台已存在状态为 `draft` 或 `published` 的该作品,当前流程只提交新构建版本,并保留平台上的名称、简介、作者信息和封面。窗口打开后平台状态发生变化会产生资料冲突,本次不会覆盖或继续提交。 + +## 只能由一键提交和平台确认 + +- 当前账号是否已登录,登录态是否仍有效。 +- 当前本地项目是否仍存在,以及本机固定 Node/npm 发布运行时、npm 缓存和依赖来源是否可用。 +- `npm ci`、项目 Vite 配置/插件、实际构建输出和双视口页面运行是否通过。 +- 当前账号是否拥有目标作品,平台资料状态是否与发布窗口一致。 +- 同一项目是否已有本地构建占用,以及平台是否因现有构建、版本或审核状态拒绝新版本。 +- 平台是否接受首次作品资料、封面、源码归档、构建归档和版本化产物合同。 +- 云端构建最终是成功、失败、取消还是状态未知,以及运营审核最终是通过还是拒绝。 + +本 Skill 的静态检查不会安装依赖、执行 Vite 配置、启动浏览器预检、上传归档或提交审核。用户明确发起一键提交后,以该流程返回的错误码、构建状态和平台审核状态为准。 + +## 项目检查时的输出顺序 + +对具体项目做发布就绪检查时,按以下顺序报告,不打分: + +1. **结论**:只能使用 Skill 工作流定义的三种结论之一。 +2. **阻断项**:仅列有当前证据的直接阻断,并附项目相对路径和观察值。 +3. **打包排除影响**:列出实际命中的排除路径及其可能丢失的必需内容;若没有依赖证据,明确写“存在但不阻断”。 +4. **待权威流程验证**:列出本次没有也不能静态证明的构建、浏览器、账号和平台项。 +5. **已通过静态项**:列出已读取并确认的必要输入,不把未检查项写成通过。 diff --git a/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/scripts/scaffold.mjs b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/scripts/scaffold.mjs new file mode 100644 index 0000000..9047eeb --- /dev/null +++ b/plugins/makelore-project-scaffold/skills/makelore-project-scaffold/scripts/scaffold.mjs @@ -0,0 +1,304 @@ +#!/usr/bin/env node + +import { + lstat, + mkdir, + open, + readFile, + rmdir, + unlink, +} from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const skillDirectory = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const templateDirectory = path.join(skillDirectory, "assets", "templates"); +const fileOperations = { mkdir, open, rmdir, unlink }; + +const commonTargets = [ + ["package.json", "common/package.json"], + ["package-lock.json", "common/package-lock.json"], + ["vite.config.js", "common/vite.config.js"], +]; + +const targetsByProjectType = { + interactive_ai_app: [ + ...commonTargets, + ["index.html", "interactive_ai_app/index.html"], + ["src/main.js", "interactive_ai_app/main.js"], + ["src/style.css", "interactive_ai_app/style.css"], + ], +}; + +const canonicalProjectTypes = { + interactive_ai_app: "interactive_ai_app", + mini_game: "interactive_ai_app", + mini_program: "interactive_ai_app", +}; + +class ScaffoldError extends Error { + constructor(code, message, details = {}) { + super(message); + this.code = code; + this.details = details; + } +} + +function parseArguments(argv) { + if (argv.length !== 2 || argv[0] !== "--project-root" || !argv[1]?.trim()) { + throw new ScaffoldError( + "USAGE_INVALID", + "脚本参数无效,必须提供 --project-root 。", + ); + } + + return path.resolve(argv[1]); +} + +async function readProjectType(projectRoot) { + const configPath = path.join(projectRoot, ".makelore", "project.json"); + let source; + + try { + source = await readFile(configPath, "utf8"); + } catch (error) { + if (error?.code === "ENOENT") { + throw new ScaffoldError( + "PROJECT_CONFIG_MISSING", + "当前目录缺少 .makelore/project.json。", + ); + } + throw new ScaffoldError( + "PROJECT_CONFIG_INVALID", + "无法读取 .makelore/project.json。", + ); + } + + let config; + try { + config = JSON.parse(source); + } catch { + throw new ScaffoldError( + "PROJECT_CONFIG_INVALID", + ".makelore/project.json 不是有效 JSON。", + ); + } + + if (!config || typeof config !== "object" || config.schemaVersion !== 2) { + throw new ScaffoldError( + "PROJECT_CONFIG_INVALID", + ".makelore/project.json 必须使用 schemaVersion 2。", + ); + } + + const projectType = typeof config.projectType === "string" + ? canonicalProjectTypes[config.projectType] + : undefined; + if (!projectType || !Object.hasOwn(targetsByProjectType, projectType)) { + throw new ScaffoldError( + "PROJECT_TYPE_UNSUPPORTED", + "仅支持初始化 interactive_ai_app 项目。", + ); + } + + return projectType; +} + +function toNativePath(root, relativePath) { + return path.join(root, ...relativePath.split("/")); +} + +async function readTemplates(targets) { + const loaded = []; + const missing = []; + + for (const [targetPath, templatePath] of targets) { + try { + loaded.push({ + targetPath, + contents: await readFile(toNativePath(templateDirectory, templatePath)), + }); + } catch { + missing.push(targetPath); + } + } + + if (missing.length > 0) { + throw new ScaffoldError( + "TEMPLATE_UNAVAILABLE", + "插件缺少必需的工程模板,请重新安装。", + { paths: missing.sort() }, + ); + } + + return loaded; +} + +async function pathState(candidate) { + try { + return await lstat(candidate); + } catch (error) { + if (error?.code === "ENOENT") { + return undefined; + } + throw error; + } +} + +function parentDirectories(relativePath) { + const parents = []; + let current = path.posix.dirname(relativePath); + + while (current !== ".") { + parents.push(current); + current = path.posix.dirname(current); + } + + return parents.reverse(); +} + +async function preflightTargets(projectRoot, templates) { + const conflicts = new Set(); + const missingDirectories = new Set(); + + for (const { targetPath } of templates) { + let blockedByParent = false; + for (const directory of parentDirectories(targetPath)) { + const state = await pathState(toNativePath(projectRoot, directory)); + if (!state) { + missingDirectories.add(directory); + } else if (!state.isDirectory()) { + conflicts.add(directory); + blockedByParent = true; + break; + } + } + + if (!blockedByParent && await pathState(toNativePath(projectRoot, targetPath))) { + conflicts.add(targetPath); + } + } + + if (conflicts.size > 0) { + throw new ScaffoldError( + "TARGET_CONFLICT", + "目标路径已存在或必需的父路径不是目录,未写入任何文件。", + { paths: [...conflicts].sort() }, + ); + } + + return [...missingDirectories].sort((left, right) => { + const depthDifference = left.split("/").length - right.split("/").length; + return depthDifference || left.localeCompare(right); + }); +} + +function relativeProjectPath(projectRoot, absolutePath) { + const relativePath = path.relative(projectRoot, absolutePath); + return path.sep === "/" ? relativePath : relativePath.split(path.sep).join("/"); +} + +async function rollback(projectRoot, createdFiles, createdDirectories, operations) { + const remainingPaths = []; + for (const filePath of [...createdFiles].reverse()) { + try { + await operations.unlink(filePath); + } catch { + remainingPaths.push(relativeProjectPath(projectRoot, filePath)); + } + } + + for (const directoryPath of [...createdDirectories].reverse()) { + try { + await operations.rmdir(directoryPath); + } catch { + remainingPaths.push(relativeProjectPath(projectRoot, directoryPath)); + } + } + + return [...new Set(remainingPaths)].sort(); +} + +export async function writeScaffold( + projectRoot, + templates, + missingDirectories, + operations = fileOperations, +) { + const createdFiles = []; + const createdDirectories = []; + + try { + for (const relativeDirectory of missingDirectories) { + const absoluteDirectory = toNativePath(projectRoot, relativeDirectory); + await operations.mkdir(absoluteDirectory); + createdDirectories.push(absoluteDirectory); + } + + for (const { targetPath, contents } of templates) { + const absoluteTarget = toNativePath(projectRoot, targetPath); + let handle; + try { + handle = await operations.open(absoluteTarget, "wx"); + createdFiles.push(absoluteTarget); + await handle.writeFile(contents); + } finally { + await handle?.close(); + } + } + } catch { + const remainingPaths = await rollback( + projectRoot, + createdFiles, + createdDirectories, + operations, + ); + throw new ScaffoldError( + "WRITE_FAILED", + "创建工程骨架失败;本次新建内容已尽力回滚。", + remainingPaths.length > 0 ? { paths: remainingPaths } : {}, + ); + } +} + +async function scaffold(argv) { + const projectRoot = parseArguments(argv); + const projectType = await readProjectType(projectRoot); + const templates = await readTemplates(targetsByProjectType[projectType]); + const missingDirectories = await preflightTargets(projectRoot, templates); + await writeScaffold(projectRoot, templates, missingDirectories); + + return { + schemaVersion: 1, + ok: true, + status: "created", + projectType, + createdFiles: templates.map(({ targetPath }) => targetPath).sort(), + }; +} + +const isMain = process.argv[1] + && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url; + +if (isMain) { + try { + const result = await scaffold(process.argv.slice(2)); + process.stdout.write(`${JSON.stringify(result)}\n`); + } catch (error) { + const knownError = + error instanceof ScaffoldError + ? error + : new ScaffoldError("WRITE_FAILED", "创建工程骨架失败;未写入未知来源的内容。"); + + process.stderr.write( + `${JSON.stringify({ + schemaVersion: 1, + ok: false, + code: knownError.code, + message: knownError.message, + ...knownError.details, + })}\n`, + ); + process.exitCode = 1; + } +} diff --git a/plugins/makelore-project-scaffold/tests/scaffold.test.mjs b/plugins/makelore-project-scaffold/tests/scaffold.test.mjs new file mode 100644 index 0000000..94a837b --- /dev/null +++ b/plugins/makelore-project-scaffold/tests/scaffold.test.mjs @@ -0,0 +1,289 @@ +import assert from 'node:assert/strict'; +import { + cp, + mkdtemp, + mkdir, + open, + readFile, + readdir, + rm, + rmdir, + unlink, + writeFile, +} from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import test from 'node:test'; + +const pluginRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const scaffoldScript = path.join( + pluginRoot, + 'skills', + 'makelore-project-scaffold', + 'scripts', + 'scaffold.mjs', +); +const { writeScaffold } = await import(pathToFileURL(scaffoldScript).href); + +const expectedFiles = [ + 'index.html', + 'package-lock.json', + 'package.json', + 'src/main.js', + 'src/style.css', + 'vite.config.js', +]; + +async function initializeProject(root, projectType) { + await mkdir(path.join(root, '.makelore')); + await mkdir(path.join(root, 'knowledge')); + const config = `${JSON.stringify({ + schemaVersion: 2, + projectId: 'scaffold-test', + projectType, + }, null, 2)}\n`; + await writeFile(path.join(root, '.makelore', 'project.json'), config, 'utf8'); + return config; +} + +async function createProject(projectType) { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-')); + return { root, config: await initializeProject(root, projectType) }; +} + +function runScaffold(projectRoot, scriptPath = scaffoldScript) { + const result = spawnSync( + process.execPath, + [scriptPath, '--project-root', projectRoot], + { encoding: 'utf8' }, + ); + return { + ...result, + payload: JSON.parse((result.status === 0 ? result.stdout : result.stderr).trim()), + }; +} + +async function listTree(root, directory = root) { + const result = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const absolutePath = path.join(directory, entry.name); + const relativePath = path.relative(root, absolutePath).split(path.sep).join('/'); + if (entry.isDirectory()) { + result.push(`${relativePath}/`, ...await listTree(root, absolutePath)); + } else { + result.push(relativePath); + } + } + return result.sort(); +} + +for (const sourceProjectType of ['interactive_ai_app', 'mini_game', 'mini_program']) { + test(`creates the unified scaffold for ${sourceProjectType} without changing project metadata`, async () => { + const { root, config } = await createProject(sourceProjectType); + try { + await writeFile(path.join(root, 'notes.txt'), 'keep me', 'utf8'); + const result = runScaffold(root); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, ''); + assert.deepEqual(result.payload, { + schemaVersion: 1, + ok: true, + status: 'created', + projectType: 'interactive_ai_app', + createdFiles: expectedFiles, + }); + assert.equal( + await readFile(path.join(root, '.makelore', 'project.json'), 'utf8'), + config, + ); + const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')); + const packageLock = JSON.parse(await readFile(path.join(root, 'package-lock.json'), 'utf8')); + assert.equal(packageJson.packageManager, 'npm@11.6.2'); + assert.equal(packageJson.devDependencies.vite, '7.3.1'); + assert.equal(packageLock.lockfileVersion, 3); + assert.match(await readFile(path.join(root, 'vite.config.js'), 'utf8'), /base:\s*['"]\.\/['"]/u); + assert.equal(await readFile(path.join(root, 'notes.txt'), 'utf8'), 'keep me'); + for (const relativePath of expectedFiles) { + assert.ok((await readFile(path.join(root, ...relativePath.split('/')))).length > 0); + } + assert.match(await readFile(path.join(root, 'index.html'), 'utf8'), /src="\.\/src\/main\.js"/u); + assert.match(await readFile(path.join(root, 'src', 'main.js'), 'utf8'), /import '\.\/style\.css'/u); + assert.deepEqual(await listTree(root), [ + '.makelore/', + '.makelore/project.json', + 'knowledge/', + 'notes.txt', + 'src/', + ...expectedFiles, + ].sort()); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +} + +test('reports every existing target conflict and writes nothing else', async () => { + const { root } = await createProject('interactive_ai_app'); + try { + await writeFile(path.join(root, 'index.html'), 'mine', 'utf8'); + await writeFile(path.join(root, 'package.json'), 'mine', 'utf8'); + + const result = runScaffold(root); + assert.equal(result.status, 1); + assert.equal(result.stdout, ''); + assert.deepEqual(result.payload, { + schemaVersion: 1, + ok: false, + code: 'TARGET_CONFLICT', + message: '目标路径已存在或必需的父路径不是目录,未写入任何文件。', + paths: ['index.html', 'package.json'], + }); + assert.equal(await readFile(path.join(root, 'index.html'), 'utf8'), 'mine'); + assert.equal(await readFile(path.join(root, 'package.json'), 'utf8'), 'mine'); + assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'index.html', 'knowledge', 'package.json']); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('reports a blocking parent path as a target conflict', async () => { + const { root } = await createProject('interactive_ai_app'); + try { + await writeFile(path.join(root, 'src'), 'mine', 'utf8'); + const result = runScaffold(root); + assert.equal(result.status, 1); + assert.equal(result.payload.code, 'TARGET_CONFLICT'); + assert.deepEqual(result.payload.paths, ['src']); + assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'knowledge', 'src']); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('rejects custom projects without creating scaffold files', async () => { + const { root } = await createProject('custom'); + try { + const result = runScaffold(root); + assert.equal(result.status, 1); + assert.equal(result.payload.code, 'PROJECT_TYPE_UNSUPPORTED'); + assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'knowledge']); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('rejects a missing project config without creating scaffold files', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-')); + try { + const result = runScaffold(root); + assert.equal(result.status, 1); + assert.equal(result.payload.code, 'PROJECT_CONFIG_MISSING'); + assert.deepEqual(await readdir(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('rejects invalid project metadata without creating scaffold files', async () => { + const { root } = await createProject('interactive_ai_app'); + try { + await writeFile(path.join(root, '.makelore', 'project.json'), '{broken', 'utf8'); + const result = runScaffold(root); + assert.equal(result.status, 1); + assert.equal(result.payload.code, 'PROJECT_CONFIG_INVALID'); + assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'knowledge']); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('rejects invalid CLI arguments with a single stderr JSON object', () => { + const result = spawnSync(process.execPath, [scaffoldScript], { encoding: 'utf8' }); + assert.equal(result.status, 1); + assert.equal(result.stdout, ''); + assert.deepEqual(JSON.parse(result.stderr.trim()), { + schemaVersion: 1, + ok: false, + code: 'USAGE_INVALID', + message: '脚本参数无效,必须提供 --project-root 。', + }); +}); + +test('reports a missing template before writing any target', async () => { + const outer = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-template-')); + try { + const projectRoot = path.join(outer, 'project'); + const copiedSkill = path.join(outer, 'skill'); + await mkdir(projectRoot); + await initializeProject(projectRoot, 'interactive_ai_app'); + await cp(path.dirname(path.dirname(scaffoldScript)), copiedSkill, { recursive: true }); + await rm(path.join(copiedSkill, 'assets', 'templates', 'interactive_ai_app', 'main.js')); + + const result = runScaffold(projectRoot, path.join(copiedSkill, 'scripts', 'scaffold.mjs')); + assert.equal(result.status, 1); + assert.equal(result.payload.code, 'TEMPLATE_UNAVAILABLE'); + assert.deepEqual(result.payload.paths, ['src/main.js']); + assert.deepEqual(await listTree(projectRoot), [ + '.makelore/', + '.makelore/project.json', + 'knowledge/', + ]); + } finally { + await rm(outer, { recursive: true, force: true }); + } +}); + +test('runs when both the installed Skill path and project path contain spaces and Chinese', async () => { + const outer = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-paths-')); + try { + const projectRoot = path.join(outer, '项目 空格'); + const copiedSkill = path.join(outer, '插件 安装', '项目 初始化'); + await mkdir(projectRoot); + await mkdir(path.dirname(copiedSkill), { recursive: true }); + await initializeProject(projectRoot, 'interactive_ai_app'); + await cp(path.dirname(path.dirname(scaffoldScript)), copiedSkill, { recursive: true }); + + const result = runScaffold(projectRoot, path.join(copiedSkill, 'scripts', 'scaffold.mjs')); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(result.payload.createdFiles, expectedFiles); + } finally { + await rm(outer, { recursive: true, force: true }); + } +}); + +test('rolls back files and directories created before a handled write failure', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-rollback-')); + let openCalls = 0; + try { + await assert.rejects( + writeScaffold( + root, + [ + { targetPath: 'first.txt', contents: Buffer.from('first') }, + { targetPath: 'nested/second.txt', contents: Buffer.from('second') }, + ], + ['nested'], + { + mkdir, + open: async (...args) => { + openCalls += 1; + if (openCalls === 2) throw new Error('injected write failure'); + return await open(...args); + }, + rmdir, + unlink, + }, + ), + (error) => { + assert.equal(error.code, 'WRITE_FAILED'); + assert.equal(error.details.paths, undefined); + return true; + }, + ); + assert.deepEqual(await readdir(root), []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/shared/project-config.ts b/shared/project-config.ts index f657f92..33a2611 100644 --- a/shared/project-config.ts +++ b/shared/project-config.ts @@ -1,13 +1,23 @@ 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 const PROJECT_TYPES = ['interactive_ai_app', 'custom'] as const; export type ProjectType = (typeof PROJECT_TYPES)[number]; +const LEGACY_INTERACTIVE_PROJECT_TYPES = new Set(['mini_game', 'mini_program']); + export function isProjectType(value: unknown): value is ProjectType { return typeof value === 'string' && PROJECT_TYPES.includes(value as ProjectType); } +export function normalizeProjectType(value: unknown): ProjectType | undefined { + if (isProjectType(value)) return value; + if (typeof value === 'string' && LEGACY_INTERACTIVE_PROJECT_TYPES.has(value)) { + return 'interactive_ai_app'; + } + return undefined; +} + export type ProjectAgentResponsibility = { mission: string; owns: string[]; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b0e8f44..6637de7 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -167,7 +167,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi const [newProjectName, setNewProjectName] = useState(''); const [newProjectSelectedPath, setNewProjectSelectedPath] = useState(''); const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState('use-selected-directory'); - const [newProjectType, setNewProjectType] = useState('mini_game'); + const [newProjectType, setNewProjectType] = useState('interactive_ai_app'); const [newProjectIdentityKind, setNewProjectIdentityKind] = useState('create'); const [newProjectIdentityProjectId, setNewProjectIdentityProjectId] = useState(''); const [createProjectError, setCreateProjectError] = useState(null); @@ -399,7 +399,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi setNewProjectName(''); setNewProjectSelectedPath(''); setNewProjectDirectoryMode('use-selected-directory'); - setNewProjectType('mini_game'); + setNewProjectType('interactive_ai_app'); setNewProjectIdentityKind('create'); setNewProjectIdentityProjectId(''); setCreateProjectError(null); @@ -409,7 +409,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi const closeCreateProject = () => { if (creatingProject) return; setCreateDialogOpen(false); - setNewProjectType('mini_game'); + setNewProjectType('interactive_ai_app'); setNewProjectIdentityKind('create'); setNewProjectIdentityProjectId(''); setCreateProjectError(null); @@ -856,10 +856,9 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
项目类型 -
+
{([ - ['mini_game', '小游戏', '适合实时互动、Canvas 和关卡玩法;创建后可直接提交审核。'], - ['mini_program', '小程序', '适合页面、表单和轻量工具;创建后可直接提交审核。'], + ['interactive_ai_app', '交互式 AI 应用', '创建可交互、可发布的 AI 应用项目;进入对话后可用项目初始化 Skill 生成工程骨架。'], ['custom', '自定义项目', '只创建项目空间,不配置默认发布方式。'], ] as const).map(([value, title, description]) => (
) :

当前没有可显示的能力投影。

} {detail?.permissions.length ? ( diff --git a/src/pages/ProjectConfiguration/index.tsx b/src/pages/ProjectConfiguration/index.tsx index 6dcbdcc..1e86629 100644 --- a/src/pages/ProjectConfiguration/index.tsx +++ b/src/pages/ProjectConfiguration/index.tsx @@ -460,7 +460,7 @@ export function ProjectConfiguration() {
- {draft.projectType === 'mini_game' || draft.projectType === 'mini_program' ? ( + {draft.projectType === 'interactive_ai_app' ? ( { await page.getByTestId('ai-module-option-programming').click(); await expect(page.getByTestId('main-layout')).toBeVisible(); 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: '交互式 AI 应用' })).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: '小游戏' })).toHaveCount(0); + await expect(page.getByRole('radio', { name: '小程序' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked(); await page.getByRole('button', { name: '选择路径' }).click(); await expect(page.getByLabel('项目路径')).toHaveValue(parentPath); @@ -76,7 +74,7 @@ test.describe('Project configuration skills', () => { const now = new Date().toISOString(); await mkdir(path.join(parentPath, '.makelore'), { recursive: true }); await writeFile(path.join(parentPath, '.makelore', 'project.json'), JSON.stringify({ - ...createCodingProjectConfigV2(now, 'mini_game'), + ...createCodingProjectConfigV2(now, 'interactive_ai_app'), initialized: true, agents: [{ id: 'e2e-agent', diff --git a/tests/fixtures/fake-pi-rpc-child.mjs b/tests/fixtures/fake-pi-rpc-child.mjs index 7dc7583..688a858 100644 --- a/tests/fixtures/fake-pi-rpc-child.mjs +++ b/tests/fixtures/fake-pi-rpc-child.mjs @@ -80,6 +80,12 @@ function handle(command) { writeRecord(response(command, { counts })); return; } + if (command.type === 'environment') { + writeRecord(response(command, { + makeloreNodeExecutable: process.env.MAKELORE_NODE_EXECUTABLE ?? null, + })); + return; + } if (command.type === 'stderr_secret') { const secret = process.env.FAKE_PI_SECRET ?? 'missing-secret'; process.stderr.write(`${'diagnostic '.repeat(50)}\n`); diff --git a/tests/unit/coding-project-identity.test.ts b/tests/unit/coding-project-identity.test.ts index f20bd7b..03d7711 100644 --- a/tests/unit/coding-project-identity.test.ts +++ b/tests/unit/coding-project-identity.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node -import { copyFile, mkdir, mkdtemp, realpath, rename, rm, stat } from 'node:fs/promises'; +import { copyFile, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -45,6 +45,45 @@ function makeStore(localId = 'local-project-id') { } describe('coding project durable identity', () => { + it.each(['interactive_ai_app', 'custom'] as const)( + 'creates only project-owned metadata for %s projects', + async (projectType) => { + const projectPath = await makeRoot(); + const projects = new CodingProjectService(makeStore(), { + createProjectId: () => PROJECT_ID, + }); + + const created = await projects.createProject({ + projectPath, + projectType, + identity: { kind: 'create' }, + }); + + expect(created.config.projectType).toBe(projectType); + expect((await readdir(projectPath)).sort()).toEqual(['.makelore', 'knowledge']); + }, + ); + + it.each(['mini_game', 'mini_program'] as const)( + 'reads legacy %s metadata as the canonical interactive type without rewriting it', + async (legacyProjectType) => { + const projectPath = await makeRoot(); + await mkdir(path.join(projectPath, '.makelore'), { recursive: true }); + const configPath = path.join(projectPath, '.makelore', 'project.json'); + const rawConfig = `${JSON.stringify({ + ...createCodingProjectConfigV2(CREATED, 'interactive_ai_app'), + projectType: legacyProjectType, + }, null, 2)}\n`; + await writeFile(configPath, rawConfig, 'utf8'); + + await expect(readCodingProjectConfigV2(projectPath)).resolves.toMatchObject({ + status: 'valid', + config: { projectType: 'interactive_ai_app' }, + }); + await expect(readFile(configPath, 'utf8')).resolves.toBe(rawConfig); + }, + ); + it('preserves canonical identity and rejects noncanonical values without backfill', () => { const initialized = createCodingProjectConfigV2(CREATED, 'custom', PROJECT_ID); expect(initialized.projectId).toBe(PROJECT_ID); diff --git a/tests/unit/device-package-manager.test.ts b/tests/unit/device-package-manager.test.ts index de370e0..af7ded1 100644 --- a/tests/unit/device-package-manager.test.ts +++ b/tests/unit/device-package-manager.test.ts @@ -85,6 +85,42 @@ describe('DevicePackageManager', () => { expect(changed).toHaveBeenCalledTimes(1); }); + it('discloses executable Skill scripts and preserves their bundled resources', async () => { + const root = await temporaryRoot('script-skill'); + const source = await looseSkill(root, 'script-skill'); + await mkdir(path.join(source, 'scripts'), { recursive: true }); + await mkdir(path.join(source, 'assets'), { recursive: true }); + await mkdir(path.join(source, 'references'), { recursive: true }); + await writeFile(path.join(source, 'scripts/scaffold.mjs'), 'console.log("scaffold");\n'); + await writeFile(path.join(source, 'assets/template.txt'), 'template\n'); + await writeFile(path.join(source, 'references/submission.md'), 'requirements\n'); + const manager = new DevicePackageManager({ + rootDir: path.join(root, 'store'), + now: () => NOW, + createId: () => 'plan-script-skill', + }); + + const preview = await manager.prepare(source, 'turn-prepare'); + + expect(preview).toMatchObject({ + kind: 'skill-only', + includesExecutableCode: true, + extensionEntries: [], + }); + expect(preview.warnings.join(' ')).toContain('Skill 脚本'); + expect(preview.warnings.join(' ')).toContain('文件、网络和进程权限'); + + const installed = await manager.commit(preview.planId, true, 'turn-confirm'); + expect(installed.packages[0]).toMatchObject({ confirmedExecutableCode: true }); + const [resource] = (await manager.resolveEnabledResources()).skillEntries; + expect(await readFile(path.join(resource!.packageRoot, 'scripts/scaffold.mjs'), 'utf8')) + .toContain('scaffold'); + expect(await readFile(path.join(resource!.packageRoot, 'assets/template.txt'), 'utf8')) + .toBe('template\n'); + expect(await readFile(path.join(resource!.packageRoot, 'references/submission.md'), 'utf8')) + .toBe('requirements\n'); + }); + it('recognizes Pi extension and mixed package manifests and shows the desktop-permission warning', async () => { const root = await temporaryRoot('pi-manifests'); const source = path.join(root, 'mixed-package'); diff --git a/tests/unit/pi-agent-server-process-real.test.ts b/tests/unit/pi-agent-server-process-real.test.ts index 79f18f5..b0bb251 100644 --- a/tests/unit/pi-agent-server-process-real.test.ts +++ b/tests/unit/pi-agent-server-process-real.test.ts @@ -5,6 +5,7 @@ import { createServer, type ServerResponse } from 'node:http'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import { afterEach, describe, expect, it } from 'vitest'; import { PiAgentServerProcess } from '../../electron/coding-runtime/pi/agent-server-process'; import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host'; @@ -196,6 +197,33 @@ afterEach(async () => { }); describe('Pi Agent Server real process', () => { + it('exposes its executable to Skill scripts', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-environment-')); + roots.push(root); + const configDir = path.join(root, 'config'); + const serverPath = path.join(root, 'pi-agent-server-environment.mjs'); + await mkdir(configDir, { recursive: true }); + await writeFile(serverPath, [ + "if (process.env.MAKELORE_NODE_EXECUTABLE !== process.execPath) {", + " throw new Error('MAKELORE_NODE_EXECUTABLE does not match the Agent Server executable');", + '}', + `await import(${JSON.stringify(pathToFileURL(path.resolve('resources/pi-agent-server.mjs')).href)});`, + '', + ].join('\n')); + const server = new PiAgentServerProcess({ + executablePath: process.execPath, + serverPath, + runtimeRoot: path.resolve('node_modules/@earendil-works/pi-coding-agent'), + configDir, + }); + + try { + await expect(server.start()).resolves.toBeUndefined(); + } finally { + await server.stop().catch(() => undefined); + } + }, 10_000); + it('resolves runtime packages when the server resource is outside the project module tree', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-packaged-layout-')); roots.push(root); diff --git a/tests/unit/pi-rpc-foundation.test.ts b/tests/unit/pi-rpc-foundation.test.ts index 4f555e7..974bdc2 100644 --- a/tests/unit/pi-rpc-foundation.test.ts +++ b/tests/unit/pi-rpc-foundation.test.ts @@ -469,7 +469,11 @@ describe('Pi worker process', () => { it('inherits only the worker-safe environment allowlist', () => { const env = buildPiWorkerEnvironment( 'D:\\managed-pi', - { MAKELore_PI_SELECTED_API_KEY: 'selected-secret' }, + 'D:\\Makelore App\\Makelore.exe', + { + MAKELore_PI_SELECTED_API_KEY: 'selected-secret', + MAKELORE_NODE_EXECUTABLE: 'D:\\spoofed-node.exe', + }, { PATH: 'D:\\tools', OPENAI_API_KEY: 'unrelated-openai-secret', @@ -484,12 +488,24 @@ describe('Pi worker process', () => { PI_OFFLINE: '1', PI_TELEMETRY: '0', ELECTRON_RUN_AS_NODE: '1', + MAKELORE_NODE_EXECUTABLE: 'D:\\Makelore App\\Makelore.exe', }); expect(env).not.toHaveProperty('OPENAI_API_KEY'); expect(env).not.toHaveProperty('ANTHROPIC_API_KEY'); expect(env).not.toHaveProperty('CUSTOM_APPLICATION_SECRET'); }); + it('exposes the direct worker executable to Skill scripts', async () => { + const worker = await makeWorker({ + env: { MAKELORE_NODE_EXECUTABLE: 'spoofed-node' }, + }); + + await expect(worker.request<{ makeloreNodeExecutable: string }>({ type: 'environment' })) + .resolves.toMatchObject({ + data: { makeloreNodeExecutable: process.execPath }, + }); + }); + it('refuses to put a selected worker credential in argv', async () => { const secret = 'argv-secret-value'; await expect(makeWorker({ diff --git a/tests/unit/plugins-page.test.tsx b/tests/unit/plugins-page.test.tsx index 6707dfa..dced718 100644 --- a/tests/unit/plugins-page.test.tsx +++ b/tests/unit/plugins-page.test.tsx @@ -471,6 +471,33 @@ describe('PluginsView', () => { }); }); + it('describes executable Skill scripts without calling them Pi extensions', () => { + const scriptSkill: PluginWorkspaceItem = { + ...localItem, + key: 'local:makelore-project-scaffold', + pluginId: 'makelore-project-scaffold', + packageId: 'makelore-project-scaffold', + title: 'MakeLore 项目初始化', + local: { + ...localItem.local!, + packageId: 'makelore-project-scaffold', + displayName: 'MakeLore 项目初始化', + kind: 'skill-only', + skillEntries: [{ + id: 'makelore-project-scaffold', + entryPath: 'skills/makelore-project-scaffold/SKILL.md', + }], + extensionEntries: [], + }, + }; + + render(); + + const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' }); + expect(dialog).toHaveTextContent('包含可执行 Skill 脚本'); + expect(dialog).not.toHaveTextContent('包含可执行 Pi extension'); + }); + it('shows cached, suspended, retired, and retained-device reason copy with an explicit sign-in action', () => { const unavailableItem: PluginWorkspaceItem = { ...officialItem, diff --git a/tests/unit/project-packager.test.ts b/tests/unit/project-packager.test.ts index 7fe2db8..838a88f 100644 --- a/tests/unit/project-packager.test.ts +++ b/tests/unit/project-packager.test.ts @@ -42,7 +42,7 @@ const AdmZip = require('adm-zip') as typeof import('adm-zip'); const tempDirectories: string[] = []; -async function createProject(projectType: ProjectType = 'mini_game'): Promise { +async function createProject(projectType: ProjectType = 'interactive_ai_app'): Promise { const projectPath = await mkdtemp(join(tmpdir(), 'makelore-project-packager-')); tempDirectories.push(projectPath); await mkdir(join(projectPath, 'src'), { recursive: true }); @@ -180,7 +180,7 @@ describe('project packager', () => { ]); expect(archive.readAsText('niancode.yml')).toBe( 'schema_version: 1\n' - + 'project_type: mini_game\n' + + 'project_type: interactive_ai_app\n' + 'kind: web\n' + 'runtime: static\n' + 'build:\n' @@ -188,22 +188,34 @@ describe('project packager', () => { + ' package_manager: npm\n' + ' entry: index.html\n', ); - expect(first.manifest.project_type).toBe('mini_game'); + expect(first.manifest.project_type).toBe('interactive_ai_app'); 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'); + it.each(['mini_game', 'mini_program'] as const)( + 'normalizes a legacy %s project to the canonical manifest type', + async (legacyProjectType) => { + const projectPath = await createProject(); + await writeFile( + join(projectPath, '.makelore', 'project.json'), + JSON.stringify({ + ...createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', 'interactive_ai_app'), + projectType: legacyProjectType, + }), + 'utf8', + ); + const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-')); + tempDirectories.push(outputDirectory); + const archivePath = join(outputDirectory, 'project.zip'); - const summary = await createStaticProjectPackage({ projectPath, archivePath }); + 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'); - }); + expect(summary.manifest.project_type).toBe('interactive_ai_app'); + expect(new AdmZip(archivePath).readAsText('niancode.yml')) + .toContain('project_type: interactive_ai_app\n'); + }, + ); it('rejects custom projects before validating Vite source files', async () => { const projectPath = await mkdtemp(join(tmpdir(), 'makelore-custom-packager-')); @@ -222,7 +234,7 @@ describe('project packager', () => { archivePath: join(outputDirectory, 'project.zip'), })).rejects.toMatchObject({ code: 'PROJECT_TYPE_UNPUBLISHABLE', - message: '自定义项目暂未配置发布方式,请新建小游戏或小程序项目', + message: '自定义项目暂未配置发布方式,请新建交互式 AI 应用项目', }); }); diff --git a/tests/unit/project-publish-action.test.tsx b/tests/unit/project-publish-action.test.tsx index caef5e9..cf63175 100644 --- a/tests/unit/project-publish-action.test.tsx +++ b/tests/unit/project-publish-action.test.tsx @@ -110,7 +110,7 @@ describe('ProjectPublishAction', () => { it('creates a new project with a cover DTO and locks after Builder succeeds', async () => { fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded')); - render(); + render(); await submitProjectMetadata(); expect(screen.getByRole('button', { name: '正在等待云端检查…' })).toBeDisabled(); @@ -130,7 +130,7 @@ describe('ProjectPublishAction', () => { cover_url: null, creator_name: '小明', creator_age: 12, - category: 'mini_game', + category: 'interactive_ai_app', age_band: null, difficulty: null, }, @@ -153,7 +153,7 @@ describe('ProjectPublishAction', () => { }); it('blocks first submission until a cover is selected', async () => { - render(); + render(); await submitProjectMetadata({ includeCover: false }); @@ -164,7 +164,7 @@ describe('ProjectPublishAction', () => { it('rejects unsupported and oversized cover files before confirmation', async () => { fetchCurrentWorksProjectStatusMock.mockRejectedValue(Object.assign(new Error('missing'), { statusCode: 404 })); - render(); + render(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); await Promise.resolve(); @@ -185,7 +185,7 @@ describe('ProjectPublishAction', () => { fetchCurrentWorksProjectStatusMock.mockResolvedValue( projectStatus('failed', 'BROWSER_SMOKE_FAILED'), ); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -204,7 +204,7 @@ describe('ProjectPublishAction', () => { fetchCurrentWorksProjectStatusMock.mockRejectedValue( new Error('socket closed at /srv/private/status.py'), ); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -227,7 +227,7 @@ describe('ProjectPublishAction', () => { }, }); fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded')); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -248,7 +248,7 @@ describe('ProjectPublishAction', () => { const status = projectStatus('queued'); status.latest_version.id = 'another-version'; fetchCurrentWorksProjectStatusMock.mockResolvedValue(status); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -268,7 +268,7 @@ describe('ProjectPublishAction', () => { new Error('Traceback: failed at /srv/private/package.ts token=secret'), { code: 'INTERNAL_DATABASE_FAILURE', statusCode: 503 }, )); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -285,7 +285,7 @@ describe('ProjectPublishAction', () => { new Error('private current metadata token=secret'), { code: 'PROJECT_METADATA_CONFLICT', statusCode: 409 }, )); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -302,7 +302,7 @@ describe('ProjectPublishAction', () => { new Error('D:/repo/space-cleaner/package-lock.json does not exist'), { code: 'PROJECT_FILE_MISSING' }, )); - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -317,7 +317,7 @@ describe('ProjectPublishAction', () => { it('keeps generated metadata within the server limits', async () => { const longName = `作品-${'x'.repeat(250)}`; - render(); + render(); await submitProjectMetadata(); await flushSubmission(); @@ -328,7 +328,7 @@ describe('ProjectPublishAction', () => { expect(input.project.summary).toBe('这是一个太空清洁小游戏。'); expect(input.project.creator_name).toBe('小明'); expect(input.project.creator_age).toBe(12); - expect(input.project.category).toBe('mini_program'); + expect(input.project.category).toBe('interactive_ai_app'); }); it('submits an existing draft as version-only without editable metadata or replacement cover', async () => { @@ -339,10 +339,10 @@ describe('ProjectPublishAction', () => { cover_url: 'coverimage/draft.jpg', creator_name: '草稿作者', creator_age: 10, - category: 'mini_game', + category: 'interactive_ai_app', }); fetchCurrentWorksProjectStatusMock.mockResolvedValue(draftStatus); - render(); + render(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); @@ -384,12 +384,12 @@ describe('ProjectPublishAction', () => { cover_url: 'coverimage/existing.jpg', creator_name: '原作者', creator_age: 11, - category: 'mini_game', + category: 'interactive_ai_app', age_band: '6-12岁', difficulty: '入门', }); fetchCurrentWorksProjectStatusMock.mockResolvedValue(publishedStatus); - render(); + render(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: '一键提交审核' })); diff --git a/tests/unit/project-release-builder.test.ts b/tests/unit/project-release-builder.test.ts index fb915e6..1ec2499 100644 --- a/tests/unit/project-release-builder.test.ts +++ b/tests/unit/project-release-builder.test.ts @@ -21,7 +21,7 @@ describe('project release builder', () => { it('builds from the exact source archive and returns the protocol-v1 canonical contract', async () => { root = await mkdtemp(join(tmpdir(), 'release-builder-test-')); await mkdir(join(root, '.makelore'), { recursive: true }); - await writeFile(join(root, '.makelore', 'project.json'), JSON.stringify(createCodingProjectConfigV2(new Date().toISOString(), 'mini_game'))); + await writeFile(join(root, '.makelore', 'project.json'), JSON.stringify(createCodingProjectConfigV2(new Date().toISOString(), 'interactive_ai_app'))); await writeFile(join(root, 'package.json'), JSON.stringify({ name: 'demo', packageManager: 'npm@11.6.2', devDependencies: { vite: '7.3.1' } })); await writeFile(join(root, 'package-lock.json'), JSON.stringify({ name: 'demo', lockfileVersion: 3, requires: true, packages: {} })); await writeFile(join(root, 'index.html'), '
source
'); diff --git a/tests/unit/project-scaffold-plugin.test.ts b/tests/unit/project-scaffold-plugin.test.ts new file mode 100644 index 0000000..89b20f7 --- /dev/null +++ b/tests/unit/project-scaffold-plugin.test.ts @@ -0,0 +1,193 @@ +// @vitest-environment node + +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { + devicePackageKind, + inspectDevicePackage, +} from '@electron/coding-packages/device-package-format'; +import { DevicePackageManager } from '@electron/coding-packages/device-package-manager'; +import { createCodingProjectMetadata } from '@electron/coding-projects/project-config'; +import { createStaticProjectPackage } from '@electron/services/project-packager'; +import type { ProjectType } from '../../shared/project-config'; + +const pluginRoot = resolve('plugins/makelore-project-scaffold'); +const scaffoldScript = join( + pluginRoot, + 'skills', + 'makelore-project-scaffold', + 'scripts', + 'scaffold.mjs', +); +const tempDirectories: string[] = []; + +function extractStringList(source: string, declaration: string): string[] { + const declarationStart = source.indexOf(`const ${declaration} =`); + expect(declarationStart).toBeGreaterThanOrEqual(0); + const listStart = source.indexOf('[', declarationStart); + const listEnd = source.indexOf(']', listStart); + expect(listStart).toBeGreaterThanOrEqual(0); + expect(listEnd).toBeGreaterThan(listStart); + return [...source.slice(listStart + 1, listEnd).matchAll(/'([^']+)'/g)] + .map((match) => match[1]); +} + +afterEach(async () => { + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +describe('MakeLore project scaffold plugin', () => { + it('is discovered as a skill-only package with executable Skill scripts', async () => { + const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8')); + const pluginManifest = JSON.parse(await readFile( + join(pluginRoot, '.codex-plugin', 'plugin.json'), + 'utf8', + )); + const inspected = await inspectDevicePackage(pluginRoot); + + expect({ name: packageJson.name, version: packageJson.version }).toEqual({ + name: pluginManifest.name, + version: pluginManifest.version, + }); + expect(pluginManifest.skills).toBe('./skills/'); + expect(devicePackageKind(inspected)).toBe('skill-only'); + expect(inspected).toMatchObject({ + packageId: 'makelore-project-scaffold', + resolvedVersion: '0.1.0', + extensionEntries: [], + hasSkillScripts: true, + ignoredLifecycleScripts: [], + skillEntries: [{ + id: 'makelore-project-scaffold', + entryPath: 'skills/makelore-project-scaffold/SKILL.md', + }], + }); + }); + + it('prepares, confirms, and installs the real package with every Skill resource', async () => { + const root = await mkdtemp(join(tmpdir(), 'makelore-scaffold-device-package-')); + tempDirectories.push(root); + const manager = new DevicePackageManager({ + rootDir: join(root, 'store'), + now: () => Date.parse('2026-09-04T00:00:00.000Z'), + createId: () => 'project-scaffold-plan', + }); + + const preview = await manager.prepare(pluginRoot, 'prepare-turn'); + expect(preview).toMatchObject({ + kind: 'skill-only', + includesExecutableCode: true, + skillEntries: [{ id: 'makelore-project-scaffold' }], + extensionEntries: [], + }); + expect(preview.warnings.join(' ')).toContain('可执行 Skill 脚本'); + await manager.commit(preview.planId, true, 'confirm-turn'); + + const [resource] = (await manager.resolveEnabledResources()).skillEntries; + expect(resource?.id).toBe('makelore-project-scaffold'); + const skillRoot = dirname(join(resource!.packageRoot, resource!.entryPath)); + expect(await readFile(join(skillRoot, 'scripts', 'scaffold.mjs'), 'utf8')) + .toContain('schemaVersion: 1'); + expect(await readFile(join(skillRoot, 'assets', 'templates', 'common', 'package-lock.json'), 'utf8')) + .toContain('"lockfileVersion": 3'); + expect(await readFile(join(skillRoot, 'references', 'submission-requirements.md'), 'utf8')) + .toContain('提交审批要求'); + }); + + it('carries the complete categorized release-readiness contract', async () => { + const skill = await readFile( + join(pluginRoot, 'skills', 'makelore-project-scaffold', 'SKILL.md'), + 'utf8', + ); + const requirements = await readFile( + join( + pluginRoot, + 'skills', + 'makelore-project-scaffold', + 'references', + 'submission-requirements.md', + ), + 'utf8', + ); + const packagerSource = await readFile('electron/services/project-packager.ts', 'utf8'); + + expect(skill).toContain('Release-readiness workflow'); + expect(skill).toContain('不能进入一键提交'); + expect(skill).toContain('可进入一键提交,仍需运行期验证'); + expect(skill).toContain('Do not infer an external-network blocker'); + + for (const heading of ['必须具备', '直接阻断', '打包排除', '运行时或平台确认']) { + expect(requirements).toContain(`**${heading}**`); + } + for (const value of [ + '.makelore/project.json', + 'schema v2', + 'interactive_ai_app', + 'mini_game', + 'mini_program', + 'custom', + 'npm ci --ignore-scripts', + 'lockfileVersion', + '2,000', + '200 MiB', + '50 MiB', + 'release.json', + 'CON', + '1280×720', + '390×844', + '30 秒', + 'HTTP/HTTPS', + '.env', + 'serviceaccount', + 'works-', + 'PNG、JPEG 或 WebP', + '1–150', + '存在但不阻断', + ]) { + expect(requirements).toContain(value); + } + + const excludedValues = [ + ...extractStringList(packagerSource, 'EXCLUDED_DIRECTORY_NAMES'), + ...extractStringList(packagerSource, 'EXCLUDED_FILE_NAMES'), + ...extractStringList(packagerSource, 'EXCLUDED_FILE_SUFFIXES'), + ]; + expect(excludedValues.length).toBeGreaterThan(50); + for (const value of excludedValues) { + expect(requirements).toContain(value); + } + }); + + it('creates an interactive AI application accepted by the existing source packager', async () => { + const projectType: ProjectType = 'interactive_ai_app'; + const projectPath = await mkdtemp(join(tmpdir(), `makelore-${projectType}-`)); + const outputPath = await mkdtemp(join(tmpdir(), 'makelore-scaffold-package-')); + tempDirectories.push(projectPath, outputPath); + await createCodingProjectMetadata(projectPath, { + projectType, + now: '2026-09-04T00:00:00.000Z', + }); + + const scaffold = spawnSync( + process.execPath, + [scaffoldScript, '--project-root', projectPath], + { encoding: 'utf8' }, + ); + expect(scaffold.status, scaffold.stderr).toBe(0); + + const packaged = await createStaticProjectPackage({ + projectPath, + archivePath: join(outputPath, 'project.zip'), + }); + expect(packaged.manifest.project_type).toBe(projectType); + expect(packaged.archiveName).toBe('project.zip'); + expect(packaged.fileCount).toBeGreaterThan(1); + expect(packaged.archiveBytes).toBeGreaterThan(0); + expect(packaged.excludedPaths).toEqual(expect.arrayContaining(['.makelore/', 'knowledge/'])); + }); +}); diff --git a/tests/unit/works-project-publish.test.ts b/tests/unit/works-project-publish.test.ts index a23cee9..a9f5a7a 100644 --- a/tests/unit/works-project-publish.test.ts +++ b/tests/unit/works-project-publish.test.ts @@ -9,7 +9,7 @@ describe('works project publish guidance', () => { ['PUBLISH_PREFLIGHT_RUNTIME_ERROR', '作品打开时发生错误', '项目预览'], ['PUBLISH_PREFLIGHT_BLANK', '作品打开后没有内容', '移动端布局'], ['PROJECT_FILE_MISSING', '项目文件不完整', '修复当前项目模板'], - ['PROJECT_TYPE_UNPUBLISHABLE', '这个项目没有配置发布方式', '新建小游戏或小程序项目'], + ['PROJECT_TYPE_UNPUBLISHABLE', '这个项目没有配置发布方式', '新建交互式 AI 应用项目'], ['DEPENDENCY_PREFETCH_FAILED', '旧版提交无法继续处理', '升级 Makelore'], ['OUTPUT_MISSING', '本次构建结果未通过平台校验', '本次构建结果'], ['RELEASE_STORE_FAILED', '平台校验任务暂未完成', '构建异常'], diff --git a/tests/unit/works-routes.test.ts b/tests/unit/works-routes.test.ts index 0be93b0..42622b9 100644 --- a/tests/unit/works-routes.test.ts +++ b/tests/unit/works-routes.test.ts @@ -29,7 +29,7 @@ function preparedRelease(projectPath: string) { sourceArchive: { path: join(projectPath, 'private-source.zip'), name: 'project.zip', bytes, summary: { archivePath: join(projectPath, 'private-source.zip'), archiveName: 'project.zip', sha256: 'a'.repeat(64), fileCount: 5, sourceBytes: 10, archiveBytes: 3, excludedCount: 0, excludedPaths: [], - manifest: { schema_version: 1, project_type: 'mini_game', kind: 'web', runtime: 'static', build: { preset: 'vite', package_manager: 'npm', entry: 'index.html' } }, + manifest: { schema_version: 1, project_type: 'interactive_ai_app', kind: 'web', runtime: 'static', build: { preset: 'vite', package_manager: 'npm', entry: 'index.html' } }, } }, builtArchive: { path: join(projectPath, 'private-built.zip'), name: 'built-project.zip', bytes: Buffer.from('built') }, distRoot: join(projectPath, 'private-dist'), @@ -92,7 +92,7 @@ async function writePublishableProject(projectPath: string): Promise { await mkdir(join(projectPath, '.makelore'), { recursive: true }); await writeFile( join(projectPath, '.makelore', 'project.json'), - JSON.stringify(createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', 'mini_game')), + JSON.stringify(createCodingProjectConfigV2('2026-08-09T00:00:00.000Z', 'interactive_ai_app')), 'utf8', ); await writeFile(join(projectPath, 'package.json'), JSON.stringify({