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.
Files changed (141) hide show
  1. package/.eslintrc.json +28 -0
  2. package/.github/workflows/ci.yml +96 -0
  3. package/.prettierrc.json +10 -0
  4. package/CHANGELOG.md +55 -0
  5. package/README.md +138 -0
  6. package/__tests__/cli-e2e.test.ts +145 -0
  7. package/__tests__/config.test.ts +268 -0
  8. package/__tests__/filesystem.test.ts +453 -0
  9. package/__tests__/logger.test.ts +274 -0
  10. package/__tests__/template-engine.test.ts +450 -0
  11. package/__tests__/types.test.ts +25 -0
  12. package/deep_todos.md +766 -0
  13. package/dist/cli/commands/create.d.ts +26 -0
  14. package/dist/cli/commands/create.d.ts.map +1 -0
  15. package/dist/cli/commands/create.js +308 -0
  16. package/dist/cli/commands/create.js.map +1 -0
  17. package/dist/cli/commands/git.d.ts +10 -0
  18. package/dist/cli/commands/git.d.ts.map +1 -0
  19. package/dist/cli/commands/git.js +887 -0
  20. package/dist/cli/commands/git.js.map +1 -0
  21. package/dist/cli/commands/list.d.ts +10 -0
  22. package/dist/cli/commands/list.d.ts.map +1 -0
  23. package/dist/cli/commands/list.js +90 -0
  24. package/dist/cli/commands/list.js.map +1 -0
  25. package/dist/cli/index.d.ts +15 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +62 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/core/config.d.ts +35 -0
  30. package/dist/core/config.d.ts.map +1 -0
  31. package/dist/core/config.js +260 -0
  32. package/dist/core/config.js.map +1 -0
  33. package/dist/core/filesystem.d.ts +84 -0
  34. package/dist/core/filesystem.d.ts.map +1 -0
  35. package/dist/core/filesystem.js +417 -0
  36. package/dist/core/filesystem.js.map +1 -0
  37. package/dist/core/git-token.d.ts +81 -0
  38. package/dist/core/git-token.d.ts.map +1 -0
  39. package/dist/core/git-token.js +244 -0
  40. package/dist/core/git-token.js.map +1 -0
  41. package/dist/core/git.d.ts +70 -0
  42. package/dist/core/git.d.ts.map +1 -0
  43. package/dist/core/git.js +367 -0
  44. package/dist/core/git.js.map +1 -0
  45. package/dist/core/prompt.d.ts +28 -0
  46. package/dist/core/prompt.d.ts.map +1 -0
  47. package/dist/core/prompt.js +253 -0
  48. package/dist/core/prompt.js.map +1 -0
  49. package/dist/core/template-engine.d.ts +52 -0
  50. package/dist/core/template-engine.d.ts.map +1 -0
  51. package/dist/core/template-engine.js +308 -0
  52. package/dist/core/template-engine.js.map +1 -0
  53. package/dist/core/template-manager.d.ts +54 -0
  54. package/dist/core/template-manager.d.ts.map +1 -0
  55. package/dist/core/template-manager.js +330 -0
  56. package/dist/core/template-manager.js.map +1 -0
  57. package/dist/index.d.ts +12 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +19 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/types/index.d.ts +244 -0
  62. package/dist/types/index.d.ts.map +1 -0
  63. package/dist/types/index.js +51 -0
  64. package/dist/types/index.js.map +1 -0
  65. package/dist/utils/logger.d.ts +68 -0
  66. package/dist/utils/logger.d.ts.map +1 -0
  67. package/dist/utils/logger.js +140 -0
  68. package/dist/utils/logger.js.map +1 -0
  69. package/memory.md +241 -0
  70. package/need-debug.md +395 -0
  71. package/package.json +42 -0
  72. package/src/cli/commands/create.ts +326 -0
  73. package/src/cli/commands/git.ts +1001 -0
  74. package/src/cli/commands/list.ts +97 -0
  75. package/src/cli/index.ts +71 -0
  76. package/src/core/config.ts +262 -0
  77. package/src/core/filesystem.ts +408 -0
  78. package/src/core/git-token.ts +248 -0
  79. package/src/core/git.ts +384 -0
  80. package/src/core/prompt.ts +345 -0
  81. package/src/core/template-engine.ts +324 -0
  82. package/src/core/template-manager.ts +338 -0
  83. package/src/index.ts +19 -0
  84. package/src/types/index.ts +259 -0
  85. package/src/utils/logger.ts +150 -0
  86. package/templates/pages/basic/README.md.mustache +63 -0
  87. package/templates/pages/basic/package.json.mustache +23 -0
  88. package/templates/pages/basic/public/css/style.css +199 -0
  89. package/templates/pages/basic/public/index.html.mustache +72 -0
  90. package/templates/pages/basic/public/js/main.js +103 -0
  91. package/templates/pages/basic/template.json +38 -0
  92. package/templates/pages/basic/tsconfig.json +21 -0
  93. package/templates/pages/basic/wrangler.toml.mustache +14 -0
  94. package/templates/pages/basic-js/README.md.mustache +62 -0
  95. package/templates/pages/basic-js/package.json.mustache +25 -0
  96. package/templates/pages/basic-js/public/css/style.css +212 -0
  97. package/templates/pages/basic-js/public/index.html.mustache +53 -0
  98. package/templates/pages/basic-js/public/js/main.js +134 -0
  99. package/templates/pages/basic-js/template.json +35 -0
  100. package/templates/pages/basic-js/wrangler.toml.mustache +14 -0
  101. package/templates/pages/react/README.md.mustache +97 -0
  102. package/templates/pages/react/index.html.mustache +14 -0
  103. package/templates/pages/react/package.json.mustache +34 -0
  104. package/templates/pages/react/src/App.css +168 -0
  105. package/templates/pages/react/src/App.tsx.mustache +62 -0
  106. package/templates/pages/react/src/index.css +53 -0
  107. package/templates/pages/react/src/main.tsx.mustache +10 -0
  108. package/templates/pages/react/src/vite-env.d.ts +1 -0
  109. package/templates/pages/react/template.json +54 -0
  110. package/templates/pages/react/tsconfig.json +21 -0
  111. package/templates/pages/react/tsconfig.node.json +10 -0
  112. package/templates/pages/react/vite.config.ts +16 -0
  113. package/templates/worker/basic/README.md.mustache +56 -0
  114. package/templates/worker/basic/package.json.mustache +29 -0
  115. package/templates/worker/basic/src/index.ts.mustache +125 -0
  116. package/templates/worker/basic/template.json +30 -0
  117. package/templates/worker/basic/tsconfig.json +24 -0
  118. package/templates/worker/basic/wrangler.toml.mustache +33 -0
  119. package/templates/worker/basic-js/README.md.mustache +55 -0
  120. package/templates/worker/basic-js/package.json.mustache +25 -0
  121. package/templates/worker/basic-js/src/index.js.mustache +146 -0
  122. package/templates/worker/basic-js/template.json +27 -0
  123. package/templates/worker/basic-js/wrangler.toml.mustache +33 -0
  124. package/templates/worker/hono/README.md.mustache +79 -0
  125. package/templates/worker/hono/package.json.mustache +33 -0
  126. package/templates/worker/hono/src/index.ts.mustache +64 -0
  127. package/templates/worker/hono/src/routes/index.ts.mustache +165 -0
  128. package/templates/worker/hono/template.json +34 -0
  129. package/templates/worker/hono/tsconfig.json +24 -0
  130. package/templates/worker/hono/wrangler.toml.mustache +36 -0
  131. package/templates/worker/hono-js/README.md.mustache +67 -0
  132. package/templates/worker/hono-js/package.json.mustache +29 -0
  133. package/templates/worker/hono-js/src/index.js.mustache +55 -0
  134. package/templates/worker/hono-js/src/routes/index.js.mustache +127 -0
  135. package/templates/worker/hono-js/template.json +31 -0
  136. package/templates/worker/hono-js/wrangler.toml.mustache +36 -0
  137. package/thoughts/ledgers/CONTINUITY_ses_287e.md +74 -0
  138. package/thoughts/ledgers/CONTINUITY_ses_28b5.md +85 -0
  139. package/tsconfig.json +30 -0
  140. package/vitest.config.ts +20 -0
  141. package//351/240/205/347/233/256/350/241/250.md +140 -0
@@ -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 };