diff --git a/src/commands/create.ts b/src/commands/create.ts index a9d6d6a..e657199 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { checkbox, select, input } from "@inquirer/prompts"; -import { resolve, basename } from "node:path"; -import { writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { writeFile, mkdir } from "node:fs/promises"; import { resolveSource } from "../lib/config.js"; import { loadIndex } from "../lib/index-loader.js"; import { inferRepoUrl, cloneBlock, cleanupTemp } from "../lib/git.js"; @@ -15,13 +15,11 @@ interface SelectedBlock { variantLabel: string; } -const STYLES: { value: string; label: string }[] = [ - { value: "standard", label: "标准多模块 — 根 pom.xml 聚合所有积木子模块" }, - { - value: "bom", - label: "BOM 模式 — 根 pom + bom 子模块统一依赖版本管理", - }, - { value: "simple", label: "简单模式 — 不生成根 pom,积木模块平铺" }, +const APP_STYLES: { value: string; label: string }[] = [ + { value: "mvc", label: "MVC 经典三层 — controller / service / mapper" }, + { value: "ddd", label: "DDD 四层 — interfaces / application / domain / infrastructure" }, + { value: "biz-api", label: "biz/api 分离 — api 接口层 + biz 实现层" }, + { value: "simple", label: "极简 — 只生成 Application 入口类" }, ]; const AI_TOOLS: { value: string; label: string }[] = [ @@ -51,182 +49,193 @@ export function registerCreate(program: Command): void { } async function runCreate(projectName: string): Promise { - const cwd = process.cwd(); + const cwd = process.cwd(); - // 1. Load index - const source = resolveSource(); - console.log(`[1/7] 拉取积木索引...`); - const index = await loadIndex(source); + // 1. Load index + console.log("[1/8] 拉取积木索引..."); + const source = resolveSource(); + const index = await loadIndex(source); - if (index.blocks.length === 0) { - console.log("没有可用的积木。"); - return; - } + if (index.blocks.length === 0) { + console.log("没有可用的积木。"); + return; + } - // 2. Interactive prompts - // 2a. Select blocks - const chosenNames = await checkbox({ - message: "选择要集成的积木(空格选择,回车确认):", - choices: index.blocks.map((b) => ({ - name: `${b.name} — ${b.description}`, - value: b.name, - checked: true, + // 2a. Select blocks + const chosenNames = await checkbox({ + message: "选择要集成的通用模块:", + choices: index.blocks.map((b) => ({ + name: `${b.name} — ${b.description}`, + value: b.name, + checked: true, + })), + }); + + if (chosenNames.length === 0) { + console.log("未选择任何积木,已取消。"); + return; + } + + const chosenBlocks = index.blocks.filter((b) => chosenNames.includes(b.name)); + + // 2b. Resolve dependencies + const resolved = resolveDeps(chosenBlocks, index.blocks); + if (resolved.added.length > 0) { + console.log( + ` 自动补全依赖:${resolved.added.map((b) => b.name).join(", ")}` + ); + } + + // 2c. Select variants + const selected: SelectedBlock[] = []; + for (const block of resolved.all) { + let branch = block.defaultBranch; + let variantLabel = block.defaultBranch; + + if (block.variants.length > 1) { + const chosen = await select({ + message: `为 ${block.name} 选择变体:`, + choices: block.variants.map((v) => ({ + name: `${v.label} (${v.branch})`, + value: v.branch, })), + default: block.defaultBranch, }); + branch = chosen; + variantLabel = block.variants.find((v) => v.branch === chosen)?.label ?? chosen; + } - if (chosenNames.length === 0) { - console.log("未选择任何积木,已取消。"); - return; - } + selected.push({ block, branch, variantLabel }); + } - const chosenBlocks = index.blocks.filter((b) => - chosenNames.includes(b.name) - ); + // 2d. Business module style + const appStyle = await select({ + message: "选择业务模块风格:", + choices: APP_STYLES, + default: "mvc", + }); - // 2b. Resolve dependencies (auto-add missing) - const resolved = resolveDeps(chosenBlocks, index.blocks); - if (resolved.added.length > 0) { - console.log( - ` 自动补全依赖:${resolved.added.map((b) => b.name).join(", ")}` - ); - } + // 2e. Variables + const defaultPkg = `com.example.${projectName.replace(/[^a-zA-Z0-9]/g, "")}`; - // 2c. Select variant for blocks with multiple variants - const selected: SelectedBlock[] = []; - for (const block of resolved.all) { - let branch = block.defaultBranch; - let variantLabel = block.defaultBranch; + const basePackage = await input({ + message: "基础包名 (basePackage):", + default: defaultPkg, + }); - if (block.variants.length > 1) { - const chosen = await select({ - message: `为 ${block.name} 选择变体:`, - choices: block.variants.map((v) => ({ - name: `${v.label} (${v.branch})`, - value: v.branch, - })), - default: block.defaultBranch, - }); - branch = chosen; - variantLabel = - block.variants.find((v) => v.branch === chosen)?.label ?? chosen; - } + const groupId = await input({ + message: "Maven groupId:", + default: basePackage, + }); - selected.push({ block, branch, variantLabel }); - } + const javaVersion = await input({ + message: "Java 版本:", + default: "17", + }); - // 2d. Project style - const style = await select({ - message: "选择工程风格:", - choices: STYLES, - default: "standard", - }); + // 2f. AI tool + const aiTool = await select({ + message: "选择 AI 工具:", + choices: AI_TOOLS, + default: "claude", + }); - // 2e. Variables - const defaultPkg = `com.example.${projectName.replace(/[^a-zA-Z0-9]/g, "")}`; + // Summary + console.log("\n--- 配置摘要 ---"); + console.log(` 项目名: ${projectName}`); + console.log(` 通用模块: ${selected.map((s) => `${s.block.name} (${s.variantLabel})`).join(", ")}`); + console.log(` 业务模块风格: ${APP_STYLES.find((s) => s.value === appStyle)?.label}`); + console.log(` 基础包名: ${basePackage}`); + console.log(` groupId: ${groupId}`); + console.log(` Java 版本: ${javaVersion}`); + console.log(` AI 工具: ${AI_TOOLS.find((t) => t.value === aiTool)?.label}`); + console.log("-----------------\n"); - const basePackage = await input({ - message: "基础包名 (basePackage):", - default: defaultPkg, - }); + // 3. Clone blocks + console.log("[2/8] 拉取积木仓库..."); + const blockDirs: { name: string; dir: string }[] = []; - const groupId = await input({ - message: "Maven groupId:", - default: basePackage, - }); + for (const sel of selected) { + const url = inferRepoUrl(sel.block.name, sel.block.repo); + console.log(` clone ${url} (branch: ${sel.branch})...`); + const dir = await cloneBlock(url, sel.branch); + blockDirs.push({ name: sel.block.name, dir }); + } - const javaVersion = await input({ - message: "Java 版本:", - default: "17", - }); + // 4. Apply templates — common modules under demo-common/ + console.log("[3/8] 生成通用模块..."); + const targetDir = resolve(cwd, projectName); + const commonDir = resolve(targetDir, `${projectName}-common`); + const vars: Record = { + basePackage, + groupId, + projectName, + javaVersion, + }; - // 2f. AI tool - const aiTool = await select({ - message: "选择 AI 工具:", - choices: AI_TOOLS, - default: "claude", - }); + const commonModules: string[] = []; - // Summary - console.log("\n--- 配置摘要 ---"); - console.log(` 项目名: ${projectName}`); - console.log(` 积木: ${selected.map((s) => `${s.block.name} (${s.variantLabel})`).join(", ")}`); - console.log(` 工程风格: ${STYLES.find((s) => s.value === style)?.label}`); - console.log(` 基础包名: ${basePackage}`); - console.log(` groupId: ${groupId}`); - console.log(` Java 版本: ${javaVersion}`); - console.log(` AI 工具: ${AI_TOOLS.find((t) => t.value === aiTool)?.label}`); - console.log("-----------------\n"); + for (const sel of selected) { + const moduleName = `${projectName}-common-${sel.block.module}`; + const moduleDir = resolve(commonDir, moduleName); + const bd = blockDirs.find((d) => d.name === sel.block.name)!; + await applyTemplate(bd.dir, moduleDir, vars); + commonModules.push(moduleName); + } - // 3. Clone blocks - console.log("[2/7] 拉取积木仓库..."); - const blockDirs: { name: string; dir: string }[] = []; + // 5. Generate demo-common/pom.xml + console.log("[4/8] 生成 common 父 POM..."); + await mkdir(commonDir, { recursive: true }); + await writeFile( + resolve(commonDir, "pom.xml"), + buildCommonParentPom(projectName, groupId, javaVersion, commonModules), + "utf-8" + ); - for (const sel of selected) { - const url = inferRepoUrl(sel.block.name, sel.block.repo); - console.log(` clone ${url} (branch: ${sel.branch})...`); - const dir = await cloneBlock(url, sel.branch); - blockDirs.push({ name: sel.block.name, dir }); - } + // 6. Generate demo-boot/ + console.log("[5/8] 生成业务模块..."); + const bootDir = resolve(targetDir, `${projectName}-boot`); + await generateBootModule(bootDir, projectName, groupId, basePackage, javaVersion, appStyle, commonModules); - // 4. Apply templates - console.log("[3/7] 生成项目源码..."); - const targetDir = resolve(cwd, projectName); - const vars: Record = { - basePackage, - groupId, - projectName, - javaVersion, - }; + // 7. Generate AI rules + console.log("[6/8] 生成 AI 规则文件..."); + await compileAiRules(blockDirs, targetDir, aiTool); - for (const bd of blockDirs) { - await applyTemplate(bd.dir, targetDir, vars); - } + // 8. Generate root pom.xml + console.log("[7/8] 生成根 pom.xml..."); + await writeFile( + resolve(targetDir, "pom.xml"), + buildRootPom(projectName, groupId, javaVersion), + "utf-8" + ); - // 5. Generate AI rules - console.log("[4/7] 生成 AI 规则文件..."); - await compileAiRules(blockDirs, targetDir, aiTool); + // 9. Generate brick.json + console.log("[8/8] 生成 brick.json..."); + const brickJson: BrickJson = { + projectName, + basePackage, + javaVersion, + aiTool, + style: appStyle, + blocks: selected.map((s) => ({ + name: s.block.name, + variant: s.variantLabel, + version: s.block.version, + })), + }; + await writeFile( + resolve(targetDir, "brick.json"), + JSON.stringify(brickJson, null, 2), + "utf-8" + ); - // 6. Generate root pom.xml - console.log("[5/7] 生成根 pom.xml..."); - if (style !== "simple") { - const rootPom = buildRootPom( - projectName, - groupId, - javaVersion, - selected, - style - ); - await writeFile(resolve(targetDir, "pom.xml"), rootPom, "utf-8"); - } + // Cleanup + for (const bd of blockDirs) { + await cleanupTemp(bd.dir); + } - // 7. Generate brick.json - console.log("[6/7] 生成 brick.json..."); - const brickJson: BrickJson = { - projectName, - basePackage, - javaVersion, - aiTool, - style, - blocks: selected.map((s) => ({ - name: s.block.name, - variant: s.variantLabel, - version: s.block.version, - })), - }; - await writeFile( - resolve(targetDir, "brick.json"), - JSON.stringify(brickJson, null, 2), - "utf-8" - ); - - // 8. Cleanup temp dirs - console.log("[7/7] 清理临时文件..."); - for (const bd of blockDirs) { - await cleanupTemp(bd.dir); - } - - console.log(`\n项目已创建:${targetDir}`); - console.log("Done!"); + console.log(`\n项目已创建:${targetDir}`); + console.log("Done!"); } function resolveDeps( @@ -237,7 +246,6 @@ function resolveDeps( const result = new Map(chosen.map((b) => [b.name, b])); const added: Block[] = []; - // Iteratively resolve dependencies let changed = true; while (changed) { changed = false; @@ -262,60 +270,13 @@ function resolveDeps( return { all: [...result.values()], added }; } +// ── POM generators ────────────────────────────────────────── + function buildRootPom( projectName: string, groupId: string, - javaVersion: string, - selected: SelectedBlock[], - style: string + javaVersion: string ): string { - const modules = selected - .map((s) => ` ${projectName}-common-${s.block.module}`) - .join("\n"); - - if (style === "bom") { - return [ - '', - '', - ' 4.0.0', - "", - ` ${groupId}`, - ` ${projectName}`, - " 0.0.0", - " pom", - "", - ` ${projectName}`, - "", - " ", - ` ${javaVersion}`, - ` UTF-8`, - ` 0.0.0`, - " ", - "", - " ", - " ", - ...selected.map( - (s) => - ` \n` + - ` ${groupId}\n` + - ` ${projectName}-common-${s.block.module}\n` + - ` ${"$"}{brick.version}\n` + - ` ` - ), - " ", - " ", - "", - " ", - ` ${projectName}-common-bom`, - modules, - " ", - "", - ].join("\n"); - } - - // standard return [ '', '", ` ${javaVersion}`, - ` UTF-8`, + ' UTF-8', " ", "", " ", - modules, + ` ${projectName}-common`, + ` ${projectName}-boot`, " ", "", ].join("\n"); } + +function buildCommonParentPom( + projectName: string, + groupId: string, + javaVersion: string, + modules: string[] +): string { + const moduleEntries = modules + .map((m) => ` ${m}`) + .join("\n"); + + return [ + '', + '', + ' 4.0.0', + "", + " ", + " org.springframework.boot", + " spring-boot-starter-parent", + " 3.4.0", + " ", + " ", + "", + ` ${groupId}`, + ` ${projectName}-common`, + " 0.0.0", + " pom", + "", + " ", + ` ${javaVersion}`, + " ", + "", + " ", + moduleEntries, + " ", + "", + ].join("\n"); +} + +function buildBootPom( + projectName: string, + groupId: string, + javaVersion: string, + commonModules: string[] +): string { + const deps = commonModules + .map( + (m) => + " \n" + + ` ${groupId}\n` + + ` ${m}\n` + + " 0.0.0\n" + + " " + ) + .join("\n"); + + return [ + '', + '', + ' 4.0.0', + "", + " ", + " org.springframework.boot", + " spring-boot-starter-parent", + " 3.4.0", + " ", + " ", + "", + ` ${groupId}`, + ` ${projectName}-boot`, + " 0.0.0", + " jar", + "", + " ", + ` ${javaVersion}`, + " ", + "", + " ", + " ", + " org.springframework.boot", + " spring-boot-starter-web", + " ", + deps, + " ", + "", + " ", + " ", + " ", + " org.springframework.boot", + " spring-boot-maven-plugin", + " ", + " ", + " ", + "", + ].join("\n"); +} + +// ── Business module generator ──────────────────────────────── + +async function generateBootModule( + bootDir: string, + projectName: string, + groupId: string, + basePackage: string, + javaVersion: string, + appStyle: string, + commonModules: string[] +): Promise { + const pkgPath = basePackage.replace(/\./g, "/"); + const srcDir = resolve(bootDir, "src", "main", "java", pkgPath); + + await mkdir(srcDir, { recursive: true }); + + // Generate style-specific packages (placeholder .gitkeep) + const styleDirs: string[] = []; + switch (appStyle) { + case "mvc": + styleDirs.push("controller", "service", "mapper"); + break; + case "ddd": + styleDirs.push("interfaces", "application", "domain", "infrastructure"); + break; + case "biz-api": + styleDirs.push("api", "biz"); + break; + // simple: no extra dirs + } + + for (const d of styleDirs) { + await mkdir(resolve(srcDir, d), { recursive: true }); + } + + // Generate Application.java + const appName = projectName.charAt(0).toUpperCase() + projectName.slice(1) + "Application"; + await writeFile( + resolve(srcDir, `${appName}.java`), + [ + `package ${basePackage};`, + "", + "import org.springframework.boot.SpringApplication;", + "import org.springframework.boot.autoconfigure.SpringBootApplication;", + "", + "@SpringBootApplication", + `public class ${appName} {`, + "", + ` public static void main(String[] args) {`, + ` SpringApplication.run(${appName}.class, args);`, + " }", + "}", + ].join("\n"), + "utf-8" + ); + + // Generate pom.xml + await writeFile( + resolve(bootDir, "pom.xml"), + buildBootPom(projectName, groupId, javaVersion, commonModules), + "utf-8" + ); +} diff --git a/src/lib/template.ts b/src/lib/template.ts index e822881..b1190ef 100644 --- a/src/lib/template.ts +++ b/src/lib/template.ts @@ -1,4 +1,4 @@ -import { cp, readFile, writeFile, rename, readdir, mkdir } from "node:fs/promises"; +import { cp, readFile, writeFile, readdir, mkdir, rm } from "node:fs/promises"; import { join, dirname } from "node:path"; import { existsSync } from "node:fs"; @@ -82,8 +82,15 @@ async function renamePlaceholders( if (newName !== oldName) { const dest = join(parent, newName); - await mkdir(dirname(dest), { recursive: true }); - await rename(path, dest); + await mkdir(dest, { recursive: true }); + // Copy children to new location, then remove old dir + const children = await readdir(path, { withFileTypes: true }); + for (const child of children) { + const srcChild = join(path, child.name); + const dstChild = join(dest, child.name); + await cp(srcChild, dstChild, { recursive: true }); + } + await rm(path, { recursive: true, force: true }); } } }