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,1001 @@
1
+ /**
2
+ * Git 命令模組
3
+ * 提供子命令(init/push/clone)和互動式選單
4
+ */
5
+
6
+ import { Command } from 'commander';
7
+ import { logger } from '../../utils/logger';
8
+ import type { GitResult } from '../../types';
9
+ import {
10
+ isGitRepository,
11
+ gitInit,
12
+ createGitignore,
13
+ gitCommit,
14
+ gitAddRemote,
15
+ gitPush,
16
+ gitClone,
17
+ getCurrentBranch,
18
+ getRemotes,
19
+ removeGitRepo,
20
+ } from '../../core/git';
21
+ import {
22
+ loadTokens,
23
+ addToken,
24
+ deleteToken,
25
+ updateTokenName,
26
+ getTokenList,
27
+ } from '../../core/git-token';
28
+ import { execa } from 'execa';
29
+
30
+ // Git 操作類型
31
+ enum GitOperationType {
32
+ INIT = 'init',
33
+ PUSH = 'push',
34
+ CLONE = 'clone',
35
+ MANAGE_TOKENS = 'manage-tokens',
36
+ REMOVE_REPO = 'remove-repo',
37
+ }
38
+
39
+ // 操作選項定義
40
+ const operationChoices = [
41
+ { name: '初始化 Git 倉庫', value: GitOperationType.INIT },
42
+ { name: '推送到遠端倉庫', value: GitOperationType.PUSH },
43
+ { name: '克隆遠端倉庫', value: GitOperationType.CLONE },
44
+ { name: '管理 Git Token', value: GitOperationType.MANAGE_TOKENS },
45
+ { name: '取消 Git 倉庫', value: GitOperationType.REMOVE_REPO },
46
+ { name: '❌ 退出', value: 'exit' },
47
+ ];
48
+
49
+ /**
50
+ * Git 命令主處理函數
51
+ * 透過互動式問答選擇操作類型,然後執行對應功能
52
+ */
53
+ async function gitCommandHandler(): Promise<void> {
54
+ let running = true;
55
+
56
+ while (running) {
57
+ try {
58
+ logger.empty();
59
+ logger.icon('🔧', 'Git 操作選單');
60
+ logger.divider();
61
+ logger.empty();
62
+
63
+ // 第一步:詢問操作類型
64
+ const { operation } = await import('inquirer').then((mod) =>
65
+ mod.default.prompt([
66
+ {
67
+ type: 'rawlist',
68
+ name: 'operation',
69
+ message: '請選擇要執行的 Git 操作:',
70
+ choices: operationChoices,
71
+ },
72
+ ])
73
+ );
74
+
75
+ // 根據選擇的操作執行對應功能
76
+ switch (operation) {
77
+ case GitOperationType.INIT:
78
+ await handleGitInit();
79
+ break;
80
+ case GitOperationType.PUSH:
81
+ await handleGitPush();
82
+ break;
83
+ case GitOperationType.CLONE:
84
+ await handleGitClone();
85
+ break;
86
+ case GitOperationType.MANAGE_TOKENS:
87
+ await handleManageGitToken();
88
+ break;
89
+ case GitOperationType.REMOVE_REPO:
90
+ await handleRemoveGitRepo();
91
+ break;
92
+ case 'exit':
93
+ logger.info('再見!');
94
+ running = false;
95
+ break;
96
+ default:
97
+ logger.error('無效的操作類型');
98
+ }
99
+ } catch (error: unknown) {
100
+ const errorMessage = error instanceof Error ? error.message : String(error);
101
+ logger.error(`Git 操作失敗:${errorMessage}`);
102
+ running = false;
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 處理 Git 初始化操作
109
+ */
110
+ async function handleGitInit(): Promise<void> {
111
+ logger.empty();
112
+ logger.icon('🔧', '初始化 Git 倉庫');
113
+ logger.divider();
114
+ logger.empty();
115
+
116
+ // 檢查是否已為 Git 倉庫
117
+ const alreadyGit = await isGitRepository();
118
+ if (alreadyGit) {
119
+ logger.warn('此目錄已經是 Git 倉庫');
120
+ return;
121
+ }
122
+
123
+ // 執行 git init
124
+ logger.info('正在執行 git init...');
125
+ const initResult = await gitInit();
126
+
127
+ if (!initResult.success) {
128
+ logger.error(`Git 初始化失敗:${initResult.message}`);
129
+ process.exit(1);
130
+ }
131
+
132
+ logger.success('✓ Git 倉庫初始化成功');
133
+
134
+ // 建立 .gitignore(若不存在)
135
+ const gitignoreCreated = createGitignore();
136
+ if (gitignoreCreated) {
137
+ logger.success('✓ 已建立 .gitignore 檔案');
138
+ } else {
139
+ logger.info('ℹ️ .gitignore 檔案已存在');
140
+ }
141
+
142
+ // 詢問是否進行初始提交
143
+ const { confirm } = await import('inquirer').then((mod) =>
144
+ mod.default.prompt([
145
+ {
146
+ type: 'confirm',
147
+ name: 'confirm',
148
+ message: '是否進行初始提交?',
149
+ default: true,
150
+ },
151
+ ])
152
+ );
153
+
154
+ if (confirm) {
155
+ logger.info('正在執行 git add...');
156
+ const commitResult = await gitCommit('Initial commit');
157
+
158
+ if (commitResult.success) {
159
+ logger.success('✓ 初始提交完成');
160
+ } else {
161
+ logger.warn(`初始提交失敗:${commitResult.message}`);
162
+ }
163
+ }
164
+
165
+ logger.empty();
166
+ logger.success('✅ Git 初始化完成!');
167
+ logger.empty();
168
+ }
169
+
170
+ /**
171
+ * Token 選擇策略類型
172
+ * 定義推送時如何選擇和使用 Git Token
173
+ */
174
+ type TokenSelectionStrategy = 'none' | 'first' | 'interactive';
175
+
176
+ /**
177
+ * 選擇並執行推送用的 Token
178
+ *
179
+ * 選擇策略說明:
180
+ * 1. 若無 Token:直接推送,不附加認證
181
+ * 2. 若有 1 個 Token:自動使用第一個 Token 進行推送
182
+ * 3. 若有 2+ 個 Token:進入互動選擇流程,讓用戶選擇要使用的 Token
183
+ *
184
+ * @param remoteName - 遠端倉庫名稱(通常為 'origin')
185
+ * @param branch - 分支名稱
186
+ * @returns GitResult - 推送操作的結果,包含成功狀態和輸出訊息
187
+ */
188
+ async function selectAndExecutePushToken(remoteName: string, branch: string): Promise<GitResult> {
189
+ const tokens = loadTokens();
190
+ let pushResult: GitResult;
191
+
192
+ // 根據 Token 數量決定選擇策略
193
+ const strategy: TokenSelectionStrategy =
194
+ tokens.tokens.length === 0 ? 'none' : tokens.tokens.length === 1 ? 'first' : 'interactive';
195
+
196
+ switch (strategy) {
197
+ case 'none': {
198
+ // 無 Token:使用一般推送
199
+ logger.info(`正在推送到 ${remoteName}/${branch}...`);
200
+ pushResult = await gitPush(remoteName, branch);
201
+ break;
202
+ }
203
+
204
+ case 'first': {
205
+ // 單一 Token:自動使用第一個
206
+ const firstToken = tokens.tokens[0];
207
+ if (firstToken) {
208
+ logger.info(`正在使用 Token「${firstToken.name}」推送到 ${remoteName}/${branch}...`);
209
+ pushResult = await gitPush(remoteName, branch, undefined, firstToken.token);
210
+ } else {
211
+ // 防禦性檢查:理論上不會進入此分支
212
+ logger.warn('Token 列表異常,使用一般推送');
213
+ pushResult = await gitPush(remoteName, branch);
214
+ }
215
+ break;
216
+ }
217
+
218
+ case 'interactive': {
219
+ // 多個 Token:讓用戶互動選擇
220
+ const { selectedTokenId } = await import('inquirer').then((mod) =>
221
+ mod.default.prompt([
222
+ {
223
+ type: 'list',
224
+ name: 'selectedTokenId',
225
+ message: '選擇要使用的 Git Token:',
226
+ choices: [
227
+ ...tokens.tokens.map((tokenItem) => ({
228
+ name: tokenItem.name,
229
+ value: tokenItem.id,
230
+ })),
231
+ {
232
+ name: '不使用 Token(一般推送)',
233
+ value: 'none',
234
+ },
235
+ ],
236
+ },
237
+ ])
238
+ );
239
+
240
+ if (selectedTokenId === 'none') {
241
+ logger.info(`正在推送到 ${remoteName}/${branch}...`);
242
+ pushResult = await gitPush(remoteName, branch);
243
+ } else {
244
+ const selectedToken = tokens.tokens.find((t) => t.id === selectedTokenId);
245
+ if (selectedToken) {
246
+ logger.info(`正在使用 Token「${selectedToken.name}」推送到 ${remoteName}/${branch}...`);
247
+ pushResult = await gitPush(remoteName, branch, undefined, selectedToken.token);
248
+ } else {
249
+ // 防禦性檢查:選擇的 Token 不存在
250
+ logger.warn('選擇的 Token 不存在,使用一般推送');
251
+ pushResult = await gitPush(remoteName, branch);
252
+ }
253
+ }
254
+ break;
255
+ }
256
+
257
+ default: {
258
+ // 防禦性預設:使用一般推送
259
+ logger.info(`正在推送到 ${remoteName}/${branch}...`);
260
+ pushResult = await gitPush(remoteName, branch);
261
+ }
262
+ }
263
+
264
+ return pushResult;
265
+ }
266
+
267
+ /**
268
+ * 處理 Git 推送操作
269
+ */
270
+ async function handleGitPush(): Promise<void> {
271
+ logger.empty();
272
+ logger.icon('📤', '推送到遠端倉庫');
273
+ logger.divider();
274
+ logger.empty();
275
+
276
+ // 檢查是否為 Git 倉庫
277
+ const isGit = await isGitRepository();
278
+ if (!isGit) {
279
+ logger.error('此目錄不是 Git 倉庫,請先執行 git init');
280
+ process.exit(1);
281
+ }
282
+
283
+ // 獲取當前分支
284
+ const currentBranch = await getCurrentBranch();
285
+ if (!currentBranch) {
286
+ logger.error('無法獲取當前分支');
287
+ process.exit(1);
288
+ }
289
+
290
+ // 檢查是否有未提交的更改
291
+ const hasUncommittedChanges = await checkForUncommittedChanges();
292
+ if (hasUncommittedChanges) {
293
+ logger.info('檢測到有未提交的更改,正在自動提交...');
294
+ const commitResult = await gitCommit('chore: auto-commit before push');
295
+ if (commitResult.success) {
296
+ logger.success('✓ 已自動提交更改');
297
+ } else {
298
+ logger.error(`自動提交失敗:${commitResult.message}`);
299
+ process.exit(1);
300
+ }
301
+ }
302
+
303
+ // 獲取 remotes
304
+ const remotes = await getRemotes();
305
+ let remoteName = 'origin';
306
+
307
+ // 如果沒有 remotes,詢問遠端 URL
308
+ if (remotes.length === 0 || !remotes.includes(remoteName)) {
309
+ const { remoteUrl } = await import('inquirer').then((mod) =>
310
+ mod.default.prompt([
311
+ {
312
+ type: 'input',
313
+ name: 'remoteUrl',
314
+ message: '請輸入遠端倉庫 URL:',
315
+ default: '',
316
+ validate: (input: string) => {
317
+ if (!input.trim()) {
318
+ return '遠端 URL 不能為空';
319
+ }
320
+ // 簡單驗證 URL 格式
321
+ const urlPattern = /^(https?:\/\/|git@|ssh:\/\/)/;
322
+ if (!urlPattern.test(input.trim())) {
323
+ return '請輸入有效的 Git URL(以 https://、git@ 或 ssh:// 開頭)';
324
+ }
325
+ return true;
326
+ },
327
+ },
328
+ ])
329
+ );
330
+
331
+ // 添加 remote
332
+ logger.info(`正在添加 remote "${remoteName}"...`);
333
+ const addRemoteResult = await gitAddRemote(remoteName, remoteUrl);
334
+
335
+ if (!addRemoteResult.success) {
336
+ logger.error(`添加 remote 失敗:${addRemoteResult.message}`);
337
+ process.exit(1);
338
+ }
339
+
340
+ logger.success(`✓ 已添加 remote "${remoteName}"`);
341
+ }
342
+
343
+ // 詢問分支名稱
344
+ const { branch } = await import('inquirer').then((mod) =>
345
+ mod.default.prompt([
346
+ {
347
+ type: 'input',
348
+ name: 'branch',
349
+ message: '要推送到哪個分支?',
350
+ default: currentBranch,
351
+ validate: (input: string) => {
352
+ if (!input.trim()) {
353
+ return '分支名稱不能為空';
354
+ }
355
+ return true;
356
+ },
357
+ },
358
+ ])
359
+ );
360
+
361
+ // 確認推送
362
+ const { confirm } = await import('inquirer').then((mod) =>
363
+ mod.default.prompt([
364
+ {
365
+ type: 'confirm',
366
+ name: 'confirm',
367
+ message: `確認推送到 ${remoteName}/${branch}?`,
368
+ default: true,
369
+ },
370
+ ])
371
+ );
372
+
373
+ if (!confirm) {
374
+ logger.info('已取消推送');
375
+ return;
376
+ }
377
+
378
+ // 選擇並執行推送用的 Token
379
+ const pushResult = await selectAndExecutePushToken(remoteName, branch);
380
+
381
+ // 調試:輸出推送結果
382
+ logger.info(`推送結果:success=${pushResult.success}, message=${pushResult.message}`);
383
+ if (pushResult.output) {
384
+ logger.info(`推送輸出:${pushResult.output}`);
385
+ }
386
+
387
+ if (!pushResult.success) {
388
+ // 檢查是否為認證錯誤且尚未使用 Token
389
+ const isAuthError =
390
+ pushResult.output?.includes('Authentication failed') ||
391
+ pushResult.output?.includes('could not read Username') ||
392
+ pushResult.output?.includes('fatal: Authentication failed');
393
+
394
+ if (isAuthError) {
395
+ // 沒有 Token 且為認證錯誤,嘗試使用 Token 認證
396
+ logger.warn('檢測到認證錯誤,嘗試使用 Git Token...');
397
+ const tokenUsed = await handleGitTokenAuth(remoteName, branch);
398
+ if (tokenUsed) {
399
+ return; // Token 認證後重新推送成功
400
+ }
401
+ }
402
+
403
+ logger.error(`推送失敗:${pushResult.message}`);
404
+ if (pushResult.output) {
405
+ logger.info(pushResult.output);
406
+ }
407
+ process.exit(1);
408
+ }
409
+
410
+ logger.success('✓ 推送成功');
411
+ logger.empty();
412
+ logger.success('✅ Git push 完成!');
413
+ logger.empty();
414
+ }
415
+
416
+ /**
417
+ * 處理 Git Token 認證
418
+ * @param remote remote 名稱
419
+ * @param branch 分支名稱
420
+ * @returns 是否成功使用 Token 認證
421
+ */
422
+ async function handleGitTokenAuth(remote: string, branch: string): Promise<boolean> {
423
+ const tokens = loadTokens();
424
+
425
+ // 若無 Token
426
+ if (tokens.tokens.length === 0) {
427
+ logger.info('尚未儲存任何 Git Token');
428
+ const { token, name } = await import('inquirer').then((mod) =>
429
+ mod.default.prompt([
430
+ {
431
+ type: 'input',
432
+ name: 'name',
433
+ message: '為 Token 設定名稱:',
434
+ default: 'GitHub 個人',
435
+ validate: (input: string) => {
436
+ if (!input.trim()) {
437
+ return '名稱不能為空';
438
+ }
439
+ return true;
440
+ },
441
+ },
442
+ {
443
+ type: 'password',
444
+ name: 'token',
445
+ message: '請輸入 Git Token:',
446
+ validate: (input: string) => {
447
+ if (!input.trim()) {
448
+ return 'Token 不能為空';
449
+ }
450
+ return true;
451
+ },
452
+ },
453
+ ])
454
+ );
455
+
456
+ const newToken = addToken(name, token);
457
+ logger.success('✓ Token 已保存');
458
+
459
+ // 重新推送(使用 Token)
460
+ logger.info('正在使用新 Token 重新推送...');
461
+ const pushResult = await gitPush(remote, branch, undefined, newToken.token);
462
+ if (!pushResult.success) {
463
+ logger.error(`推送失敗:${pushResult.message}`);
464
+ if (pushResult.output) {
465
+ logger.info(pushResult.output);
466
+ }
467
+ return false;
468
+ }
469
+ logger.success('✓ 推送成功');
470
+ return true;
471
+ }
472
+
473
+ // 若只有 1 個 Token
474
+ if (tokens.tokens.length === 1) {
475
+ const firstToken = tokens.tokens[0];
476
+ if (!firstToken) {
477
+ return false;
478
+ }
479
+ const { useExisting } = await import('inquirer').then((mod) =>
480
+ mod.default.prompt([
481
+ {
482
+ type: 'confirm',
483
+ name: 'useExisting',
484
+ message: `是否使用已儲存的 Token「${firstToken.name}」?`,
485
+ default: true,
486
+ },
487
+ ])
488
+ );
489
+
490
+ if (useExisting) {
491
+ // 使用已存在的 Token 重新推送
492
+ logger.info('正在使用已儲存的 Token 重新推送...');
493
+ const pushResult = await gitPush(remote, branch, undefined, firstToken.token);
494
+ if (!pushResult.success) {
495
+ logger.error(`推送失敗:${pushResult.message}`);
496
+ if (pushResult.output) {
497
+ logger.info(pushResult.output);
498
+ }
499
+ return false;
500
+ }
501
+ logger.success('✓ 推送成功');
502
+ return true;
503
+ } else {
504
+ // 不使用原 Token,詢問新 Token
505
+ const { token, name } = await import('inquirer').then((mod) =>
506
+ mod.default.prompt([
507
+ {
508
+ type: 'input',
509
+ name: 'name',
510
+ message: '為 Token 設定名稱:',
511
+ default: 'GitHub 個人',
512
+ validate: (input: string) => {
513
+ if (!input.trim()) {
514
+ return '名稱不能為空';
515
+ }
516
+ return true;
517
+ },
518
+ },
519
+ {
520
+ type: 'password',
521
+ name: 'token',
522
+ message: '請輸入 Git Token:',
523
+ validate: (input: string) => {
524
+ if (!input.trim()) {
525
+ return 'Token 不能為空';
526
+ }
527
+ return true;
528
+ },
529
+ },
530
+ ])
531
+ );
532
+
533
+ const newToken = addToken(name, token);
534
+ logger.success('✓ Token 已保存');
535
+
536
+ // 重新推送
537
+ logger.info('正在使用新 Token 重新推送...');
538
+ const pushResult = await gitPush(remote, branch, undefined, newToken.token);
539
+ if (!pushResult.success) {
540
+ logger.error(`推送失敗:${pushResult.message}`);
541
+ if (pushResult.output) {
542
+ logger.info(pushResult.output);
543
+ }
544
+ return false;
545
+ }
546
+ logger.success('✓ 推送成功');
547
+ return true;
548
+ }
549
+ }
550
+
551
+ // 若有多個 Token,顯示列表讓用戶選擇
552
+ const tokenList = getTokenList();
553
+ const { selectedTokenId } = await import('inquirer').then((mod) =>
554
+ mod.default.prompt([
555
+ {
556
+ type: 'rawlist',
557
+ name: 'selectedTokenId',
558
+ message: '選擇要使用的 Git Token:',
559
+ choices: [
560
+ ...tokenList.map((t) => ({
561
+ name: t.name,
562
+ value: t.id,
563
+ })),
564
+ {
565
+ name: '➕ 使用新 Token',
566
+ value: 'new',
567
+ },
568
+ {
569
+ name: '❌ 退出',
570
+ value: 'exit',
571
+ },
572
+ ],
573
+ },
574
+ ])
575
+ );
576
+
577
+ if (selectedTokenId === 'exit') {
578
+ logger.info('已取消推送');
579
+ return false;
580
+ }
581
+
582
+ if (selectedTokenId === 'new') {
583
+ // 使用新 Token
584
+ const { token, name } = await import('inquirer').then((mod) =>
585
+ mod.default.prompt([
586
+ {
587
+ type: 'input',
588
+ name: 'name',
589
+ message: '為 Token 設定名稱:',
590
+ default: 'GitHub 個人',
591
+ validate: (input: string) => {
592
+ if (!input.trim()) {
593
+ return '名稱不能為空';
594
+ }
595
+ return true;
596
+ },
597
+ },
598
+ {
599
+ type: 'password',
600
+ name: 'token',
601
+ message: '請輸入 Git Token:',
602
+ validate: (input: string) => {
603
+ if (!input.trim()) {
604
+ return 'Token 不能為空';
605
+ }
606
+ return true;
607
+ },
608
+ },
609
+ ])
610
+ );
611
+
612
+ const newToken = addToken(name, token);
613
+ logger.success('✓ Token 已保存');
614
+
615
+ // 重新推送
616
+ logger.info('正在使用新 Token 重新推送...');
617
+ const pushResult = await gitPush(remote, branch, undefined, newToken.token);
618
+ if (!pushResult.success) {
619
+ logger.error(`推送失敗:${pushResult.message}`);
620
+ if (pushResult.output) {
621
+ logger.info(pushResult.output);
622
+ }
623
+ return false;
624
+ }
625
+ logger.success('✓ 推送成功');
626
+ return true;
627
+ }
628
+
629
+ // 使用已選擇的 Token
630
+ const selectedToken = tokens.tokens.find((t) => t.id === selectedTokenId);
631
+ if (selectedToken) {
632
+ logger.info(`使用 Token「${selectedToken.name}」重新推送...`);
633
+ const pushResult = await gitPush(remote, branch, undefined, selectedToken.token);
634
+ if (!pushResult.success) {
635
+ logger.error(`推送失敗:${pushResult.message}`);
636
+ if (pushResult.output) {
637
+ logger.info(pushResult.output);
638
+ }
639
+ return false;
640
+ }
641
+ logger.success('✓ 推送成功');
642
+ return true;
643
+ }
644
+
645
+ return false;
646
+ }
647
+
648
+ /**
649
+ * 處理 Git 克隆操作
650
+ */
651
+ async function handleGitClone(): Promise<void> {
652
+ logger.empty();
653
+ logger.icon('📥', '克隆 Git 倉庫');
654
+ logger.divider();
655
+ logger.empty();
656
+
657
+ // 詢問倉庫 URL
658
+ const { url } = await import('inquirer').then((mod) =>
659
+ mod.default.prompt([
660
+ {
661
+ type: 'input',
662
+ name: 'url',
663
+ message: '請輸入 Git 倉庫 URL:',
664
+ default: '',
665
+ validate: (input: string) => {
666
+ if (!input.trim()) {
667
+ return '倉庫 URL 不能為空';
668
+ }
669
+ // 簡單驗證 URL 格式
670
+ const urlPattern = /^(https?:\/\/|git@|ssh:\/\/)/;
671
+ if (!urlPattern.test(input.trim())) {
672
+ return '請輸入有效的 Git URL(以 https://、git@ 或 ssh:// 開頭)';
673
+ }
674
+ return true;
675
+ },
676
+ },
677
+ ])
678
+ );
679
+
680
+ // 詢問目標目錄
681
+ const { directory } = await import('inquirer').then((mod) =>
682
+ mod.default.prompt([
683
+ {
684
+ type: 'input',
685
+ name: 'directory',
686
+ message: '目標目錄名稱:',
687
+ default: url.split('/').pop()?.replace('.git', '') || '',
688
+ validate: (input: string) => {
689
+ if (!input.trim()) {
690
+ return '目錄名稱不能為空';
691
+ }
692
+ return true;
693
+ },
694
+ },
695
+ ])
696
+ );
697
+
698
+ // 確認克隆
699
+ const { confirm } = await import('inquirer').then((mod) =>
700
+ mod.default.prompt([
701
+ {
702
+ type: 'confirm',
703
+ name: 'confirm',
704
+ message: `確認克隆到目錄 "${directory}"?`,
705
+ default: true,
706
+ },
707
+ ])
708
+ );
709
+
710
+ if (!confirm) {
711
+ logger.info('已取消克隆');
712
+ return;
713
+ }
714
+
715
+ // 執行 git clone
716
+ logger.info(`正在克隆 ${url} 到 ${directory}...`);
717
+ const cloneResult = await gitClone(url, directory);
718
+
719
+ if (!cloneResult.success) {
720
+ logger.error(`克隆失敗:${cloneResult.message}`);
721
+ if (cloneResult.output) {
722
+ logger.info(cloneResult.output);
723
+ }
724
+ process.exit(1);
725
+ }
726
+
727
+ logger.success('✓ 克隆成功');
728
+ logger.empty();
729
+ logger.success('✅ Git clone 完成!');
730
+ logger.empty();
731
+ logger.icon('📂', `專案位置:${directory}`);
732
+ logger.empty();
733
+ }
734
+
735
+ /**
736
+ * 管理 Git Token
737
+ */
738
+ async function handleManageGitToken(): Promise<void> {
739
+ let running = true;
740
+
741
+ while (running) {
742
+ logger.empty();
743
+ logger.icon('🔑', '管理 Git Token');
744
+ logger.divider();
745
+ logger.empty();
746
+
747
+ const tokenList = getTokenList();
748
+
749
+ if (tokenList.length === 0) {
750
+ logger.info('目前沒有已儲存的 Token');
751
+ } else {
752
+ logger.info(`目前共有 ${tokenList.length} 個 Token:`);
753
+ tokenList.forEach((t, index) => {
754
+ logger.info(` ${index + 1}. ${t.name}`);
755
+ });
756
+ logger.empty();
757
+ }
758
+
759
+ const { action } = await import('inquirer').then((mod) =>
760
+ mod.default.prompt([
761
+ {
762
+ type: 'rawlist',
763
+ name: 'action',
764
+ message: '選擇操作:',
765
+ choices: [
766
+ { name: '➕ 新增 Token', value: 'add' },
767
+ { name: '🗑️ 刪除 Token', value: 'delete' },
768
+ { name: '✏️ 編輯 Token 名稱', value: 'edit' },
769
+ { name: '❌ 返回主選單', value: 'back' },
770
+ ],
771
+ },
772
+ ])
773
+ );
774
+
775
+ switch (action) {
776
+ case 'add':
777
+ await handleAddToken();
778
+ break;
779
+ case 'delete':
780
+ await handleDeleteToken();
781
+ break;
782
+ case 'edit':
783
+ await handleEditToken();
784
+ break;
785
+ case 'back':
786
+ logger.info('返回主選單');
787
+ running = false;
788
+ break;
789
+ default:
790
+ logger.error('無效的操作');
791
+ }
792
+ }
793
+ }
794
+
795
+ /**
796
+ * 新增 Token
797
+ */
798
+ async function handleAddToken(): Promise<void> {
799
+ logger.empty();
800
+ logger.icon('➕', '新增 Token');
801
+ logger.divider();
802
+ logger.empty();
803
+
804
+ const { name } = await import('inquirer').then((mod) =>
805
+ mod.default.prompt([
806
+ {
807
+ type: 'input',
808
+ name: 'name',
809
+ message: '為 Token 設定名稱:',
810
+ default: 'GitHub 個人',
811
+ validate: (input: string) => {
812
+ if (!input.trim()) {
813
+ return '名稱不能為空';
814
+ }
815
+ return true;
816
+ },
817
+ },
818
+ ])
819
+ );
820
+
821
+ const { token } = await import('inquirer').then((mod) =>
822
+ mod.default.prompt([
823
+ {
824
+ type: 'password',
825
+ name: 'token',
826
+ message: '請輸入 Git Token:',
827
+ validate: (input: string) => {
828
+ if (!input.trim()) {
829
+ return 'Token 不能為空';
830
+ }
831
+ return true;
832
+ },
833
+ },
834
+ ])
835
+ );
836
+
837
+ addToken(name, token);
838
+ logger.success(`✓ Token「${name}」已保存`);
839
+ logger.empty();
840
+ }
841
+
842
+ /**
843
+ * 刪除 Token
844
+ */
845
+ async function handleDeleteToken(): Promise<void> {
846
+ logger.empty();
847
+ logger.icon('🗑️', '刪除 Token');
848
+ logger.divider();
849
+ logger.empty();
850
+
851
+ const tokenList = getTokenList();
852
+
853
+ if (tokenList.length === 0) {
854
+ logger.warn('目前沒有已儲存的 Token');
855
+ return;
856
+ }
857
+
858
+ const { tokenId } = await import('inquirer').then((mod) =>
859
+ mod.default.prompt([
860
+ {
861
+ type: 'rawlist',
862
+ name: 'tokenId',
863
+ message: '選擇要刪除的 Token:',
864
+ choices: tokenList.map((t) => ({
865
+ name: t.name,
866
+ value: t.id,
867
+ })),
868
+ },
869
+ ])
870
+ );
871
+
872
+ const deleted = deleteToken(tokenId);
873
+ if (deleted) {
874
+ logger.success('✓ Token 已刪除');
875
+ } else {
876
+ logger.error('刪除失敗');
877
+ }
878
+ logger.empty();
879
+ }
880
+
881
+ /**
882
+ * 編輯 Token 名稱
883
+ */
884
+ async function handleEditToken(): Promise<void> {
885
+ logger.empty();
886
+ logger.icon('✏️', '編輯 Token 名稱');
887
+ logger.divider();
888
+ logger.empty();
889
+
890
+ const tokenList = getTokenList();
891
+
892
+ if (tokenList.length === 0) {
893
+ logger.warn('目前沒有已儲存的 Token');
894
+ return;
895
+ }
896
+
897
+ const { tokenId } = await import('inquirer').then((mod) =>
898
+ mod.default.prompt([
899
+ {
900
+ type: 'rawlist',
901
+ name: 'tokenId',
902
+ message: '選擇要編輯的 Token:',
903
+ choices: tokenList.map((t) => ({
904
+ name: t.name,
905
+ value: t.id,
906
+ })),
907
+ },
908
+ ])
909
+ );
910
+
911
+ const { newName } = await import('inquirer').then((mod) =>
912
+ mod.default.prompt([
913
+ {
914
+ type: 'input',
915
+ name: 'newName',
916
+ message: '輸入新名稱:',
917
+ validate: (input: string) => {
918
+ if (!input.trim()) {
919
+ return '名稱不能為空';
920
+ }
921
+ return true;
922
+ },
923
+ },
924
+ ])
925
+ );
926
+
927
+ const updated = updateTokenName(tokenId, newName);
928
+ if (updated) {
929
+ logger.success(`✓ Token 名稱已更新為「${newName}」`);
930
+ } else {
931
+ logger.error('更新失敗');
932
+ }
933
+ logger.empty();
934
+ }
935
+
936
+ /**
937
+ * 處理取消 Git 倉庫操作
938
+ */
939
+ /**
940
+ * 檢查是否有未提交的更改
941
+ */
942
+ async function checkForUncommittedChanges(): Promise<boolean> {
943
+ try {
944
+ const { stdout } = await execa('git', ['status', '--porcelain']);
945
+ return stdout.trim().length > 0;
946
+ } catch {
947
+ return false;
948
+ }
949
+ }
950
+
951
+ async function handleRemoveGitRepo(): Promise<void> {
952
+ logger.empty();
953
+ logger.icon('🗑️', '取消 Git 倉庫');
954
+ logger.divider();
955
+ logger.empty();
956
+
957
+ // 檢查是否為 Git 倉庫
958
+ const isGit = await isGitRepository();
959
+ if (!isGit) {
960
+ logger.warn('此目錄不是 Git 倉庫');
961
+ return;
962
+ }
963
+
964
+ // 確認刪除
965
+ const { confirm } = await import('inquirer').then((mod) =>
966
+ mod.default.prompt([
967
+ {
968
+ type: 'confirm',
969
+ name: 'confirm',
970
+ message: '確定要取消 Git 倉庫嗎?這將刪除 .git 目錄(無法恢復)',
971
+ default: false,
972
+ },
973
+ ])
974
+ );
975
+
976
+ if (!confirm) {
977
+ logger.info('已取消操作');
978
+ return;
979
+ }
980
+
981
+ // 執行刪除
982
+ const result = await removeGitRepo();
983
+
984
+ if (result.success) {
985
+ logger.success('✓ 已成功取消 Git 倉庫');
986
+ logger.empty();
987
+ logger.success('✅ 完成!');
988
+ } else {
989
+ logger.error(`取消失敗:${result.message}`);
990
+ process.exit(1);
991
+ }
992
+
993
+ logger.empty();
994
+ }
995
+
996
+ /**
997
+ * 建立 git 命令
998
+ */
999
+ export function createGitCommand(program: Command): void {
1000
+ program.command('git').description('Git 相關操作(互動式選單)').action(gitCommandHandler);
1001
+ }