Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1ede289

Browse files
committed
feat: 添加远程仓库清理功能并改进错误处理
添加远程仓库自动清理功能,当初始化过程中出现错误时自动删除已创建的仓库 改进错误处理逻辑,区分仓库已存在错误和其他错误 将默认分支从硬编码改为使用常量 DEFAULT_BRANCH 更新 API 基础 URL 为 GitHub API
1 parent 8d52e39 commit 1ede289

File tree

7 files changed

+154
-33
lines changed

7 files changed

+154
-33
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ NODE_ENV=development
55
DEBUG=false
66

77
# API 配置
8-
API_BASE_URL=https://api.example.com
8+
API_BASE_URL=https://api.github.com
99

1010
# GitHub API (可选)
1111
GITHUB_TOKEN=your_github_token_here

src/api/github.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,35 @@ export async function createRepo(data: Record<string, unknown>, auth?: string) {
1818
},
1919
});
2020
}
21+
22+
export async function deleteRepo(owner: string, repo: string, auth?: string) {
23+
const token = auth || process.env.GITHUB_TOKEN;
24+
25+
if (!token) {
26+
throw new Error("GitHub token is required. Please provide it as parameter or set GITHUB_TOKEN environment variable.");
27+
}
28+
29+
return fetch(`${baseURL}/repos/${owner}/${repo}`, {
30+
method: "DELETE",
31+
headers: {
32+
authorization: `Bearer ${token}`,
33+
"Content-Type": "application/json",
34+
},
35+
});
36+
}
37+
38+
export async function getCurrentUser(auth?: string) {
39+
const token = auth || process.env.GITHUB_TOKEN;
40+
41+
if (!token) {
42+
throw new Error("GitHub token is required. Please provide it as parameter or set GITHUB_TOKEN environment variable.");
43+
}
44+
45+
return fetch(`${baseURL}/user`, {
46+
method: "GET",
47+
headers: {
48+
authorization: `Bearer ${token}`,
49+
"Content-Type": "application/json",
50+
},
51+
});
52+
}

src/commands/create.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { consola } from "consola";
33
import { create } from "../inquirer/create";
44
import { download } from "../utils/download";
55
import Strategy from "../strategy/core";
6+
import { cleanupRemoteRepo } from "../utils/init";
67
import { PROJECT_NAME_REGEX, MESSAGES } from "../constants";
78

89
/**
@@ -52,12 +53,21 @@ export default defineCommand({
5253
}
5354

5455
// 执行创建流程
55-
const conf = await create(projectName);
56-
const path = await download(projectName, conf);
57-
if (path && conf?.template) {
58-
const strategy = new Strategy(projectName, path);
59-
// @ts-ignore - 动态调用模板方法
60-
await strategy?.[conf.template]?.(conf);
56+
try {
57+
const conf = await create(projectName);
58+
const path = await download(projectName, conf);
59+
if (path && conf?.template) {
60+
const strategy = new Strategy(projectName, path);
61+
// @ts-ignore - 动态调用模板方法
62+
await strategy?.[conf.template]?.(conf);
63+
}
64+
} catch (error) {
65+
// 检查是否是远程仓库同名错误,如果不是则清理远程仓库
66+
const errorMessage = error?.toString() || "";
67+
if (!errorMessage.includes("already exists") && !errorMessage.includes("name already exists")) {
68+
await cleanupRemoteRepo();
69+
}
70+
throw error;
6171
}
6272
},
6373
});

src/strategy/base.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from "fs-extra";
22
import { resolve } from "pathe";
3-
import { init, initInstall, initCommitPush } from "../utils/init";
3+
import { init, initInstall, initCommitPush, cleanupRemoteRepo } from "../utils/init";
44
import type { CreateConfig } from "../inquirer/create";
55

66
/**
@@ -24,24 +24,33 @@ export default class BaseStrategy {
2424
conf: CreateConfig,
2525
themeHandler?: (path: string) => Promise<void>
2626
) {
27-
const packagePath = resolve(this.path, "./package.json");
28-
const pkg = await this._updatePackageJson(packagePath, conf);
27+
let sshUrl = "";
28+
try {
29+
const packagePath = resolve(this.path, "./package.json");
30+
const pkg = await this._updatePackageJson(packagePath, conf);
2931

30-
// 处理主题功能
31-
const features = conf.features || [];
32-
if (!features?.includes?.("theme") && themeHandler) {
33-
await themeHandler(this.path);
34-
}
32+
// 处理主题功能
33+
const features = conf.features || [];
34+
if (!features?.includes?.("theme") && themeHandler) {
35+
await themeHandler(this.path);
36+
}
3537

36-
// 创建远程仓库,初始化 git
37-
const sshUrl = await init(pkg);
38+
// 创建远程仓库,初始化 git
39+
sshUrl = await init(pkg);
3840

39-
// 重新写入 package.json
40-
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
41+
// 重新写入 package.json
42+
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
4143

42-
// 安装依赖和提交代码
43-
await initInstall(pkg.name);
44-
if (sshUrl) await initCommitPush(pkg.name);
44+
// 安装依赖和提交代码
45+
await initInstall(pkg.name);
46+
if (sshUrl) await initCommitPush(pkg.name);
47+
} catch (error) {
48+
// 如果在初始化过程中出现错误且已创建远程仓库,则清理
49+
if (sshUrl) {
50+
await cleanupRemoteRepo();
51+
}
52+
throw error;
53+
}
4554
}
4655

4756
/**

src/utils/download.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export async function download(repoName: string, conf: CreateConfig) {
6363
]);
6464
if (res.override) {
6565
await fs.emptyDir(`./${dirName}`);
66+
} else {
67+
// 用户选择不覆盖,退出程序
68+
console.log(chalk.yellow("❌ 已取消创建项目"));
69+
process.exit(0);
6670
}
6771
}
6872

src/utils/init.ts

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,38 @@ import { $ } from "execa";
22
import { chdir } from "node:process";
33
import ora from "ora";
44
import chalk from "chalk";
5-
import { createRepo } from "../api/github";
5+
import { createRepo, deleteRepo, getCurrentUser } from "../api/github";
66
import { debug } from "./index";
77
import { DOWNLOAD_CONFIG, MESSAGES } from "../constants";
8+
import { DEFAULT_BRANCH } from "../constants";
9+
// 存储已创建的远程仓库信息,用于错误时清理
10+
let createdRepoInfo: { owner: string; repo: string; token: string } | null =
11+
null;
812
const spinner = ora({
913
text: chalk.cyan("🚀 正在初始化项目..."),
1014
spinner: DOWNLOAD_CONFIG.spinner.type,
1115
color: DOWNLOAD_CONFIG.spinner.color,
1216
});
1317

18+
// 清理远程仓库的函数
19+
export async function cleanupRemoteRepo() {
20+
if (createdRepoInfo) {
21+
try {
22+
console.log(chalk.yellow("🗑️ 正在清理远程仓库..."));
23+
await deleteRepo(
24+
createdRepoInfo.owner,
25+
createdRepoInfo.repo,
26+
createdRepoInfo.token
27+
);
28+
console.log(chalk.green("✅ 远程仓库已清理"));
29+
} catch (error) {
30+
console.log(chalk.red("❌ 清理远程仓库失败:"), error);
31+
} finally {
32+
createdRepoInfo = null;
33+
}
34+
}
35+
}
36+
1437
export async function init(pkg: Record<string, string>) {
1538
let sshUrl = "";
1639
try {
@@ -58,8 +81,8 @@ export async function initREPO(pkg: Record<string, unknown>) {
5881
},
5982
token
6083
);
61-
const result = await res?.json();
62-
const { ssh_url, html_url, message } = result;
84+
const result = (await res?.json()) as any;
85+
const { ssh_url, html_url, message } = result || {};
6386
pkg.homepage = `${html_url}#readme`;
6487
if (
6588
typeof pkg.repository === "object" &&
@@ -71,6 +94,20 @@ export async function initREPO(pkg: Record<string, unknown>) {
7194
if (typeof pkg.bugs === "object" && pkg.bugs && "url" in pkg.bugs)
7295
pkg.bugs.url = `${html_url}/issues`;
7396
if (ssh_url) {
97+
// 获取用户信息并保存仓库信息,用于错误时清理
98+
try {
99+
const userRes = await getCurrentUser(token);
100+
const userData = (await userRes?.json()) as any;
101+
if (userData?.login) {
102+
createdRepoInfo = {
103+
owner: userData.login,
104+
repo: pkg.name as string,
105+
token: token,
106+
};
107+
}
108+
} catch (userError) {
109+
console.log(chalk.yellow("⚠️ 获取用户信息失败,无法设置自动清理"));
110+
}
74111
spinner.succeed(chalk.green(MESSAGES.success.repoCreated));
75112
return ssh_url;
76113
} else {
@@ -82,7 +119,12 @@ export async function initREPO(pkg: Record<string, unknown>) {
82119
console.log(chalk.gray(MESSAGES.tips.regenerateToken));
83120
throw new Error("授权失败,请检查权限");
84121
}
85-
if (message?.includes("name already exists")) {
122+
if (
123+
message?.includes("name already exists") ||
124+
result?.errors?.some((error: any) =>
125+
error.message?.includes("name already exists")
126+
)
127+
) {
86128
spinner.fail(chalk.red(MESSAGES.error.repoExists));
87129
console.log(chalk.yellow(MESSAGES.tips.tryDifferentName));
88130
throw new Error("仓库名称已存在");
@@ -107,7 +149,7 @@ export async function initGIT(name: string, sshURL: string) {
107149
// Git初始化
108150
await $`git init`;
109151
// 切换 main 分支
110-
await $`git checkout -b main`;
152+
await $`git checkout -b ${DEFAULT_BRANCH}`;
111153

112154
if (sshURL) {
113155
// 初始化远程仓库
@@ -119,6 +161,11 @@ export async function initGIT(name: string, sshURL: string) {
119161
} catch (error) {
120162
spinner.fail(chalk.red(MESSAGES.error.gitInitFailed));
121163
debug(error);
164+
165+
// 如果已创建远程仓库,则清理
166+
if (createdRepoInfo) {
167+
await cleanupRemoteRepo();
168+
}
122169
throw error;
123170
} finally {
124171
chdir(`../`);
@@ -137,6 +184,11 @@ export async function initInstall(name: string) {
137184
console.log(chalk.gray(MESSAGES.tips.network));
138185
console.log(chalk.gray(MESSAGES.tips.manualInstall));
139186
debug(error);
187+
188+
// 如果已创建远程仓库,则清理
189+
if (createdRepoInfo) {
190+
await cleanupRemoteRepo();
191+
}
140192
throw error;
141193
} finally {
142194
chdir(`../`);
@@ -160,11 +212,15 @@ export async function initCommitPush(name: string) {
160212
await $`git commit --no-verify --message ${"Initial commit"}`;
161213
// Git推送到远程仓库
162214
await $`git config pull.rebase false`;
163-
await $`git pull origin main --allow-unrelated-histories`.catch(() => {
164-
// 如果远程仓库为空,pull 可能会失败,这是正常的
165-
});
215+
await $`git pull origin ${DEFAULT_BRANCH} --allow-unrelated-histories`.catch(
216+
() => {
217+
// 如果远程仓库为空,pull 可能会失败,这是正常的
218+
}
219+
);
166220
// Git推送到远程仓库
167-
await $`git push origin main`;
221+
await $`git push origin ${DEFAULT_BRANCH}`;
222+
// 推送成功,清除仓库信息,避免误删除
223+
createdRepoInfo = null;
168224
spinner.succeed(chalk.green(MESSAGES.success.codePushed));
169225
} catch (error) {
170226
spinner.fail(chalk.red(MESSAGES.error.pushFailed));
@@ -173,6 +229,16 @@ export async function initCommitPush(name: string) {
173229
console.log(chalk.gray(MESSAGES.tips.checkRepoAccess));
174230
console.log(chalk.gray(MESSAGES.tips.manualPush));
175231
debug(error);
232+
233+
// 检查是否是远程仓库同名错误,如果不是则清理远程仓库
234+
const errorMessage = error?.toString() || "";
235+
if (
236+
!errorMessage.includes("already exists") &&
237+
!errorMessage.includes("name already exists") &&
238+
createdRepoInfo
239+
) {
240+
await cleanupRemoteRepo();
241+
}
176242
// 不抛出错误,允许项目创建继续完成
177243
} finally {
178244
chdir(`../`);

src/utils/template.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TemplateMap } from "../config/templates";
22
import type { CreateConfig } from "../inquirer/create";
3-
3+
import { DEFAULT_BRANCH } from "../constants";
44
/**
55
* 根据配置生成仓库分支名称
66
* @param conf 配置对象
@@ -10,7 +10,7 @@ export function getRepoBranchName(conf: CreateConfig): string {
1010
const features = conf.features || [];
1111
const component = conf.component;
1212

13-
let refName = "main";
13+
let refName = DEFAULT_BRANCH;
1414

1515
if (features.includes("i18n")) {
1616
refName = component ? `${component}-i18n` : "i18n";

0 commit comments

Comments
 (0)