Files
brick-cli/src/commands/create.ts
T
yangzhaohan 6b19fd4601
Publish to Gitea Registry / publish (push) Failing after 1m32s
feat(cli): add create command with interactive block selection
- register create command in Commander program
- add @inquirer/prompts dependency for interactive prompts
- extend Block type with optional repo field
- scaffold create command module with initial implementation
2026-06-22 17:19:10 +08:00

344 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Command } from "commander";
import { checkbox, select, input } from "@inquirer/prompts";
import { resolve, basename } from "node:path";
import { writeFile } 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";
import { applyTemplate } from "../lib/template.js";
import { compileAiRules } from "../lib/ai-compiler.js";
import type { Block, BrickJson } from "../types/index.js";
interface SelectedBlock {
block: Block;
branch: string;
variantLabel: string;
}
const STYLES: { value: string; label: string }[] = [
{ value: "standard", label: "标准多模块 — 根 pom.xml 聚合所有积木子模块" },
{
value: "bom",
label: "BOM 模式 — 根 pom + bom 子模块统一依赖版本管理",
},
{ value: "simple", label: "简单模式 — 不生成根 pom,积木模块平铺" },
];
const AI_TOOLS: { value: string; label: string }[] = [
{ value: "claude", label: "Claude Code" },
{ value: "cursor", label: "Cursor" },
{ value: "copilot", label: "GitHub Copilot" },
{ value: "aider", label: "Aider" },
{ value: "generic", label: "通用(AI_CONTEXT.md" },
];
export function registerCreate(program: Command): void {
program
.command("create <project-name>")
.description("创建新项目")
.action(async (projectName: string) => {
try {
await runCreate(projectName);
} catch (err: unknown) {
const msg = (err as Error).message ?? "";
if (msg.includes("User force closed") || msg.includes("ExitPromptError")) {
console.log("\n已取消。");
process.exit(0);
}
throw err;
}
});
}
async function runCreate(projectName: string): Promise<void> {
const cwd = process.cwd();
// 1. Load index
const source = resolveSource();
console.log(`[1/7] 拉取积木索引...`);
const index = await loadIndex(source);
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,
})),
});
if (chosenNames.length === 0) {
console.log("未选择任何积木,已取消。");
return;
}
const chosenBlocks = index.blocks.filter((b) =>
chosenNames.includes(b.name)
);
// 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(", ")}`
);
}
// 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;
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;
}
selected.push({ block, branch, variantLabel });
}
// 2d. Project style
const style = await select({
message: "选择工程风格:",
choices: STYLES,
default: "standard",
});
// 2e. Variables
const defaultPkg = `com.example.${projectName.replace(/[^a-zA-Z0-9]/g, "")}`;
const basePackage = await input({
message: "基础包名 (basePackage):",
default: defaultPkg,
});
const groupId = await input({
message: "Maven groupId:",
default: basePackage,
});
const javaVersion = await input({
message: "Java 版本:",
default: "17",
});
// 2f. AI tool
const aiTool = await select({
message: "选择 AI 工具:",
choices: AI_TOOLS,
default: "claude",
});
// 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");
// 3. Clone blocks
console.log("[2/7] 拉取积木仓库...");
const blockDirs: { name: string; dir: string }[] = [];
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 });
}
// 4. Apply templates
console.log("[3/7] 生成项目源码...");
const targetDir = resolve(cwd, projectName);
const vars: Record<string, string> = {
basePackage,
groupId,
projectName,
javaVersion,
};
for (const bd of blockDirs) {
await applyTemplate(bd.dir, targetDir, vars);
}
// 5. Generate AI rules
console.log("[4/7] 生成 AI 规则文件...");
await compileAiRules(blockDirs, targetDir, aiTool);
// 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");
}
// 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!");
}
function resolveDeps(
chosen: Block[],
all: Block[]
): { all: Block[]; added: Block[] } {
const blockMap = new Map(all.map((b) => [b.name, b]));
const result = new Map(chosen.map((b) => [b.name, b]));
const added: Block[] = [];
// Iteratively resolve dependencies
let changed = true;
while (changed) {
changed = false;
for (const block of [...result.values()]) {
for (const depName of block.dependsOn) {
if (!result.has(depName)) {
const dep = blockMap.get(depName);
if (dep) {
result.set(depName, dep);
added.push(dep);
changed = true;
} else {
process.stderr.write(
`Warning: Dependency ${depName} not found in index\n`
);
}
}
}
}
}
return { all: [...result.values()], added };
}
function buildRootPom(
projectName: string,
groupId: string,
javaVersion: string,
selected: SelectedBlock[],
style: string
): string {
const modules = selected
.map((s) => ` <module>${projectName}-common-${s.block.module}</module>`)
.join("\n");
if (style === "bom") {
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<project xmlns="http://maven.apache.org/POM/4.0.0"',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
' xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">',
' <modelVersion>4.0.0</modelVersion>',
"",
` <groupId>${groupId}</groupId>`,
` <artifactId>${projectName}</artifactId>`,
" <version>0.0.0</version>",
" <packaging>pom</packaging>",
"",
` <name>${projectName}</name>`,
"",
" <properties>",
` <java.version>${javaVersion}</java.version>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` <brick.version>0.0.0</brick.version>`,
" </properties>",
"",
" <dependencyManagement>",
" <dependencies>",
...selected.map(
(s) =>
` <dependency>\n` +
` <groupId>${groupId}</groupId>\n` +
` <artifactId>${projectName}-common-${s.block.module}</artifactId>\n` +
` <version>${"$"}{brick.version}</version>\n` +
` </dependency>`
),
" </dependencies>",
" </dependencyManagement>",
"",
" <modules>",
` <module>${projectName}-common-bom</module>`,
modules,
" </modules>",
"</project>",
].join("\n");
}
// standard
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<project xmlns="http://maven.apache.org/POM/4.0.0"',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
' xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">',
' <modelVersion>4.0.0</modelVersion>',
"",
` <groupId>${groupId}</groupId>`,
` <artifactId>${projectName}</artifactId>`,
" <version>0.0.0</version>",
" <packaging>pom</packaging>",
"",
` <name>${projectName}</name>`,
"",
" <properties>",
` <java.version>${javaVersion}</java.version>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
" </properties>",
"",
" <modules>",
modules,
" </modules>",
"</project>",
].join("\n");
}