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
|
@@ -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
|
+
}
|