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,408 @@
1
+ /**
2
+ * 檔案系統操作模組
3
+ * 負責目錄創建、檔案寫入、衝突處理
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { FileSystemResult, ConflictResolution } from '../types';
9
+ import { logger } from '../utils/logger';
10
+
11
+ /**
12
+ * 檢查目錄是否存在
13
+ */
14
+ export function directoryExists(dirPath: string): boolean {
15
+ try {
16
+ const stats = fs.statSync(dirPath);
17
+ return stats.isDirectory();
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * 檢查檔案是否存在
25
+ */
26
+ export function fileExists(filePath: string): boolean {
27
+ try {
28
+ const stats = fs.statSync(filePath);
29
+ return stats.isFile();
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 檢查路徑是否為空目錄
37
+ */
38
+ export function isEmptyDir(dirPath: string): boolean {
39
+ if (!directoryExists(dirPath)) {
40
+ return true;
41
+ }
42
+
43
+ try {
44
+ const files = fs.readdirSync(dirPath);
45
+ return files.length === 0;
46
+ } catch {
47
+ return true;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 創建目錄(遞迴創建)
53
+ */
54
+ export function createDirectory(dirPath: string, options?: { recursive?: boolean }): boolean {
55
+ const { recursive = true } = options || {};
56
+
57
+ try {
58
+ fs.mkdirSync(dirPath, { recursive });
59
+ logger.debug(`目錄已創建:${dirPath}`);
60
+ return true;
61
+ } catch (error: unknown) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ logger.error(`創建目錄失敗:${message}`);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * 寫入檔案內容
70
+ */
71
+ export function writeFile(
72
+ filePath: string,
73
+ content: string,
74
+ options?: { encoding?: BufferEncoding; overwrite?: boolean }
75
+ ): boolean {
76
+ const { encoding = 'utf-8', overwrite = true } = options || {};
77
+
78
+ try {
79
+ // 如果檔案已存在且不允許覆蓋,則抛出錯誤
80
+ if (!overwrite && fileExists(filePath)) {
81
+ throw new Error(`檔案已存在:${filePath}`);
82
+ }
83
+
84
+ // 確保父目錄存在
85
+ const dirPath = path.dirname(filePath);
86
+ if (!directoryExists(dirPath)) {
87
+ createDirectory(dirPath, { recursive: true });
88
+ }
89
+
90
+ fs.writeFileSync(filePath, content, { encoding });
91
+ logger.debug(`檔案已寫入:${filePath}`);
92
+ return true;
93
+ } catch (error: unknown) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ logger.error(`寫入檔案失敗:${message}`);
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 讀取檔案內容
102
+ */
103
+ export function readFile(filePath: string, options?: { encoding?: BufferEncoding }): string | null {
104
+ const { encoding = 'utf-8' } = options || {};
105
+
106
+ try {
107
+ const content = fs.readFileSync(filePath, { encoding });
108
+ logger.debug(`檔案已讀取:${filePath}`);
109
+ return content;
110
+ } catch (error: unknown) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ logger.error(`讀取檔案失敗:${message}`);
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 複製檔案
119
+ */
120
+ export function copyFile(
121
+ srcPath: string,
122
+ destPath: string,
123
+ options?: { overwrite?: boolean }
124
+ ): boolean {
125
+ const { overwrite = false } = options || {};
126
+
127
+ try {
128
+ // 如果目標檔案已存在且不允許覆蓋,則抛出錯誤
129
+ if (!overwrite && fileExists(destPath)) {
130
+ throw new Error(`目標檔案已存在:${destPath}`);
131
+ }
132
+
133
+ // 確保目標目錄存在
134
+ const destDir = path.dirname(destPath);
135
+ if (!directoryExists(destDir)) {
136
+ createDirectory(destDir, { recursive: true });
137
+ }
138
+
139
+ fs.copyFileSync(srcPath, destPath);
140
+ logger.debug(`檔案已複製:${srcPath} -> ${destPath}`);
141
+ return true;
142
+ } catch (error: unknown) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ logger.error(`複製檔案失敗:${message}`);
145
+ return false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 複製目錄(遞迴)
151
+ */
152
+ export function copyDirectory(
153
+ srcDir: string,
154
+ destDir: string,
155
+ options?: { overwrite?: boolean }
156
+ ): { success: boolean; files: string[] } {
157
+ const { overwrite = false } = options || {};
158
+ const files: string[] = [];
159
+
160
+ try {
161
+ if (!directoryExists(srcDir)) {
162
+ throw new Error(`來源目錄不存在:${srcDir}`);
163
+ }
164
+
165
+ // 創建目標目錄
166
+ if (!directoryExists(destDir)) {
167
+ createDirectory(destDir, { recursive: true });
168
+ }
169
+
170
+ // 讀取來源目錄
171
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
172
+
173
+ for (const entry of entries) {
174
+ const srcPath = path.join(srcDir, entry.name);
175
+ const destPath = path.join(destDir, entry.name);
176
+
177
+ if (entry.isDirectory()) {
178
+ // 遞迴複製子目錄
179
+ const result = copyDirectory(srcPath, destPath, { overwrite });
180
+ if (result.success) {
181
+ files.push(...result.files);
182
+ }
183
+ } else {
184
+ // 複製檔案
185
+ if (copyFile(srcPath, destPath, { overwrite })) {
186
+ files.push(destPath);
187
+ }
188
+ }
189
+ }
190
+
191
+ logger.debug(`目錄已複製:${srcDir} -> ${destDir} (${files.length} 個檔案)`);
192
+ return { success: true, files };
193
+ } catch (error: unknown) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ logger.error(`複製目錄失敗:${message}`);
196
+ return { success: false, files: [] };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * 刪除檔案
202
+ */
203
+ export function deleteFile(filePath: string): boolean {
204
+ try {
205
+ if (fileExists(filePath)) {
206
+ fs.unlinkSync(filePath);
207
+ logger.debug(`檔案已刪除:${filePath}`);
208
+ }
209
+ return true;
210
+ } catch (error: unknown) {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ logger.error(`刪除檔案失敗:${message}`);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * 刪除目錄(遞迴)
219
+ */
220
+ export function deleteDirectory(dirPath: string): boolean {
221
+ try {
222
+ if (directoryExists(dirPath)) {
223
+ fs.rmSync(dirPath, { recursive: true, force: true });
224
+ logger.debug(`目錄已刪除:${dirPath}`);
225
+ }
226
+ return true;
227
+ } catch (error: unknown) {
228
+ const message = error instanceof Error ? error.message : String(error);
229
+ logger.error(`刪除目錄失敗:${message}`);
230
+ return false;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 檢查並處理目錄衝突
236
+ * 如果目錄已存在且不為空,則需要用戶確認
237
+ */
238
+ export function checkDirectoryConflict(
239
+ dirPath: string,
240
+ onConflict: (resolution: ConflictResolution) => void
241
+ ): { hasConflict: boolean; resolution?: ConflictResolution } {
242
+ // 目錄不存在,沒有衝突
243
+ if (!directoryExists(dirPath)) {
244
+ return { hasConflict: false };
245
+ }
246
+
247
+ // 目錄存在但為空,沒有衝突
248
+ if (isEmptyDir(dirPath)) {
249
+ return { hasConflict: false };
250
+ }
251
+
252
+ // 目錄存在且不為空,需要處理衝突
253
+ const resolution: ConflictResolution = {
254
+ action: 'abort', // 預設行為
255
+ };
256
+
257
+ // 調用回調函數讓用戶決定
258
+ onConflict(resolution);
259
+
260
+ return { hasConflict: true, resolution };
261
+ }
262
+
263
+ /**
264
+ * 創建專案目錄結構
265
+ */
266
+ export function createProjectStructure(targetDir: string, projectName: string): FileSystemResult {
267
+ const files: string[] = [];
268
+ const directories: string[] = [];
269
+
270
+ try {
271
+ // 創建主目錄
272
+ if (!createDirectory(targetDir)) {
273
+ return {
274
+ success: false,
275
+ error: '無法創建專案目錄',
276
+ };
277
+ }
278
+ directories.push(targetDir);
279
+
280
+ // 創建基礎目錄結構
281
+ const subDirs = ['src', 'public', 'tests'];
282
+ for (const dir of subDirs) {
283
+ const dirPath = path.join(targetDir, dir);
284
+ if (createDirectory(dirPath)) {
285
+ directories.push(dirPath);
286
+ }
287
+ }
288
+
289
+ // 創建 .gitignore
290
+ const gitignorePath = path.join(targetDir, '.gitignore');
291
+ const gitignoreContent = `# 依賴
292
+ node_modules/
293
+ dist/
294
+
295
+ # 環境變數
296
+ .env
297
+ .env.local
298
+ .env.*.local
299
+
300
+ # 日誌
301
+ *.log
302
+ npm-debug.log*
303
+
304
+ # 操作系統
305
+ .DS_Store
306
+ Thumbs.db
307
+
308
+ # IDE
309
+ .vscode/
310
+ .idea/
311
+ *.swp
312
+ *.swo
313
+ `;
314
+ if (writeFile(gitignorePath, gitignoreContent)) {
315
+ files.push(gitignorePath);
316
+ }
317
+
318
+ // 創建 README.md
319
+ const readmePath = path.join(targetDir, 'README.md');
320
+ const readmeContent = `# ${projectName}
321
+
322
+ ## 專案說明
323
+
324
+ 這是一個 Cloudflare 專案,使用 create-cf-project 腳手架工具建立。
325
+
326
+ ## 快速開始
327
+
328
+ \`\`\`bash
329
+ # 安裝依賴
330
+ npm install
331
+
332
+ # 開發模式
333
+ npm run dev
334
+
335
+ # 建置專案
336
+ npm run build
337
+ \`\`\`
338
+
339
+ ## 功能特色
340
+
341
+ - ☁️ 基於 Cloudflare 平台
342
+ - 📦 使用 TypeScript 開發
343
+ - ⚡ 快速部署
344
+
345
+ ## 授權
346
+
347
+ MIT License
348
+ `;
349
+ if (writeFile(readmePath, readmeContent)) {
350
+ files.push(readmePath);
351
+ }
352
+
353
+ // 創建 .gitignore 提示
354
+ logger.success('專案結構已建立');
355
+
356
+ return {
357
+ success: true,
358
+ files,
359
+ directories,
360
+ };
361
+ } catch (error: unknown) {
362
+ const message = error instanceof Error ? error.message : String(error);
363
+ return {
364
+ success: false,
365
+ error: message || '建立專案結構時發生錯誤',
366
+ };
367
+ }
368
+ }
369
+
370
+ /**
371
+ * 獲取目錄內容列表
372
+ */
373
+ export function listDirectory(dirPath: string): string[] {
374
+ try {
375
+ if (!directoryExists(dirPath)) {
376
+ return [];
377
+ }
378
+ return fs.readdirSync(dirPath);
379
+ } catch (error: unknown) {
380
+ const message = error instanceof Error ? error.message : String(error);
381
+ logger.error(`讀取目錄列表失敗:${message}`);
382
+ return [];
383
+ }
384
+ }
385
+
386
+ /**
387
+ * 獲取檔案大小(位元組)
388
+ */
389
+ export function getFileSize(filePath: string): number {
390
+ try {
391
+ const stats = fs.statSync(filePath);
392
+ return stats.size;
393
+ } catch {
394
+ return 0;
395
+ }
396
+ }
397
+
398
+ /**
399
+ * 獲取檔案修改時間
400
+ */
401
+ export function getFileModifiedDate(filePath: string): Date | null {
402
+ try {
403
+ const stats = fs.statSync(filePath);
404
+ return stats.mtime;
405
+ } catch {
406
+ return null;
407
+ }
408
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Git Token 管理模組
3
+ * 負責管理私人倉庫推送所需的 Git Token
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+ import { v4 as uuidv4 } from 'uuid';
10
+ import { logger } from '../utils/logger';
11
+
12
+ /**
13
+ * Git Token 資料結構
14
+ */
15
+ export interface GitToken {
16
+ id: string;
17
+ name: string;
18
+ token: string;
19
+ createdAt: string;
20
+ }
21
+
22
+ /**
23
+ * Git Token 配置結構
24
+ */
25
+ export interface GitTokenConfig {
26
+ tokens: GitToken[];
27
+ }
28
+
29
+ /**
30
+ * 獲取配置檔路徑
31
+ * 路徑:~/.config/cf-yoyo/git-token.json(跨平台)
32
+ * 使用 os.homedir() 確保在 Windows、macOS、Linux 上都能正確獲取用戶目錄
33
+ */
34
+ export function getTokenConfigPath(): string {
35
+ const userHome = homedir();
36
+ if (!userHome) {
37
+ throw new Error('無法獲取用戶目錄路徑');
38
+ }
39
+ const configDir = join(userHome, '.config', 'cf-yoyo');
40
+ const configPath = join(configDir, 'git-token.json');
41
+ return configPath;
42
+ }
43
+
44
+ /**
45
+ * 確保配置目錄存在
46
+ */
47
+ function ensureConfigDir(): void {
48
+ const configPath = getTokenConfigPath();
49
+ const configDir = join(configPath, '..');
50
+
51
+ if (!existsSync(configDir)) {
52
+ mkdirSync(configDir, { recursive: true });
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 載入已存 Token
58
+ * @returns GitTokenConfig 配置對象
59
+ */
60
+ export function loadTokens(): GitTokenConfig {
61
+ const configPath = getTokenConfigPath();
62
+
63
+ if (!existsSync(configPath)) {
64
+ return { tokens: [] };
65
+ }
66
+
67
+ try {
68
+ const content = readFileSync(configPath, 'utf-8');
69
+ const config = JSON.parse(content) as GitTokenConfig;
70
+ return config;
71
+ } catch (error) {
72
+ logger.warn('讀取 Token 配置失敗,返回空配置');
73
+ return { tokens: [] };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 保存 Token 配置
79
+ * @param tokens Token 配置對象
80
+ */
81
+ export function saveTokens(tokens: GitTokenConfig): void {
82
+ ensureConfigDir();
83
+ const configPath = getTokenConfigPath();
84
+ writeFileSync(configPath, JSON.stringify(tokens, null, 2), 'utf-8');
85
+ }
86
+
87
+ /**
88
+ * 新增 Token
89
+ * @param name Token 名稱
90
+ * @param token Token 值
91
+ * @returns 新增的 GitToken 對象
92
+ */
93
+ export function addToken(name: string, token: string): GitToken {
94
+ const config = loadTokens();
95
+
96
+ const newToken: GitToken = {
97
+ id: uuidv4(),
98
+ name,
99
+ token,
100
+ createdAt: new Date().toISOString(),
101
+ };
102
+
103
+ config.tokens.push(newToken);
104
+ saveTokens(config);
105
+
106
+ return newToken;
107
+ }
108
+
109
+ /**
110
+ * 刪除 Token
111
+ * @param id Token ID
112
+ * @returns 是否刪除成功
113
+ */
114
+ export function deleteToken(id: string): boolean {
115
+ const config = loadTokens();
116
+ const initialLength = config.tokens.length;
117
+ config.tokens = config.tokens.filter((tokenItem) => tokenItem.id !== id);
118
+
119
+ if (config.tokens.length < initialLength) {
120
+ saveTokens(config);
121
+ return true;
122
+ }
123
+
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * 更新 Token
129
+ * @param id Token ID
130
+ * @param name Token 名稱
131
+ * @param token Token 值
132
+ * @returns 是否更新成功
133
+ */
134
+ export function updateToken(id: string, name: string, token: string): boolean {
135
+ const config = loadTokens();
136
+ const tokenIndex = config.tokens.findIndex((tokenItem) => tokenItem.id === id);
137
+
138
+ if (tokenIndex === -1) {
139
+ return false;
140
+ }
141
+
142
+ const existingToken = config.tokens[tokenIndex];
143
+ if (!existingToken) {
144
+ return false;
145
+ }
146
+
147
+ config.tokens[tokenIndex] = {
148
+ ...existingToken,
149
+ name,
150
+ token,
151
+ };
152
+
153
+ saveTokens(config);
154
+ return true;
155
+ }
156
+
157
+ /**
158
+ * 更新 Token 名稱
159
+ * @param id Token ID
160
+ * @param name 新名稱
161
+ * @returns 是否更新成功
162
+ */
163
+ export function updateTokenName(id: string, name: string): boolean {
164
+ const config = loadTokens();
165
+ const tokenIndex = config.tokens.findIndex((tokenItem) => tokenItem.id === id);
166
+
167
+ if (tokenIndex === -1) {
168
+ return false;
169
+ }
170
+
171
+ const existingToken = config.tokens[tokenIndex];
172
+ if (!existingToken) {
173
+ return false;
174
+ }
175
+
176
+ existingToken.name = name;
177
+ saveTokens(config);
178
+ return true;
179
+ }
180
+
181
+ /**
182
+ * 選擇 Token(互動式)
183
+ * 根據不同情況返回對應的 Token
184
+ * - 若無 Token:返回 null
185
+ * - 若有 1 個 Token:返回該 Token
186
+ * - 若有 2+ 個 Token:讓用戶選擇
187
+ * @returns 選擇的 GitToken 或 null
188
+ */
189
+ export async function selectToken(): Promise<GitToken | null> {
190
+ const config = loadTokens();
191
+
192
+ // 若無 Token
193
+ if (config.tokens.length === 0) {
194
+ return null;
195
+ }
196
+
197
+ // 若只有 1 個 Token
198
+ if (config.tokens.length === 1) {
199
+ const firstToken = config.tokens[0];
200
+ if (!firstToken) {
201
+ return null;
202
+ }
203
+ return firstToken;
204
+ }
205
+
206
+ // 若有多個 Token,讓用戶選擇
207
+ const { selectedToken } = await import('inquirer').then((mod) =>
208
+ mod.default.prompt([
209
+ {
210
+ type: 'list',
211
+ name: 'selectedToken',
212
+ message: '選擇要使用的 Git Token:',
213
+ choices: [
214
+ ...config.tokens.map((tokenItem) => ({
215
+ name: tokenItem.name,
216
+ value: tokenItem.id,
217
+ })),
218
+ {
219
+ name: '❌ 退出',
220
+ value: 'exit',
221
+ },
222
+ ],
223
+ },
224
+ ])
225
+ );
226
+
227
+ if (selectedToken === 'exit') {
228
+ return null;
229
+ }
230
+
231
+ const selectedTokenItem = config.tokens.find((tokenItem) => tokenItem.id === selectedToken);
232
+ if (!selectedTokenItem) {
233
+ return null;
234
+ }
235
+ return selectedTokenItem;
236
+ }
237
+
238
+ /**
239
+ * 獲取所有 Token 名稱列表(用於顯示,不顯示 Token 值)
240
+ */
241
+ export function getTokenList(): { id: string; name: string; createdAt: string }[] {
242
+ const config = loadTokens();
243
+ return config.tokens.map((tokenItem) => ({
244
+ id: tokenItem.id,
245
+ name: tokenItem.name,
246
+ createdAt: tokenItem.createdAt,
247
+ }));
248
+ }