6b19fd4601
Publish to Gitea Registry / publish (push) Failing after 1m32s
- 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
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
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");
|
||
}
|