import { readFileSync, writeFileSync } from "node:fs"; import { createInterface } from "node:readline"; import { join } from "node:path"; export interface Semver { major: number; minor: number; patch: number; } export type BumpType = "major" | "minor" | "patch"; export function parseSemver(version: string): Semver { const parts = version.split("."); if (parts.length !== 3) { throw new Error(`无效的版本号格式: ${version}`); } const [major, minor, patch] = parts.map((p) => { const n = Number(p); if (Number.isNaN(n) || !Number.isInteger(n) || n < 0) { throw new Error(`无效的版本号格式: ${version}`); } return n; }); return { major: major!, minor: minor!, patch: patch! }; } export function bumpVersion(current: string, type: BumpType): string { const semver = parseSemver(current); switch (type) { case "major": return `${semver.major + 1}.0.0`; case "minor": return `${semver.major}.${semver.minor + 1}.0`; case "patch": return `${semver.major}.${semver.minor}.${semver.patch + 1}`; } } async function ask(query: string): Promise { const rl = createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(query, (answer) => { rl.close(); resolve(answer.trim()); }); }); } async function selectBumpType(): Promise { console.log("选择版本递增类型:"); console.log(" 1) major - 不兼容的 API 变更"); console.log(" 2) minor - 向下兼容的功能新增"); console.log(" 3) patch - 向下兼容的问题修正"); while (true) { const answer = await ask("请输入 1/2/3 [3]: "); const choice = answer || "3"; if (choice === "1") return "major"; if (choice === "2") return "minor"; if (choice === "3") return "patch"; console.log("无效选择,请输入 1、2 或 3"); } } async function stepBumpVersion(): Promise { const pkgPath = join(import.meta.dir, "..", "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string }; if (typeof pkg.version !== "string" || pkg.version.length === 0) { throw new Error("package.json 中缺少有效的 version 字段"); } const currentVersion = pkg.version; const bumpType = await selectBumpType(); const newVersion = bumpVersion(currentVersion, bumpType); const answer = await ask(`确认版本号 ${currentVersion} → ${newVersion}? [y/N]: `); if (answer.toLowerCase() !== "y") { console.log("已取消"); process.exit(0); } pkg.version = newVersion; writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); console.log(`版本号已更新: ${currentVersion} → ${newVersion}`); return newVersion; } async function runTests(): Promise { console.log("\n运行测试..."); const proc = Bun.spawn(["bun", "test", "--path-ignore-patterns", "tests/agent/**"], { stdio: ["inherit", "inherit", "inherit"], }); const exitCode = await proc.exited; if (exitCode !== 0) { throw new Error(`测试失败 (exit code: ${exitCode}),已跳过 git 和发布步骤`); } } async function stepGitCommitTag(version: string): Promise { // 检查工作区状态 const statusProc = Bun.spawn(["git", "status", "--porcelain"], { stdio: ["inherit", "pipe", "inherit"], }); const statusOutput = await new Response(statusProc.stdout).text(); const statusLines = statusOutput.trim().split("\n").filter(Boolean); const nonPkgChanges = statusLines.filter((line) => !/^[?MADRCU ]{2} package\.json$/.test(line)); if (nonPkgChanges.length > 0) { throw new Error("工作区有其他未提交变更,请先清理后再运行 release"); } console.log("\n准备提交:"); console.log(` git add package.json`); console.log(` git commit -m "chore: release v${version}"`); console.log(` git tag v${version}`); const answer = await ask("确认执行以上 git 操作? [y/N]: "); if (answer.toLowerCase() !== "y") { console.log("已取消 git 操作"); process.exit(0); } // git add package.json const addProc = Bun.spawn(["git", "add", "package.json"], { stdio: ["inherit", "inherit", "inherit"], }); const addExit = await addProc.exited; if (addExit !== 0) { throw new Error("git add 失败"); } // git commit const commitProc = Bun.spawn(["git", "commit", "-m", `chore: release v${version}`], { stdio: ["inherit", "inherit", "inherit"], }); const commitExit = await commitProc.exited; if (commitExit !== 0) { throw new Error("git commit 失败"); } // git tag const tagProc = Bun.spawn( ["git", "tag", "-a", `v${version}`, "-m", `chore: release v${version}`], { stdio: ["inherit", "inherit", "inherit"], }, ); const tagExit = await tagProc.exited; if (tagExit !== 0) { throw new Error("git tag 失败"); } console.log(`git commit 和 tag v${version} 已完成`); } async function stepNpmPublish(): Promise { console.log("\nnpm 发布预览:"); const dryRunProc = Bun.spawn(["bun", "publish", "--dry-run"], { stdio: ["inherit", "inherit", "inherit"], }); const dryRunExit = await dryRunProc.exited; if (dryRunExit !== 0) { throw new Error("npm publish --dry-run 失败"); } const answer = await ask("确认发布到 npm? [y/N]: "); if (answer.toLowerCase() !== "y") { console.log("已取消发布"); process.exit(0); } const proc = Bun.spawn(["bun", "publish", "--access", "public"], { stdio: ["inherit", "inherit", "inherit"], }); const exitCode = await proc.exited; if (exitCode !== 0) { throw new Error(`npm publish 失败 (exit code: ${exitCode}),请检查 npm 登录状态 (npm whoami)`); } console.log("npm 发布成功"); } async function main(): Promise { const newVersion = await stepBumpVersion(); console.log(`[1/4] 版本号递增完成: ${newVersion}`); await runTests(); console.log("[2/4] 测试通过"); await stepGitCommitTag(newVersion); console.log(`[3/4] git commit 和 tag v${newVersion} 完成`); await stepNpmPublish(); console.log("[4/4] npm 发布完成"); } if (import.meta.main) { main().catch((err: unknown) => { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); }); }