#!/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; } }