cf-yoyo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +28 -0
- package/.github/workflows/ci.yml +96 -0
- package/.prettierrc.json +10 -0
- package/CHANGELOG.md +55 -0
- package/README.md +138 -0
- package/__tests__/cli-e2e.test.ts +145 -0
- package/__tests__/config.test.ts +268 -0
- package/__tests__/filesystem.test.ts +453 -0
- package/__tests__/logger.test.ts +274 -0
- package/__tests__/template-engine.test.ts +450 -0
- package/__tests__/types.test.ts +25 -0
- package/deep_todos.md +766 -0
- package/dist/cli/commands/create.d.ts +26 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +308 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/git.d.ts +10 -0
- package/dist/cli/commands/git.d.ts.map +1 -0
- package/dist/cli/commands/git.js +887 -0
- package/dist/cli/commands/git.js.map +1 -0
- package/dist/cli/commands/list.d.ts +10 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +90 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +62 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/config.d.ts +35 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +260 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/filesystem.d.ts +84 -0
- package/dist/core/filesystem.d.ts.map +1 -0
- package/dist/core/filesystem.js +417 -0
- package/dist/core/filesystem.js.map +1 -0
- package/dist/core/git-token.d.ts +81 -0
- package/dist/core/git-token.d.ts.map +1 -0
- package/dist/core/git-token.js +244 -0
- package/dist/core/git-token.js.map +1 -0
- package/dist/core/git.d.ts +70 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +367 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/prompt.d.ts +28 -0
- package/dist/core/prompt.d.ts.map +1 -0
- package/dist/core/prompt.js +253 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/template-engine.d.ts +52 -0
- package/dist/core/template-engine.d.ts.map +1 -0
- package/dist/core/template-engine.js +308 -0
- package/dist/core/template-engine.js.map +1 -0
- package/dist/core/template-manager.d.ts +54 -0
- package/dist/core/template-manager.d.ts.map +1 -0
- package/dist/core/template-manager.js +330 -0
- package/dist/core/template-manager.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +244 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +51 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +68 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +140 -0
- package/dist/utils/logger.js.map +1 -0
- package/memory.md +241 -0
- package/need-debug.md +395 -0
- package/package.json +42 -0
- package/src/cli/commands/create.ts +326 -0
- package/src/cli/commands/git.ts +1001 -0
- package/src/cli/commands/list.ts +97 -0
- package/src/cli/index.ts +71 -0
- package/src/core/config.ts +262 -0
- package/src/core/filesystem.ts +408 -0
- package/src/core/git-token.ts +248 -0
- package/src/core/git.ts +384 -0
- package/src/core/prompt.ts +345 -0
- package/src/core/template-engine.ts +324 -0
- package/src/core/template-manager.ts +338 -0
- package/src/index.ts +19 -0
- package/src/types/index.ts +259 -0
- package/src/utils/logger.ts +150 -0
- package/templates/pages/basic/README.md.mustache +63 -0
- package/templates/pages/basic/package.json.mustache +23 -0
- package/templates/pages/basic/public/css/style.css +199 -0
- package/templates/pages/basic/public/index.html.mustache +72 -0
- package/templates/pages/basic/public/js/main.js +103 -0
- package/templates/pages/basic/template.json +38 -0
- package/templates/pages/basic/tsconfig.json +21 -0
- package/templates/pages/basic/wrangler.toml.mustache +14 -0
- package/templates/pages/basic-js/README.md.mustache +62 -0
- package/templates/pages/basic-js/package.json.mustache +25 -0
- package/templates/pages/basic-js/public/css/style.css +212 -0
- package/templates/pages/basic-js/public/index.html.mustache +53 -0
- package/templates/pages/basic-js/public/js/main.js +134 -0
- package/templates/pages/basic-js/template.json +35 -0
- package/templates/pages/basic-js/wrangler.toml.mustache +14 -0
- package/templates/pages/react/README.md.mustache +97 -0
- package/templates/pages/react/index.html.mustache +14 -0
- package/templates/pages/react/package.json.mustache +34 -0
- package/templates/pages/react/src/App.css +168 -0
- package/templates/pages/react/src/App.tsx.mustache +62 -0
- package/templates/pages/react/src/index.css +53 -0
- package/templates/pages/react/src/main.tsx.mustache +10 -0
- package/templates/pages/react/src/vite-env.d.ts +1 -0
- package/templates/pages/react/template.json +54 -0
- package/templates/pages/react/tsconfig.json +21 -0
- package/templates/pages/react/tsconfig.node.json +10 -0
- package/templates/pages/react/vite.config.ts +16 -0
- package/templates/worker/basic/README.md.mustache +56 -0
- package/templates/worker/basic/package.json.mustache +29 -0
- package/templates/worker/basic/src/index.ts.mustache +125 -0
- package/templates/worker/basic/template.json +30 -0
- package/templates/worker/basic/tsconfig.json +24 -0
- package/templates/worker/basic/wrangler.toml.mustache +33 -0
- package/templates/worker/basic-js/README.md.mustache +55 -0
- package/templates/worker/basic-js/package.json.mustache +25 -0
- package/templates/worker/basic-js/src/index.js.mustache +146 -0
- package/templates/worker/basic-js/template.json +27 -0
- package/templates/worker/basic-js/wrangler.toml.mustache +33 -0
- package/templates/worker/hono/README.md.mustache +79 -0
- package/templates/worker/hono/package.json.mustache +33 -0
- package/templates/worker/hono/src/index.ts.mustache +64 -0
- package/templates/worker/hono/src/routes/index.ts.mustache +165 -0
- package/templates/worker/hono/template.json +34 -0
- package/templates/worker/hono/tsconfig.json +24 -0
- package/templates/worker/hono/wrangler.toml.mustache +36 -0
- package/templates/worker/hono-js/README.md.mustache +67 -0
- package/templates/worker/hono-js/package.json.mustache +29 -0
- package/templates/worker/hono-js/src/index.js.mustache +55 -0
- package/templates/worker/hono-js/src/routes/index.js.mustache +127 -0
- package/templates/worker/hono-js/template.json +31 -0
- package/templates/worker/hono-js/wrangler.toml.mustache +36 -0
- package/thoughts/ledgers/CONTINUITY_ses_287e.md +74 -0
- package/thoughts/ledgers/CONTINUITY_ses_28b5.md +85 -0
- package/tsconfig.json +30 -0
- package/vitest.config.ts +20 -0
- package//351/240/205/347/233/256/350/241/250.md +140 -0
package/src/core/git.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git 操作核心功能模組
|
|
3
|
+
* 負責執行 Git 相關操作(init、push、clone 等)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { GitResult } from '../types';
|
|
10
|
+
import { loadTokens, GitToken } from './git-token';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execa 錯誤介面
|
|
14
|
+
* 定義 execa 執行失敗時拋出的錯誤物件結構
|
|
15
|
+
*/
|
|
16
|
+
interface ExecaError extends Error {
|
|
17
|
+
stderr?: string;
|
|
18
|
+
stdout?: string;
|
|
19
|
+
exitCode?: number;
|
|
20
|
+
command?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 檢查錯誤是否為 ExecaError
|
|
25
|
+
*/
|
|
26
|
+
function isExecaError(error: unknown): error is ExecaError {
|
|
27
|
+
return error instanceof Error && 'stderr' in error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 將 HTTPS URL 轉換為包含 Token 的格式
|
|
32
|
+
* 例如:https://github.com/user/repo.git -> https://<token>@github.com/user/repo.git
|
|
33
|
+
* @param url Git 倉庫 URL
|
|
34
|
+
* @param token Git Token
|
|
35
|
+
* @returns 包含 Token 的 URL
|
|
36
|
+
*/
|
|
37
|
+
function injectTokenToUrl(url: string, token: string): string {
|
|
38
|
+
// 只處理 HTTPS URL
|
|
39
|
+
if (!url.startsWith('https://')) {
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 移除 https:// 前綴
|
|
44
|
+
const urlWithoutProtocol = url.slice(8);
|
|
45
|
+
|
|
46
|
+
// 插入 token@
|
|
47
|
+
return `https://${token}@${urlWithoutProtocol}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 獲取要使用的 Git Token(若有)
|
|
52
|
+
* @returns GitToken 或 null
|
|
53
|
+
*/
|
|
54
|
+
function getGitToken(): GitToken | null {
|
|
55
|
+
const config = loadTokens();
|
|
56
|
+
if (config.tokens.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// 返回第一個 token(預設使用最新或第一個)
|
|
60
|
+
return config.tokens[0] || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 執行 Git 命令
|
|
65
|
+
* @param args Git 命令參數
|
|
66
|
+
* @param options 執行選項
|
|
67
|
+
*/
|
|
68
|
+
async function executeGitCommand(
|
|
69
|
+
args: string[],
|
|
70
|
+
options: { cwd?: string; timeout?: number } = {}
|
|
71
|
+
): Promise<GitResult> {
|
|
72
|
+
try {
|
|
73
|
+
const execOptions: { cwd: string; timeout: number } = {
|
|
74
|
+
cwd: options.cwd ?? process.cwd(),
|
|
75
|
+
timeout: options.timeout ?? 60000, // 預設 60 秒超時
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { stdout, stderr } = await execa('git', args, execOptions);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
message: '命令執行成功',
|
|
83
|
+
output: stdout || stderr,
|
|
84
|
+
};
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
87
|
+
const stderr = isExecaError(error) ? error.stderr : undefined;
|
|
88
|
+
|
|
89
|
+
const result: GitResult = {
|
|
90
|
+
success: false,
|
|
91
|
+
message: `Git 命令執行失敗:${errorMessage}`,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// 只有在有 stderr 時才添加 output 屬性
|
|
95
|
+
if (stderr !== undefined) {
|
|
96
|
+
result.output = stderr;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 檢查是否為 Git 倉庫
|
|
105
|
+
* @param cwd 工作目錄
|
|
106
|
+
*/
|
|
107
|
+
export async function isGitRepository(cwd?: string): Promise<boolean> {
|
|
108
|
+
const result = await executeGitCommand(['rev-parse', '--git-dir'], { cwd: cwd ?? process.cwd() });
|
|
109
|
+
return result.success;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 初始化 Git 倉庫
|
|
114
|
+
* @param cwd 工作目錄
|
|
115
|
+
*/
|
|
116
|
+
export async function gitInit(cwd?: string): Promise<GitResult> {
|
|
117
|
+
const targetDir = cwd || process.cwd();
|
|
118
|
+
|
|
119
|
+
// 檢查是否已為 Git 倉庫
|
|
120
|
+
const alreadyGit = await isGitRepository(targetDir);
|
|
121
|
+
if (alreadyGit) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
message: '此目錄已經是 Git 倉庫',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return await executeGitCommand(['init'], { cwd: targetDir });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 建立 .gitignore 檔案(若不存在)
|
|
133
|
+
* @param cwd 工作目錄
|
|
134
|
+
*/
|
|
135
|
+
export function createGitignore(cwd?: string): boolean {
|
|
136
|
+
const targetDir = cwd || process.cwd();
|
|
137
|
+
const gitignorePath = join(targetDir, '.gitignore');
|
|
138
|
+
|
|
139
|
+
if (existsSync(gitignorePath)) {
|
|
140
|
+
return false; // 已存在
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const defaultGitignore = `# 依賴
|
|
144
|
+
node_modules/
|
|
145
|
+
.pnpm-store/
|
|
146
|
+
|
|
147
|
+
# 構建輸出
|
|
148
|
+
dist/
|
|
149
|
+
build/
|
|
150
|
+
.next/
|
|
151
|
+
out/
|
|
152
|
+
|
|
153
|
+
# 環境變量
|
|
154
|
+
.env
|
|
155
|
+
.env.local
|
|
156
|
+
.env.*.local
|
|
157
|
+
|
|
158
|
+
# 日誌
|
|
159
|
+
*.log
|
|
160
|
+
npm-debug.log*
|
|
161
|
+
yarn-debug.log*
|
|
162
|
+
yarn-error.log*
|
|
163
|
+
|
|
164
|
+
# 編輯器
|
|
165
|
+
.vscode/
|
|
166
|
+
.idea/
|
|
167
|
+
*.swp
|
|
168
|
+
*.swo
|
|
169
|
+
*~
|
|
170
|
+
|
|
171
|
+
# 操作系統
|
|
172
|
+
.DS_Store
|
|
173
|
+
Thumbs.db
|
|
174
|
+
|
|
175
|
+
# Cloudflare
|
|
176
|
+
.wrangler/
|
|
177
|
+
.dev.vars
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
writeFileSync(gitignorePath, defaultGitignore, 'utf-8');
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Git 提交
|
|
186
|
+
* @param message 提交信息
|
|
187
|
+
* @param cwd 工作目錄
|
|
188
|
+
*/
|
|
189
|
+
export async function gitCommit(message: string, cwd?: string): Promise<GitResult> {
|
|
190
|
+
const targetCwd = cwd ?? process.cwd();
|
|
191
|
+
const result = await executeGitCommand(['add', '-A'], { cwd: targetCwd });
|
|
192
|
+
if (!result.success) {
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return await executeGitCommand(['commit', '-m', message], { cwd: targetCwd });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 添加 Git remote
|
|
201
|
+
* @param remoteName remote 名稱
|
|
202
|
+
* @param url remote URL
|
|
203
|
+
* @param cwd 工作目錄
|
|
204
|
+
*/
|
|
205
|
+
export async function gitAddRemote(
|
|
206
|
+
remoteName: string,
|
|
207
|
+
url: string,
|
|
208
|
+
cwd?: string
|
|
209
|
+
): Promise<GitResult> {
|
|
210
|
+
return await executeGitCommand(['remote', 'add', remoteName, url], { cwd: cwd ?? process.cwd() });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Git push
|
|
215
|
+
* @param remote remote 名稱
|
|
216
|
+
* @param branch 分支名稱
|
|
217
|
+
* @param cwd 工作目錄
|
|
218
|
+
* @param token Git Token(可選,若提供則使用 Token 認證)
|
|
219
|
+
*/
|
|
220
|
+
export async function gitPush(
|
|
221
|
+
remote: string,
|
|
222
|
+
branch: string,
|
|
223
|
+
cwd?: string,
|
|
224
|
+
token?: string
|
|
225
|
+
): Promise<GitResult> {
|
|
226
|
+
const targetCwd = cwd ?? process.cwd();
|
|
227
|
+
|
|
228
|
+
// 若有提供 token,先修改 remote URL 以包含 token
|
|
229
|
+
if (token) {
|
|
230
|
+
try {
|
|
231
|
+
// 獲取 remote URL
|
|
232
|
+
const { stdout: remoteUrl } = await execa('git', ['remote', 'get-url', remote], {
|
|
233
|
+
cwd: targetCwd,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!remoteUrl) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
message: '無法獲取 remote URL',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (remoteUrl.startsWith('https://')) {
|
|
244
|
+
// 將 token 注入 URL
|
|
245
|
+
const tokenUrl = injectTokenToUrl(remoteUrl, token);
|
|
246
|
+
|
|
247
|
+
// 設置臨時 remote URL
|
|
248
|
+
await execa('git', ['remote', 'set-url', remote, tokenUrl], {
|
|
249
|
+
cwd: targetCwd,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// 執行 push
|
|
254
|
+
const result = await executeGitCommand(['push', '-u', remote, branch], {
|
|
255
|
+
cwd: targetCwd,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 還原 remote URL(移除 token)
|
|
259
|
+
await execa('git', ['remote', 'set-url', remote, remoteUrl], {
|
|
260
|
+
cwd: targetCwd,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
// 如果失敗,還是要嘗試還原 URL
|
|
266
|
+
try {
|
|
267
|
+
await execa('git', ['remote', 'set-url', remote, remoteUrl], {
|
|
268
|
+
cwd: targetCwd,
|
|
269
|
+
});
|
|
270
|
+
} catch {
|
|
271
|
+
// 忽略還原失敗
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// 非 HTTPS URL(例如 SSH),無法使用 Token
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
message: `Token 認證僅支援 HTTPS URL,目前為:${remoteUrl},請改用 HTTPS 格式的 remote URL`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
} catch (error: unknown) {
|
|
283
|
+
// 如果 token 認證失敗,返回錯誤
|
|
284
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
message: `Token 認證推送失敗:${errorMessage}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return await executeGitCommand(['push', '-u', remote, branch], { cwd: targetCwd });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Git clone
|
|
297
|
+
* @param url 倉庫 URL
|
|
298
|
+
* @param directory 目標目錄
|
|
299
|
+
* @param token Git Token(可選,若提供則使用 Token 認證)
|
|
300
|
+
*/
|
|
301
|
+
export async function gitClone(
|
|
302
|
+
url: string,
|
|
303
|
+
directory?: string,
|
|
304
|
+
token?: string
|
|
305
|
+
): Promise<GitResult> {
|
|
306
|
+
// 若有 token 且為 HTTPS URL,注入 token
|
|
307
|
+
const cloneUrl = token && url.startsWith('https://') ? injectTokenToUrl(url, token) : url;
|
|
308
|
+
|
|
309
|
+
const args = ['clone', cloneUrl];
|
|
310
|
+
if (directory) {
|
|
311
|
+
args.push(directory);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return await executeGitCommand(args);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 獲取當前分支名稱
|
|
319
|
+
* @param cwd 工作目錄
|
|
320
|
+
*/
|
|
321
|
+
export async function getCurrentBranch(cwd?: string): Promise<string | null> {
|
|
322
|
+
const result = await executeGitCommand(['branch', '--show-current'], {
|
|
323
|
+
cwd: cwd ?? process.cwd(),
|
|
324
|
+
});
|
|
325
|
+
if (result.success && result.output) {
|
|
326
|
+
return result.output.trim();
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 獲取 remote 列表
|
|
333
|
+
* @param cwd 工作目錄
|
|
334
|
+
*/
|
|
335
|
+
export async function getRemotes(cwd?: string): Promise<string[]> {
|
|
336
|
+
const result = await executeGitCommand(['remote'], { cwd: cwd ?? process.cwd() });
|
|
337
|
+
if (result.success && result.output) {
|
|
338
|
+
return result.output
|
|
339
|
+
.trim()
|
|
340
|
+
.split('\n')
|
|
341
|
+
.filter((remoteName) => remoteName.trim() !== '');
|
|
342
|
+
}
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 設置 default branch
|
|
348
|
+
* @param branch 分支名稱
|
|
349
|
+
* @param cwd 工作目錄
|
|
350
|
+
*/
|
|
351
|
+
export async function gitBranchM1(branch: string, cwd?: string): Promise<GitResult> {
|
|
352
|
+
return await executeGitCommand(['branch', '-M', branch], { cwd: cwd ?? process.cwd() });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 取消 Git 倉庫(刪除 .git 目錄)
|
|
357
|
+
* @param cwd 工作目錄
|
|
358
|
+
*/
|
|
359
|
+
export async function removeGitRepo(cwd?: string): Promise<GitResult> {
|
|
360
|
+
const gitDir = join(cwd ?? process.cwd(), '.git');
|
|
361
|
+
|
|
362
|
+
if (!existsSync(gitDir)) {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
message: '此目錄不是 Git 倉庫',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const { rmSync } = await import('fs');
|
|
371
|
+
rmSync(gitDir, { recursive: true, force: true });
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
message: '已成功取消 Git 倉庫',
|
|
376
|
+
};
|
|
377
|
+
} catch (error: unknown) {
|
|
378
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
message: `取消 Git 倉庫失敗:${errorMessage}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 互動式問答模組
|
|
3
|
+
* 使用 Inquirer.js 實現用戶輸入收集與驗證
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import inquirer, { DistinctQuestion } from 'inquirer';
|
|
7
|
+
import { ProjectType, TemplateType, UserInput, Language } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 問題定義接口
|
|
11
|
+
*/
|
|
12
|
+
interface PromptQuestions {
|
|
13
|
+
projectName: string;
|
|
14
|
+
language: Language;
|
|
15
|
+
projectType: ProjectType;
|
|
16
|
+
template: TemplateType;
|
|
17
|
+
initGit: boolean;
|
|
18
|
+
installDeps: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 語言選擇問題類型
|
|
23
|
+
*/
|
|
24
|
+
interface LanguageQuestion {
|
|
25
|
+
type: 'select';
|
|
26
|
+
name: 'language';
|
|
27
|
+
message: string;
|
|
28
|
+
choices: Array<{ name: string; value: Language }>;
|
|
29
|
+
default: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 專案名稱問題類型
|
|
34
|
+
*/
|
|
35
|
+
interface ProjectNameQuestion {
|
|
36
|
+
type: 'input';
|
|
37
|
+
name: 'projectName';
|
|
38
|
+
message: string;
|
|
39
|
+
validate: (input: string) => boolean | string;
|
|
40
|
+
filter: (input: string) => string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 專案類型問題類型
|
|
45
|
+
*/
|
|
46
|
+
interface ProjectTypeQuestion {
|
|
47
|
+
type: 'select';
|
|
48
|
+
name: 'projectType';
|
|
49
|
+
message: string;
|
|
50
|
+
choices: Array<{ name: string; value: ProjectType }>;
|
|
51
|
+
default: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 模板問題類型
|
|
56
|
+
*/
|
|
57
|
+
interface TemplateQuestion {
|
|
58
|
+
type: 'select';
|
|
59
|
+
name: 'template';
|
|
60
|
+
message: string;
|
|
61
|
+
choices: Array<{ name: string; value: TemplateType }>;
|
|
62
|
+
default: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Git 初始化問題類型
|
|
67
|
+
*/
|
|
68
|
+
interface GitInitQuestion {
|
|
69
|
+
type: 'confirm';
|
|
70
|
+
name: 'initGit';
|
|
71
|
+
message: string;
|
|
72
|
+
default: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 依賴安裝問題類型
|
|
77
|
+
*/
|
|
78
|
+
interface InstallDepsQuestion {
|
|
79
|
+
type: 'confirm';
|
|
80
|
+
name: 'installDeps';
|
|
81
|
+
message: string;
|
|
82
|
+
default: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 驗證專案名稱
|
|
87
|
+
* 規則:
|
|
88
|
+
* - 只能包含小寫字母、數字、連字號、底線
|
|
89
|
+
* - 不能以大寫字母或數字開頭
|
|
90
|
+
* - 長度 1-214 個字元
|
|
91
|
+
*/
|
|
92
|
+
function validateProjectName(name: string): boolean | string {
|
|
93
|
+
if (!name || name.trim() === '') {
|
|
94
|
+
return '專案名稱不能為空';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const trimmedName = name.trim();
|
|
98
|
+
|
|
99
|
+
// 檢查長度
|
|
100
|
+
if (trimmedName.length > 214) {
|
|
101
|
+
return '專案名稱不能超過 214 個字元';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 檢查字元
|
|
105
|
+
const validCharsRegex = /^[a-z0-9_.-]+$/;
|
|
106
|
+
if (!validCharsRegex.test(trimmedName)) {
|
|
107
|
+
return '專案名稱只能包含小寫字母、數字、連字號(-)、點(.)和底線(_)';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 檢查開頭
|
|
111
|
+
if (/^[0-9A-Z]/.test(trimmedName)) {
|
|
112
|
+
return '專案名稱不能以大寫字母或數字開頭';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 檢查是否為保留名稱
|
|
116
|
+
const reservedNames = ['node_modules', '__proto__', 'node', 'npm', 'pnpm', 'yarn'];
|
|
117
|
+
if (reservedNames.includes(trimmedName.toLowerCase())) {
|
|
118
|
+
return `「${trimmedName}」是保留名稱,請改用其他名稱`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 獲取語言選擇問題
|
|
126
|
+
*/
|
|
127
|
+
function getLanguageQuestion(): LanguageQuestion {
|
|
128
|
+
return {
|
|
129
|
+
type: 'select' as const,
|
|
130
|
+
name: 'language',
|
|
131
|
+
message: '請選擇程式語言:',
|
|
132
|
+
choices: [
|
|
133
|
+
{ name: '🔷 TypeScript - 帶有型別安全的開發體驗', value: Language.TYPESCRIPT },
|
|
134
|
+
{ name: '🟨 JavaScript - 純 JavaScript 開發', value: Language.JAVASCRIPT },
|
|
135
|
+
],
|
|
136
|
+
default: 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 獲取專案類型選擇問題
|
|
142
|
+
*/
|
|
143
|
+
function getProjectTypeQuestion(): ProjectTypeQuestion {
|
|
144
|
+
return {
|
|
145
|
+
type: 'select' as const,
|
|
146
|
+
name: 'projectType',
|
|
147
|
+
message: '請選擇專案類型:',
|
|
148
|
+
choices: [
|
|
149
|
+
{
|
|
150
|
+
name: '☁️ Worker - Cloudflare Worker 基礎模板',
|
|
151
|
+
value: ProjectType.WORKER,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: '📄 Pages - Cloudflare Pages 靜態網站',
|
|
155
|
+
value: ProjectType.PAGES,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: '🗄️ D1 - 包含 D1 資料庫的 Worker',
|
|
159
|
+
value: ProjectType.D1,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: '📦 KV - 包含 KV 存儲的 Worker',
|
|
163
|
+
value: ProjectType.KV,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: '🪣 R2 - 包含 R2 存儲的 Worker',
|
|
167
|
+
value: ProjectType.R2,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
default: 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 獲取模板選擇問題
|
|
176
|
+
* 根據專案類型和語言動態調整可選模板
|
|
177
|
+
*/
|
|
178
|
+
function getTemplateQuestion(projectType: ProjectType, language: Language): TemplateQuestion {
|
|
179
|
+
// 根據專案類型和語言獲取可用模板
|
|
180
|
+
const getTemplateChoices = (): Array<{ name: string; value: TemplateType }> => {
|
|
181
|
+
// JavaScript 語言支援的模板
|
|
182
|
+
if (language === Language.JAVASCRIPT) {
|
|
183
|
+
switch (projectType) {
|
|
184
|
+
case ProjectType.WORKER:
|
|
185
|
+
return [
|
|
186
|
+
{ name: '🔹 Basic - 基礎 Worker 模板', value: TemplateType.BASIC },
|
|
187
|
+
{ name: '🔸 Hono - Hono 框架模板', value: TemplateType.HONO },
|
|
188
|
+
];
|
|
189
|
+
case ProjectType.PAGES:
|
|
190
|
+
return [{ name: '📄 Static - 靜態網站模板', value: TemplateType.STATIC }];
|
|
191
|
+
case ProjectType.D1:
|
|
192
|
+
case ProjectType.KV:
|
|
193
|
+
case ProjectType.R2:
|
|
194
|
+
return [{ name: '🔹 Basic - 基礎模板', value: TemplateType.BASIC }];
|
|
195
|
+
default:
|
|
196
|
+
return [{ name: '🔹 Basic - 基礎模板', value: TemplateType.BASIC }];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// TypeScript 語言支援的模板(原始完整支援)
|
|
201
|
+
switch (projectType) {
|
|
202
|
+
case ProjectType.WORKER:
|
|
203
|
+
return [
|
|
204
|
+
{ name: '🔹 Basic - 基礎 Worker 模板', value: TemplateType.BASIC },
|
|
205
|
+
{ name: '🔸 Hono - Hono 框架模板', value: TemplateType.HONO },
|
|
206
|
+
{ name: '🔹 Itty Router - itty-router 模板', value: TemplateType.ITTY_ROUTER },
|
|
207
|
+
];
|
|
208
|
+
case ProjectType.PAGES:
|
|
209
|
+
return [
|
|
210
|
+
{ name: '📄 Static - 靜態網站模板', value: TemplateType.STATIC },
|
|
211
|
+
{ name: '⚛️ React - React 模板', value: TemplateType.REACT },
|
|
212
|
+
{ name: '🔷 Vue - Vue 模板', value: TemplateType.VUE },
|
|
213
|
+
{ name: '⬡ Next.js - Next.js 模板', value: TemplateType.NEXTJS },
|
|
214
|
+
];
|
|
215
|
+
case ProjectType.D1:
|
|
216
|
+
case ProjectType.KV:
|
|
217
|
+
case ProjectType.R2:
|
|
218
|
+
return [{ name: '🔹 Basic - 基礎模板', value: TemplateType.BASIC }];
|
|
219
|
+
default:
|
|
220
|
+
return [{ name: '🔹 Basic - 基礎模板', value: TemplateType.BASIC }];
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
type: 'select' as const,
|
|
226
|
+
name: 'template',
|
|
227
|
+
message: '請選擇模板:',
|
|
228
|
+
choices: getTemplateChoices(),
|
|
229
|
+
default: 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 獲取 Git 初始化問題
|
|
235
|
+
*/
|
|
236
|
+
function getGitInitQuestion(): GitInitQuestion {
|
|
237
|
+
return {
|
|
238
|
+
type: 'confirm',
|
|
239
|
+
name: 'initGit',
|
|
240
|
+
message: '是否初始化 Git 倉庫?',
|
|
241
|
+
default: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 獲取依賴安裝問題
|
|
247
|
+
*/
|
|
248
|
+
function getInstallDepsQuestion(): InstallDepsQuestion {
|
|
249
|
+
return {
|
|
250
|
+
type: 'confirm',
|
|
251
|
+
name: 'installDeps',
|
|
252
|
+
message: '是否立即安裝依賴?',
|
|
253
|
+
default: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 執行互動式問答流程
|
|
259
|
+
* 收集所有必要的用戶輸入
|
|
260
|
+
*/
|
|
261
|
+
export async function runPrompts(initialName?: string): Promise<UserInput> {
|
|
262
|
+
const result: UserInput = {};
|
|
263
|
+
|
|
264
|
+
// 專案名稱(如果有提供則跳過)
|
|
265
|
+
if (!initialName) {
|
|
266
|
+
const { projectName } = await inquirer.prompt<Pick<PromptQuestions, 'projectName'>>([
|
|
267
|
+
{
|
|
268
|
+
type: 'input',
|
|
269
|
+
name: 'projectName',
|
|
270
|
+
message: '請輸入專案名稱:',
|
|
271
|
+
validate: validateProjectName,
|
|
272
|
+
filter: (input: string) => input.trim(),
|
|
273
|
+
} as ProjectNameQuestion,
|
|
274
|
+
]);
|
|
275
|
+
result.projectName = projectName;
|
|
276
|
+
} else {
|
|
277
|
+
// 驗證提供的專案名稱
|
|
278
|
+
const validation = validateProjectName(initialName);
|
|
279
|
+
if (validation !== true) {
|
|
280
|
+
console.error(`❌ 無效的專案名稱:${validation}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
result.projectName = initialName;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 語言選擇
|
|
287
|
+
const { language } = await inquirer.prompt<Pick<PromptQuestions, 'language'>>([
|
|
288
|
+
getLanguageQuestion(),
|
|
289
|
+
]);
|
|
290
|
+
result.language = language;
|
|
291
|
+
|
|
292
|
+
// 專案類型選擇
|
|
293
|
+
const { projectType } = await inquirer.prompt<Pick<PromptQuestions, 'projectType'>>([
|
|
294
|
+
getProjectTypeQuestion(),
|
|
295
|
+
]);
|
|
296
|
+
result.projectType = projectType;
|
|
297
|
+
|
|
298
|
+
// 模板選擇(根據語言和專案類型過濾)
|
|
299
|
+
const { template } = await inquirer.prompt<Pick<PromptQuestions, 'template'>>([
|
|
300
|
+
getTemplateQuestion(projectType, language),
|
|
301
|
+
]);
|
|
302
|
+
result.template = template;
|
|
303
|
+
|
|
304
|
+
// Git 初始化
|
|
305
|
+
const { initGit } = await inquirer.prompt<Pick<PromptQuestions, 'initGit'>>([
|
|
306
|
+
getGitInitQuestion(),
|
|
307
|
+
]);
|
|
308
|
+
result.initGit = initGit;
|
|
309
|
+
|
|
310
|
+
// 依賴安裝
|
|
311
|
+
const { installDeps } = await inquirer.prompt<Pick<PromptQuestions, 'installDeps'>>([
|
|
312
|
+
getInstallDepsQuestion(),
|
|
313
|
+
]);
|
|
314
|
+
result.installDeps = installDeps;
|
|
315
|
+
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 僅收集專案名稱
|
|
321
|
+
*/
|
|
322
|
+
export async function promptForProjectName(): Promise<string> {
|
|
323
|
+
const { projectName } = await inquirer.prompt<Pick<PromptQuestions, 'projectName'>>([
|
|
324
|
+
{
|
|
325
|
+
type: 'input',
|
|
326
|
+
name: 'projectName',
|
|
327
|
+
message: '請輸入專案名稱:',
|
|
328
|
+
validate: validateProjectName,
|
|
329
|
+
filter: (input: string) => input.trim(),
|
|
330
|
+
} as ProjectNameQuestion,
|
|
331
|
+
]);
|
|
332
|
+
return projectName;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 僅選擇專案類型
|
|
337
|
+
*/
|
|
338
|
+
export async function promptForProjectType(): Promise<ProjectType> {
|
|
339
|
+
const { projectType } = await inquirer.prompt<Pick<PromptQuestions, 'projectType'>>([
|
|
340
|
+
getProjectTypeQuestion(),
|
|
341
|
+
]);
|
|
342
|
+
return projectType;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export { validateProjectName };
|