@cc123hh/gitee-cli 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +236 -3
- package/bin/gitee.js +838 -0
- package/lib/args.js +165 -0
- package/lib/config.js +128 -0
- package/lib/httputil.js +208 -0
- package/package.json +24 -6
package/bin/gitee.js
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* gitee — Gitee 仓库管理命令行工具(纯 Node 零依赖,调 Gitee OpenAPI v5)
|
|
5
|
+
*
|
|
6
|
+
* 用法示例:
|
|
7
|
+
* gitee list # 列出我的仓库(按更新时间排序)
|
|
8
|
+
* gitee info cc123hh/imgen-cli # 查看某个仓库
|
|
9
|
+
* gitee create my-tool -d "说明" # 新建仓库(默认私有)
|
|
10
|
+
* gitee public my-tool # 设为公开
|
|
11
|
+
* gitee delete my-tool -y # 删除仓库
|
|
12
|
+
* gitee push --public # 当前目录建仓并推送
|
|
13
|
+
*
|
|
14
|
+
* 配置优先级(高 → 低):
|
|
15
|
+
* 1. 命令行参数(--token / --api / --proxy / --timeout,临时覆盖)
|
|
16
|
+
* 2. 内部配置文件(gitee set 写入,持久生效,推荐)
|
|
17
|
+
* 3. 环境变量(GITEE_TOKEN / GITEE_API / GITEE_PROXY)
|
|
18
|
+
* 4. 内置默认值
|
|
19
|
+
*/
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const spawnSync = require("child_process").spawnSync;
|
|
23
|
+
const readline = require("readline");
|
|
24
|
+
const { homedir, normKey, loadConfig, saveConfig, displayValue,
|
|
25
|
+
resolveValue, showConfig } = require("../lib/config.js");
|
|
26
|
+
const { requestOnce, TimeoutError } = require("../lib/httputil.js");
|
|
27
|
+
const { parse } = require("../lib/args.js");
|
|
28
|
+
|
|
29
|
+
const DEFAULT_API = "https://gitee.com/api/v5";
|
|
30
|
+
const DEFAULT_TIMEOUT = 30;
|
|
31
|
+
const EXIT_OK = 0, EXIT_USAGE = 2, EXIT_AUTH = 3, EXIT_API = 5, EXIT_GIT = 6;
|
|
32
|
+
|
|
33
|
+
const CONFIG_PATH = path.join(homedir(), ".config", "gitee", "config.json");
|
|
34
|
+
const CONFIG_KEYS = ["token", "api", "proxy", "timeout"];
|
|
35
|
+
|
|
36
|
+
// --------------------------------------------------------------------------- //
|
|
37
|
+
// 内部配置管理(gitee set / gitee unset)
|
|
38
|
+
// --------------------------------------------------------------------------- //
|
|
39
|
+
function configRows(cfg) {
|
|
40
|
+
return [
|
|
41
|
+
{ label: "token", key: "token", env: "GITEE_TOKEN", default: null, secret: true, cfg },
|
|
42
|
+
{ label: "api", key: "api", env: "GITEE_API", default: DEFAULT_API, cfg },
|
|
43
|
+
{ label: "proxy", key: "proxy", env: "GITEE_PROXY", default: null, cfg },
|
|
44
|
+
{ label: "timeout", key: "timeout", env: null, default: DEFAULT_TIMEOUT, cfg },
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cmdSetConfig(opts) {
|
|
49
|
+
if (opts.key === undefined || opts.key === null) {
|
|
50
|
+
showConfig(configRows(loadConfig(CONFIG_PATH, CONFIG_KEYS)), CONFIG_PATH);
|
|
51
|
+
return EXIT_OK;
|
|
52
|
+
}
|
|
53
|
+
if (opts.value === undefined || opts.value === null) {
|
|
54
|
+
console.error(`用法:gitee set <key> <value>(可用 key:${CONFIG_KEYS.join(" / ")},`
|
|
55
|
+
+ "不带参数列出当前配置)");
|
|
56
|
+
return EXIT_USAGE;
|
|
57
|
+
}
|
|
58
|
+
const key = normKey(opts.key, CONFIG_KEYS);
|
|
59
|
+
if (!key) {
|
|
60
|
+
console.error(`错误:未知配置项 ${JSON.stringify(opts.key)},可用:${CONFIG_KEYS.join(" / ")}`);
|
|
61
|
+
return EXIT_USAGE;
|
|
62
|
+
}
|
|
63
|
+
let value = opts.value;
|
|
64
|
+
if (key === "timeout") {
|
|
65
|
+
const n = Number(opts.value);
|
|
66
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
67
|
+
console.error(`错误:timeout 必须为正整数,收到 ${JSON.stringify(opts.value)}`);
|
|
68
|
+
return EXIT_USAGE;
|
|
69
|
+
}
|
|
70
|
+
value = n;
|
|
71
|
+
} else if (key === "api") {
|
|
72
|
+
value = opts.value.replace(/\/+$/, "");
|
|
73
|
+
}
|
|
74
|
+
const cfg = loadConfig(CONFIG_PATH, CONFIG_KEYS);
|
|
75
|
+
cfg[key] = value;
|
|
76
|
+
saveConfig(CONFIG_PATH, cfg);
|
|
77
|
+
console.log(`✅ 已写入 ${key} = ${displayValue(key === "token", value)}`);
|
|
78
|
+
console.log(` 配置文件:${CONFIG_PATH}`);
|
|
79
|
+
return EXIT_OK;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cmdUnsetConfig(opts) {
|
|
83
|
+
const key = normKey(opts.key, CONFIG_KEYS);
|
|
84
|
+
if (!key) {
|
|
85
|
+
console.error(`错误:未知配置项 ${JSON.stringify(opts.key)},可用:${CONFIG_KEYS.join(" / ")}`);
|
|
86
|
+
return EXIT_USAGE;
|
|
87
|
+
}
|
|
88
|
+
const cfg = loadConfig(CONFIG_PATH, CONFIG_KEYS);
|
|
89
|
+
if (!(key in cfg)) {
|
|
90
|
+
console.error(`错误:${opts.key} 未在内部配置中设置(配置文件:${CONFIG_PATH})`);
|
|
91
|
+
return EXIT_USAGE;
|
|
92
|
+
}
|
|
93
|
+
delete cfg[key];
|
|
94
|
+
saveConfig(CONFIG_PATH, cfg);
|
|
95
|
+
console.log(`✅ 已删除配置项 ${key}`);
|
|
96
|
+
return EXIT_OK;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --------------------------------------------------------------------------- //
|
|
100
|
+
// HTTP 客户端
|
|
101
|
+
// --------------------------------------------------------------------------- //
|
|
102
|
+
class GiteeError extends Error {}
|
|
103
|
+
|
|
104
|
+
class Client {
|
|
105
|
+
constructor(token, api, proxy, timeout) {
|
|
106
|
+
this.token = token;
|
|
107
|
+
this.api = api.replace(/\/+$/, "");
|
|
108
|
+
this.proxy = proxy;
|
|
109
|
+
this.timeout = timeout;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async _request(method, path, params, form) {
|
|
113
|
+
const q = new URLSearchParams();
|
|
114
|
+
for (const [k, v] of Object.entries(params || {})) q.set(k, String(v));
|
|
115
|
+
q.set("access_token", this.token);
|
|
116
|
+
const urlStr = `${this.api}${path}?${q.toString()}`;
|
|
117
|
+
const headers = {};
|
|
118
|
+
let body = null;
|
|
119
|
+
if (form) {
|
|
120
|
+
const f = new URLSearchParams();
|
|
121
|
+
for (const [k, v] of Object.entries(form)) f.set(k, String(v));
|
|
122
|
+
body = Buffer.from(f.toString(), "utf8");
|
|
123
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
124
|
+
}
|
|
125
|
+
let res;
|
|
126
|
+
try {
|
|
127
|
+
res = await requestOnce({
|
|
128
|
+
urlStr, method, headers, body, proxy: this.proxy, timeoutSec: this.timeout,
|
|
129
|
+
});
|
|
130
|
+
} catch (e) {
|
|
131
|
+
const reason = e instanceof TimeoutError ? "timed out" : e.message;
|
|
132
|
+
throw new GiteeError(`网络错误:${reason}(可设置 GITEE_PROXY 走代理)`);
|
|
133
|
+
}
|
|
134
|
+
const text = res.body.toString("utf8");
|
|
135
|
+
if (res.status >= 400) {
|
|
136
|
+
const detail = text.slice(0, 300);
|
|
137
|
+
if (res.status === 401 || res.status === 403) {
|
|
138
|
+
throw new GiteeError(`鉴权失败(HTTP ${res.status}):检查 GITEE_TOKEN 是否有效`
|
|
139
|
+
+ ` / 权限是否足够。${detail}`);
|
|
140
|
+
}
|
|
141
|
+
if (res.status === 404) {
|
|
142
|
+
throw new GiteeError(`资源不存在(HTTP 404):${path}`);
|
|
143
|
+
}
|
|
144
|
+
throw new GiteeError(`HTTP ${res.status}:${detail}`);
|
|
145
|
+
}
|
|
146
|
+
return text.trim() ? JSON.parse(text) : {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get(path, params) { return this._request("GET", path, params); }
|
|
150
|
+
post(path, form) { return this._request("POST", path, null, form); }
|
|
151
|
+
patch(path, form) { return this._request("PATCH", path, null, form); }
|
|
152
|
+
delete(path) { return this._request("DELETE", path); }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --------------------------------------------------------------------------- //
|
|
156
|
+
// 终端对齐辅助
|
|
157
|
+
// --------------------------------------------------------------------------- //
|
|
158
|
+
function width(text) {
|
|
159
|
+
let n = 0;
|
|
160
|
+
for (const ch of text) n += ch.codePointAt(0) > 0x2e7f ? 2 : 1;
|
|
161
|
+
return n;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pad(text, w) {
|
|
165
|
+
return text + " ".repeat(Math.max(0, w - width(text)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --------------------------------------------------------------------------- //
|
|
169
|
+
// 命令实现
|
|
170
|
+
// --------------------------------------------------------------------------- //
|
|
171
|
+
async function cmdWhoami(client) {
|
|
172
|
+
const user = await client.get("/user");
|
|
173
|
+
console.log(`${user.login} (${user.name || "未设置昵称"})`);
|
|
174
|
+
console.log(` 主页: ${user.html_url}`);
|
|
175
|
+
console.log(` 邮箱: ${user.email || "未公开"}`);
|
|
176
|
+
return EXIT_OK;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function cmdList(client, opts) {
|
|
180
|
+
const repos = await client.get("/user/repos", {
|
|
181
|
+
sort: "updated", per_page: String(opts.limit), page: "1",
|
|
182
|
+
});
|
|
183
|
+
if (!repos.length) {
|
|
184
|
+
console.log("没有仓库");
|
|
185
|
+
return EXIT_OK;
|
|
186
|
+
}
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
console.log(JSON.stringify(repos, null, 2));
|
|
189
|
+
return EXIT_OK;
|
|
190
|
+
}
|
|
191
|
+
const nameW = Math.max(...repos.map((r) => width(r.full_name || "")), 8) + 2;
|
|
192
|
+
const header = pad("仓库", nameW) + pad("可见性", 8) + " ★ 更新";
|
|
193
|
+
console.log(header);
|
|
194
|
+
console.log("-".repeat(width(header)));
|
|
195
|
+
for (const repo of repos) {
|
|
196
|
+
const vis = repo.private ? "私有" : "公开";
|
|
197
|
+
const stars = String(repo.stargazers_count ?? 0).padStart(3);
|
|
198
|
+
const updated = (repo.updated_at || "").slice(0, 16).replace("T", " ");
|
|
199
|
+
console.log(pad(repo.full_name || "", nameW) + pad(vis, 8) + `${stars} ${updated}`);
|
|
200
|
+
}
|
|
201
|
+
return EXIT_OK;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function splitRepo(name, client) {
|
|
205
|
+
// 把 'repo' 或 'owner/repo' 规范成 'owner/repo'
|
|
206
|
+
if (name.includes("/")) return name;
|
|
207
|
+
const user = await client.get("/user");
|
|
208
|
+
return `${user.login}/${name}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function cmdInfo(client, opts) {
|
|
212
|
+
const full = await splitRepo(opts.repo, client);
|
|
213
|
+
const repo = await client.get(`/repos/${full}`);
|
|
214
|
+
console.log(`仓库: ${repo.full_name}`);
|
|
215
|
+
console.log(`地址: ${repo.html_url}`);
|
|
216
|
+
console.log(`可见性: ${repo.private ? "私有" : "公开"}`);
|
|
217
|
+
console.log(`描述: ${repo.description || "(无)"}`);
|
|
218
|
+
console.log(`统计: ★ ${repo.stargazers_count ?? 0} `
|
|
219
|
+
+ `Fork ${repo.forks_count ?? 0} `
|
|
220
|
+
+ `Watch ${repo.watchers_count ?? 0}`);
|
|
221
|
+
console.log(`默认分支: ${repo.default_branch}`);
|
|
222
|
+
console.log(`更新于: ${repo.updated_at}`);
|
|
223
|
+
return EXIT_OK;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function cmdCreate(client, opts) {
|
|
227
|
+
const user = await client.get("/user");
|
|
228
|
+
const owner = user.login;
|
|
229
|
+
const form = { name: opts.name, private: opts.public ? "false" : "true" };
|
|
230
|
+
if (opts.description) form.description = opts.description;
|
|
231
|
+
const repo = await client.post("/user/repos", form);
|
|
232
|
+
const full = repo.full_name || `${owner}/${opts.name}`;
|
|
233
|
+
const actualPrivate = repo.private !== false;
|
|
234
|
+
console.log(`已创建: ${full}`);
|
|
235
|
+
if (opts.public && actualPrivate) {
|
|
236
|
+
console.log(`提示:Gitee 不允许空仓库直接设为公开,推送首个提交后执行 `
|
|
237
|
+
+ `\`gitee public ${opts.name}\` 即可。`);
|
|
238
|
+
}
|
|
239
|
+
return EXIT_OK;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function cmdSetVisibility(client, opts, privateMode) {
|
|
243
|
+
const full = await splitRepo(opts.repo, client);
|
|
244
|
+
await client.patch(`/repos/${full}`, {
|
|
245
|
+
name: full.split("/")[1],
|
|
246
|
+
private: privateMode ? "true" : "false",
|
|
247
|
+
});
|
|
248
|
+
const repo = await client.get(`/repos/${full}`);
|
|
249
|
+
console.log(`${full} 现在是${repo.private ? "私有" : "公开"}仓库`);
|
|
250
|
+
return EXIT_OK;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function ask(question) {
|
|
254
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
255
|
+
try {
|
|
256
|
+
return (await rl.question(question)).trim();
|
|
257
|
+
} finally {
|
|
258
|
+
rl.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function cmdDelete(client, opts) {
|
|
263
|
+
const full = await splitRepo(opts.repo, client);
|
|
264
|
+
if (!opts.yes) {
|
|
265
|
+
const answer = await ask(`确认删除 ${full}?(不可恢复,输入 yes 继续):`);
|
|
266
|
+
if (answer !== "yes") {
|
|
267
|
+
console.log("已取消");
|
|
268
|
+
return EXIT_OK;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
await client.delete(`/repos/${full}`);
|
|
272
|
+
console.log(`已删除 ${full}`);
|
|
273
|
+
return EXIT_OK;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function parseRepoArg(opts, client) {
|
|
277
|
+
const name = opts.repo !== undefined && opts.repo !== null
|
|
278
|
+
? opts.repo
|
|
279
|
+
: path.basename(process.cwd());
|
|
280
|
+
return splitRepo(name, client);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function cmdIssues(client, opts) {
|
|
284
|
+
const full = await parseRepoArg(opts, client);
|
|
285
|
+
const items = await client.get(`/repos/${full}/issues`, {
|
|
286
|
+
state: opts.state, per_page: String(opts.limit),
|
|
287
|
+
});
|
|
288
|
+
if (!items.length) {
|
|
289
|
+
console.log(`${full}: 没有 Issue`);
|
|
290
|
+
return EXIT_OK;
|
|
291
|
+
}
|
|
292
|
+
for (const it of items) {
|
|
293
|
+
const user = (it.user && it.user.login) || "?";
|
|
294
|
+
console.log(`#${String(it.number).padEnd(5)} [${String(it.state).padEnd(6)}] `
|
|
295
|
+
+ `${it.title} @${user}`);
|
|
296
|
+
}
|
|
297
|
+
return EXIT_OK;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function cmdPrs(client, opts) {
|
|
301
|
+
const full = await parseRepoArg(opts, client);
|
|
302
|
+
const items = await client.get(`/repos/${full}/pulls`, {
|
|
303
|
+
state: opts.state, per_page: String(opts.limit),
|
|
304
|
+
});
|
|
305
|
+
if (!items.length) {
|
|
306
|
+
console.log(`${full}: 没有 Pull Request`);
|
|
307
|
+
return EXIT_OK;
|
|
308
|
+
}
|
|
309
|
+
for (const it of items) {
|
|
310
|
+
const user = (it.user && it.user.login) || "?";
|
|
311
|
+
console.log(`#${String(it.number).padEnd(5)} [${String(it.state).padEnd(6)}] `
|
|
312
|
+
+ `${it.title} @${user}`);
|
|
313
|
+
}
|
|
314
|
+
return EXIT_OK;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Issue 编号是 Gitee 自动生成的字符串标识(如 IKVADU),不是数字
|
|
318
|
+
const ISSUE_STATE_TEXT = {
|
|
319
|
+
open: "开启", progress: "进行中", closed: "已关闭", rejected: "已拒绝",
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
function issueStateText(state) {
|
|
323
|
+
return ISSUE_STATE_TEXT[state] || state || "?";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 统一解析 issue 单条操作的位置参数:
|
|
327
|
+
// - 两个位置参数:repo + 编号
|
|
328
|
+
// - 只给一个:视为编号,仓库取当前目录同名仓库
|
|
329
|
+
// - 编号容忍 # 前缀(用户习惯写 #IKVADU)
|
|
330
|
+
function parseIssueArg(opts) {
|
|
331
|
+
let repo = opts.repo;
|
|
332
|
+
let number = opts.number;
|
|
333
|
+
const repoEmpty = repo === undefined || repo === null || repo === "";
|
|
334
|
+
const numEmpty = number === undefined || number === null || number === "";
|
|
335
|
+
if (numEmpty && repoEmpty) return null;
|
|
336
|
+
if (numEmpty) {
|
|
337
|
+
number = repo;
|
|
338
|
+
repo = undefined;
|
|
339
|
+
}
|
|
340
|
+
number = String(number).trim().replace(/^#/, "");
|
|
341
|
+
if (!number) return null;
|
|
342
|
+
return { repo: repo === undefined || repo === null ? "" : String(repo).trim(), number };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function issueTarget(opts, client) {
|
|
346
|
+
const pos = parseIssueArg(opts);
|
|
347
|
+
if (!pos) return null;
|
|
348
|
+
const full = await splitRepo(pos.repo || path.basename(process.cwd()), client);
|
|
349
|
+
return { full, number: pos.number };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function issueUsageHint(action) {
|
|
353
|
+
return `错误:缺少 Issue 编号。用法:gitee ${action} [<仓库>] <编号>`
|
|
354
|
+
+ `(仓库省略时取当前目录同名仓库)`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function printIssue(it) {
|
|
358
|
+
console.log(`编号: #${it.number}`);
|
|
359
|
+
console.log(`标题: ${it.title}`);
|
|
360
|
+
console.log(`状态: ${it.state}(${issueStateText(it.state)})`);
|
|
361
|
+
console.log(`作者: ${(it.user && it.user.login) || "?"}`);
|
|
362
|
+
const labels = (it.labels || []).map((l) => l.name || l).join(", ");
|
|
363
|
+
console.log(`标签: ${labels || "(无)"}`);
|
|
364
|
+
console.log(`评论: ${it.comments ?? 0} 条`);
|
|
365
|
+
console.log(`创建: ${it.created_at}`);
|
|
366
|
+
console.log(`更新: ${it.updated_at}`);
|
|
367
|
+
if (it.finished_at) console.log(`完成: ${it.finished_at}`);
|
|
368
|
+
console.log(`链接: ${it.html_url}`);
|
|
369
|
+
console.log("正文:");
|
|
370
|
+
console.log(it.body || "(无)");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function cmdIssue(client, opts) {
|
|
374
|
+
const target = await issueTarget(opts, client);
|
|
375
|
+
if (!target) {
|
|
376
|
+
console.error(issueUsageHint("issue view"));
|
|
377
|
+
return EXIT_USAGE;
|
|
378
|
+
}
|
|
379
|
+
const it = await issueGet(client, target.full, target.number);
|
|
380
|
+
printIssue(it);
|
|
381
|
+
return EXIT_OK;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---------- Issue 写接口(路径不含 repo 段,仓库以表单参数 repo 传) ---------- //
|
|
385
|
+
function issueOwnerRepo(full) {
|
|
386
|
+
const [owner, repoName] = full.split("/");
|
|
387
|
+
return { owner, repoName };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function issueGet(client, full, number) {
|
|
391
|
+
return client.get(`/repos/${full}/issues/${encodeURIComponent(number)}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function issuePatch(client, full, number, form) {
|
|
395
|
+
// 统一注入表单 repo:Gitee 更新接口路径不含 repo 段(与 GitHub 不同)
|
|
396
|
+
const { owner, repoName } = issueOwnerRepo(full);
|
|
397
|
+
return client.patch(`/repos/${owner}/issues/${encodeURIComponent(number)}`,
|
|
398
|
+
{ repo: repoName, ...form });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// -b 正文 与 -f 文件正文 互斥;-f 读取 UTF-8 文件内容作为正文
|
|
402
|
+
function resolveBodyArg(opts) {
|
|
403
|
+
if (opts.body !== undefined && opts.file !== undefined) {
|
|
404
|
+
console.error("错误:-b/--body 与 -f/--file 只能二选一");
|
|
405
|
+
return { error: true };
|
|
406
|
+
}
|
|
407
|
+
if (opts.file !== undefined) {
|
|
408
|
+
try {
|
|
409
|
+
return { body: fs.readFileSync(opts.file, "utf8") };
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error(`错误:无法读取正文文件 ${opts.file}:${e.message}`);
|
|
412
|
+
return { error: true };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { body: opts.body };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function cmdIssueCreate(client, opts) {
|
|
419
|
+
if (!opts.title) {
|
|
420
|
+
console.error("错误:创建 Issue 需要 -t/--title 标题");
|
|
421
|
+
return EXIT_USAGE;
|
|
422
|
+
}
|
|
423
|
+
const bodyArg = resolveBodyArg(opts);
|
|
424
|
+
if (bodyArg.error) return EXIT_USAGE;
|
|
425
|
+
const full = await parseRepoArg(opts, client);
|
|
426
|
+
const { owner, repoName } = issueOwnerRepo(full);
|
|
427
|
+
const form = { repo: repoName, title: opts.title };
|
|
428
|
+
if (bodyArg.body !== undefined && bodyArg.body !== null) form.body = bodyArg.body;
|
|
429
|
+
if (opts.labels) form.labels = opts.labels;
|
|
430
|
+
if (opts.assignee) form.assignee = opts.assignee;
|
|
431
|
+
const it = await client.post(`/repos/${owner}/issues`, form);
|
|
432
|
+
console.log(`已创建: #${it.number} ${it.title}`);
|
|
433
|
+
console.log(`链接: ${it.html_url}`);
|
|
434
|
+
return EXIT_OK;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function cmdIssueEdit(client, opts) {
|
|
438
|
+
const target = await issueTarget(opts, client);
|
|
439
|
+
if (!target) {
|
|
440
|
+
console.error(issueUsageHint("issue edit"));
|
|
441
|
+
return EXIT_USAGE;
|
|
442
|
+
}
|
|
443
|
+
const bodyArg = resolveBodyArg(opts);
|
|
444
|
+
if (bodyArg.error) return EXIT_USAGE;
|
|
445
|
+
const form = {};
|
|
446
|
+
if (opts.title !== undefined) form.title = opts.title;
|
|
447
|
+
if (bodyArg.body !== undefined && bodyArg.body !== null) form.body = bodyArg.body;
|
|
448
|
+
if (opts.state !== undefined) form.state = opts.state;
|
|
449
|
+
if (opts.labels !== undefined) form.labels = opts.labels;
|
|
450
|
+
if (opts.assignee !== undefined) form.assignee = opts.assignee;
|
|
451
|
+
// 不带任何修改参数时按查看处理,避免空 PATCH 造成误解
|
|
452
|
+
if (!Object.keys(form).length) {
|
|
453
|
+
const it = await issueGet(client, target.full, target.number);
|
|
454
|
+
printIssue(it);
|
|
455
|
+
return EXIT_OK;
|
|
456
|
+
}
|
|
457
|
+
await issuePatch(client, target.full, target.number, form);
|
|
458
|
+
const it = await issueGet(client, target.full, target.number);
|
|
459
|
+
printIssue(it);
|
|
460
|
+
return EXIT_OK;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function cmdIssueComment(client, opts) {
|
|
464
|
+
if (opts.message === undefined && opts.file === undefined) {
|
|
465
|
+
console.error("错误:评论内容用 -m/--message 给出(或 -f/--file 从文件读)");
|
|
466
|
+
return EXIT_USAGE;
|
|
467
|
+
}
|
|
468
|
+
const bodyArg = resolveBodyArg({ body: opts.message, file: opts.file });
|
|
469
|
+
if (bodyArg.error) return EXIT_USAGE;
|
|
470
|
+
if (!bodyArg.body) {
|
|
471
|
+
console.error("错误:评论内容不能为空");
|
|
472
|
+
return EXIT_USAGE;
|
|
473
|
+
}
|
|
474
|
+
const target = await issueTarget(opts, client);
|
|
475
|
+
if (!target) {
|
|
476
|
+
console.error(issueUsageHint("issue comment"));
|
|
477
|
+
return EXIT_USAGE;
|
|
478
|
+
}
|
|
479
|
+
const c = await client.post(
|
|
480
|
+
`/repos/${target.full}/issues/${encodeURIComponent(target.number)}/comments`,
|
|
481
|
+
{ body: bodyArg.body });
|
|
482
|
+
console.log(`已评论: #${target.number}(评论 id ${c.id})`);
|
|
483
|
+
if (c.html_url) console.log(`链接: ${c.html_url}`);
|
|
484
|
+
return EXIT_OK;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function cmdSetIssueState(client, opts, state, hint = "close") {
|
|
488
|
+
const target = await issueTarget(opts, client);
|
|
489
|
+
if (!target) {
|
|
490
|
+
console.error(issueUsageHint(hint));
|
|
491
|
+
return EXIT_USAGE;
|
|
492
|
+
}
|
|
493
|
+
const { full, number } = target;
|
|
494
|
+
await issuePatch(client, full, number, { state });
|
|
495
|
+
const it = await issueGet(client, full, number);
|
|
496
|
+
console.log(`#${it.number} [${issueStateText(it.state)}] ${it.title}`);
|
|
497
|
+
return EXIT_OK;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function runGit(args, cwd) {
|
|
501
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
502
|
+
return { code: r.status, out: r.stdout || "", err: r.stderr || "" };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function cmdPush(client, opts) {
|
|
506
|
+
const cwd = process.cwd();
|
|
507
|
+
const dotGit = path.join(cwd, ".git");
|
|
508
|
+
if (!fs.existsSync(dotGit) || !fs.statSync(dotGit).isDirectory()) {
|
|
509
|
+
console.error("当前目录不是 git 仓库(缺少 .git)");
|
|
510
|
+
return EXIT_GIT;
|
|
511
|
+
}
|
|
512
|
+
const user = await client.get("/user");
|
|
513
|
+
const owner = user.login;
|
|
514
|
+
const name = opts.name || path.basename(cwd);
|
|
515
|
+
const full = `${owner}/${name}`;
|
|
516
|
+
|
|
517
|
+
let exists = true;
|
|
518
|
+
try {
|
|
519
|
+
await client.get(`/repos/${full}`);
|
|
520
|
+
} catch (e) {
|
|
521
|
+
if (e instanceof GiteeError && e.message.includes("404")) {
|
|
522
|
+
exists = false;
|
|
523
|
+
} else {
|
|
524
|
+
throw e;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (!exists) {
|
|
528
|
+
console.log(`仓库不存在,正在创建 ${full} …`);
|
|
529
|
+
await client.post("/user/repos", {
|
|
530
|
+
name,
|
|
531
|
+
private: opts.public ? "false" : "true",
|
|
532
|
+
description: opts.description || "",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd).out.trim() || "master";
|
|
537
|
+
const remoteUrl = `https://gitee.com/${full}.git`;
|
|
538
|
+
const authUrl = `https://oauth2:${client.token}@gitee.com/${full}.git`;
|
|
539
|
+
|
|
540
|
+
console.log(`推送 ${branch} → ${remoteUrl} …`);
|
|
541
|
+
const result = runGit(["push", authUrl, `${branch}:${branch}`], cwd);
|
|
542
|
+
if (result.code !== 0) {
|
|
543
|
+
// 令牌不落终端:git 报错输出可能带完整 auth_url,先把 token 打码
|
|
544
|
+
console.error(`推送失败:${result.err.trim().replaceAll(client.token, "***").slice(0, 500)}`);
|
|
545
|
+
return EXIT_GIT;
|
|
546
|
+
}
|
|
547
|
+
if (runGit(["remote", "get-url", "origin"], cwd).code !== 0) {
|
|
548
|
+
runGit(["remote", "add", "origin", remoteUrl], cwd);
|
|
549
|
+
console.log("已添加 remote origin");
|
|
550
|
+
}
|
|
551
|
+
if (opts.public) {
|
|
552
|
+
await client.patch(`/repos/${full}`, { name, private: "false" });
|
|
553
|
+
console.log("已设为公开");
|
|
554
|
+
}
|
|
555
|
+
console.log(`完成:https://gitee.com/${full}`);
|
|
556
|
+
return EXIT_OK;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// --------------------------------------------------------------------------- //
|
|
560
|
+
// 子命令表与入口
|
|
561
|
+
// --------------------------------------------------------------------------- //
|
|
562
|
+
function clientOpts() {
|
|
563
|
+
return [
|
|
564
|
+
{ long: "--token", dest: "token", arg: "value", help: "私人令牌(优先级最高,临时用别的账号时用;注意会进 shell 历史)" },
|
|
565
|
+
{ long: "--api", dest: "api", arg: "value", help: "API 根地址(优先级:命令行 > gitee set > GITEE_API > 官方地址)" },
|
|
566
|
+
{ long: "--proxy", dest: "proxy", arg: "value", help: "代理地址(优先级:命令行 > gitee set > GITEE_PROXY)" },
|
|
567
|
+
{ long: "--timeout", dest: "timeout", arg: "int", help: "超时秒数(优先级:命令行 > gitee set > 30)" },
|
|
568
|
+
];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function spec(usage, description, options, positionals) {
|
|
572
|
+
return { prog: "gitee", usage, description, options, positionals: positionals || [] };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const STATE_OPTS = [
|
|
576
|
+
{ short: "-s", long: "--state", dest: "state", arg: "value", default: "open",
|
|
577
|
+
choices: ["open", "closed", "all"], help: "状态(默认 open)" },
|
|
578
|
+
{ short: "-n", long: "--limit", dest: "limit", arg: "int", default: 20, help: "条数(默认 20)" },
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
// ---- Issue 动作族共用的选项与位置参数 ---- //
|
|
582
|
+
const ISSUE_STATE_OPT = [
|
|
583
|
+
{ short: "-s", long: "--state", dest: "state", arg: "value",
|
|
584
|
+
choices: ["open", "progressing", "closed"],
|
|
585
|
+
help: "状态:open 开启 / progressing 进行中 / closed 关闭" },
|
|
586
|
+
];
|
|
587
|
+
const ISSUE_BODY_OPTS = [
|
|
588
|
+
{ short: "-b", long: "--body", dest: "body", arg: "value", help: "正文(与 -f 二选一)" },
|
|
589
|
+
{ short: "-f", long: "--file", dest: "file", arg: "value", help: "从 UTF-8 文件读正文" },
|
|
590
|
+
];
|
|
591
|
+
const ISSUE_META_OPTS = [
|
|
592
|
+
{ short: "-l", long: "--labels", dest: "labels", arg: "value",
|
|
593
|
+
help: "标签,逗号分隔(如 bug,docs)" },
|
|
594
|
+
{ short: "-a", long: "--assignee", dest: "assignee", arg: "value", help: "负责人 username" },
|
|
595
|
+
];
|
|
596
|
+
const ISSUE_POS = [
|
|
597
|
+
{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" },
|
|
598
|
+
{ dest: "number", required: false, help: "Issue 编号(字符串标识,如 IKVADU,可带 #)" },
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
const ISSUE_FAMILY_HELP = `用法:gitee issue <动作> [参数]
|
|
602
|
+
|
|
603
|
+
动作:
|
|
604
|
+
view 查看 Issue 详情 gitee issue view [<repo>] <编号>
|
|
605
|
+
create 创建 Issue gitee issue create [<repo>] -t 标题 [-b|-f 正文] [-l 标签] [-a 负责人]
|
|
606
|
+
edit 修改 Issue gitee issue edit [<repo>] <编号> [-t 标题] [-b|-f 正文] [-s 状态] [-l 标签] [-a 负责人]
|
|
607
|
+
open 重开 Issue gitee issue open [<repo>] <编号>
|
|
608
|
+
close 关闭 Issue gitee issue close [<repo>] <编号>
|
|
609
|
+
comment 加评论 gitee issue comment [<repo>] <编号> -m 内容|-f 文件
|
|
610
|
+
|
|
611
|
+
用 gitee issue <动作> -h 查看各动作详情。`;
|
|
612
|
+
|
|
613
|
+
const ISSUE_ACTIONS = {
|
|
614
|
+
view: {
|
|
615
|
+
run: (client, o) => cmdIssue(client, o),
|
|
616
|
+
spec: spec("用法:gitee issue view [<repo>] <编号>", "查看 Issue 详情",
|
|
617
|
+
clientOpts(), ISSUE_POS),
|
|
618
|
+
},
|
|
619
|
+
create: {
|
|
620
|
+
run: (client, o) => cmdIssueCreate(client, o),
|
|
621
|
+
spec: spec("用法:gitee issue create [<repo>] -t 标题 [-b 正文|-f 文件] [-l 标签] [-a 负责人]",
|
|
622
|
+
"创建 Issue(Gitee 无删除接口,建前想清楚)",
|
|
623
|
+
[{ short: "-t", long: "--title", dest: "title", arg: "value", required: true,
|
|
624
|
+
help: "标题(必填)" },
|
|
625
|
+
...ISSUE_BODY_OPTS, ...ISSUE_META_OPTS, ...clientOpts()],
|
|
626
|
+
[{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" }]),
|
|
627
|
+
},
|
|
628
|
+
edit: {
|
|
629
|
+
run: (client, o) => cmdIssueEdit(client, o),
|
|
630
|
+
spec: spec("用法:gitee issue edit [<repo>] <编号> [-t 标题] [-b|-f 正文] [-s 状态] [-l 标签] [-a 负责人]",
|
|
631
|
+
"修改 Issue(不带任何修改参数时等于 view)",
|
|
632
|
+
[{ short: "-t", long: "--title", dest: "title", arg: "value", help: "新标题" },
|
|
633
|
+
...ISSUE_BODY_OPTS, ...ISSUE_STATE_OPT, ...ISSUE_META_OPTS, ...clientOpts()],
|
|
634
|
+
ISSUE_POS),
|
|
635
|
+
},
|
|
636
|
+
open: {
|
|
637
|
+
run: (client, o) => cmdSetIssueState(client, o, "open", "issue open"),
|
|
638
|
+
spec: spec("用法:gitee issue open [<repo>] <编号>", "重开 Issue",
|
|
639
|
+
clientOpts(), ISSUE_POS),
|
|
640
|
+
},
|
|
641
|
+
close: {
|
|
642
|
+
run: (client, o) => cmdSetIssueState(client, o, "closed", "issue close"),
|
|
643
|
+
spec: spec("用法:gitee issue close [<repo>] <编号>", "关闭 Issue",
|
|
644
|
+
clientOpts(), ISSUE_POS),
|
|
645
|
+
},
|
|
646
|
+
comment: {
|
|
647
|
+
run: (client, o) => cmdIssueComment(client, o),
|
|
648
|
+
spec: spec("用法:gitee issue comment [<repo>] <编号> -m 内容|-f 文件", "给 Issue 加评论",
|
|
649
|
+
[{ short: "-m", long: "--message", dest: "message", arg: "value", help: "评论内容" },
|
|
650
|
+
{ short: "-f", long: "--file", dest: "file", arg: "value",
|
|
651
|
+
help: "从 UTF-8 文件读评论内容(与 -m 二选一)" },
|
|
652
|
+
...clientOpts()],
|
|
653
|
+
ISSUE_POS),
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const SUBCOMMANDS = {
|
|
658
|
+
set: {
|
|
659
|
+
run: (opts) => cmdSetConfig(opts),
|
|
660
|
+
spec: spec("用法:gitee set [<key> <value>]",
|
|
661
|
+
"查看或写入内部配置(~/.config/gitee/config.json)",
|
|
662
|
+
[], [
|
|
663
|
+
{ dest: "key", required: false, help: "配置项:token / api / proxy / timeout" },
|
|
664
|
+
{ dest: "value", required: false, help: "要写入的值(不带任何参数时列出当前配置)" },
|
|
665
|
+
]),
|
|
666
|
+
noAuth: true,
|
|
667
|
+
},
|
|
668
|
+
unset: {
|
|
669
|
+
run: (opts) => cmdUnsetConfig(opts),
|
|
670
|
+
spec: spec("用法:gitee unset <key>", "删除一项内部配置",
|
|
671
|
+
[], [{ dest: "key", required: true, help: "配置项:token / api / proxy / timeout" }]),
|
|
672
|
+
noAuth: true,
|
|
673
|
+
},
|
|
674
|
+
whoami: {
|
|
675
|
+
run: (client) => cmdWhoami(client),
|
|
676
|
+
spec: spec("用法:gitee whoami", "查看当前 token 对应的账号", clientOpts()),
|
|
677
|
+
},
|
|
678
|
+
list: {
|
|
679
|
+
run: (client, o) => cmdList(client, o),
|
|
680
|
+
spec: spec("用法:gitee list [-n 条数] [--json]", "列出我的仓库",
|
|
681
|
+
[...STATE_OPTS.slice(1), { long: "--json", dest: "json", help: "输出原始 JSON" },
|
|
682
|
+
...clientOpts()]),
|
|
683
|
+
},
|
|
684
|
+
info: {
|
|
685
|
+
run: (client, o) => cmdInfo(client, o),
|
|
686
|
+
spec: spec("用法:gitee info <repo>", "查看仓库信息", clientOpts(),
|
|
687
|
+
[{ dest: "repo", required: true, help: "仓库名或 owner/repo" }]),
|
|
688
|
+
},
|
|
689
|
+
create: {
|
|
690
|
+
run: (client, o) => cmdCreate(client, o),
|
|
691
|
+
spec: spec("用法:gitee create <name> [-d 简介] [--public]", "新建仓库",
|
|
692
|
+
[{ short: "-d", long: "--description", dest: "description", arg: "value", help: "仓库简介" },
|
|
693
|
+
{ long: "--public", dest: "public", help: "设为公开(空仓库需推送后生效)" },
|
|
694
|
+
...clientOpts()],
|
|
695
|
+
[{ dest: "name", required: true, help: "仓库名" }]),
|
|
696
|
+
},
|
|
697
|
+
public: {
|
|
698
|
+
run: (client, o) => cmdSetVisibility(client, o, false),
|
|
699
|
+
spec: spec("用法:gitee public <repo>", "把仓库设为公开", clientOpts(),
|
|
700
|
+
[{ dest: "repo", required: true, help: "仓库名或 owner/repo" }]),
|
|
701
|
+
},
|
|
702
|
+
private: {
|
|
703
|
+
run: (client, o) => cmdSetVisibility(client, o, true),
|
|
704
|
+
spec: spec("用法:gitee private <repo>", "把仓库设为私有", clientOpts(),
|
|
705
|
+
[{ dest: "repo", required: true, help: "仓库名或 owner/repo" }]),
|
|
706
|
+
},
|
|
707
|
+
delete: {
|
|
708
|
+
run: (client, o) => cmdDelete(client, o),
|
|
709
|
+
spec: spec("用法:gitee delete <repo> [-y]", "删除仓库",
|
|
710
|
+
[{ short: "-y", long: "--yes", dest: "yes", help: "跳过确认" }, ...clientOpts()],
|
|
711
|
+
[{ dest: "repo", required: true, help: "仓库名或 owner/repo" }]),
|
|
712
|
+
},
|
|
713
|
+
issues: {
|
|
714
|
+
run: (client, o) => cmdIssues(client, o),
|
|
715
|
+
spec: spec("用法:gitee issues [<repo>] [-s 状态] [-n 条数]", "列出 Issue",
|
|
716
|
+
[...STATE_OPTS, ...clientOpts()],
|
|
717
|
+
[{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" }]),
|
|
718
|
+
},
|
|
719
|
+
issue: {
|
|
720
|
+
run: null, // 动作族:main 里按 argv[1] 分发到 ISSUE_ACTIONS
|
|
721
|
+
actions: ISSUE_ACTIONS,
|
|
722
|
+
spec: null,
|
|
723
|
+
},
|
|
724
|
+
close: {
|
|
725
|
+
run: (client, o) => cmdSetIssueState(client, o, "closed", "close"),
|
|
726
|
+
spec: spec("用法:gitee close [<repo>] <编号>", "关闭 Issue", clientOpts(),
|
|
727
|
+
[{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" },
|
|
728
|
+
{ dest: "number", required: false, help: "Issue 编号(可带 #)" }]),
|
|
729
|
+
},
|
|
730
|
+
reopen: {
|
|
731
|
+
run: (client, o) => cmdSetIssueState(client, o, "open", "reopen"),
|
|
732
|
+
spec: spec("用法:gitee reopen [<repo>] <编号>", "重开 Issue", clientOpts(),
|
|
733
|
+
[{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" },
|
|
734
|
+
{ dest: "number", required: false, help: "Issue 编号(可带 #)" }]),
|
|
735
|
+
},
|
|
736
|
+
prs: {
|
|
737
|
+
run: (client, o) => cmdPrs(client, o),
|
|
738
|
+
spec: spec("用法:gitee prs [<repo>] [-s 状态] [-n 条数]", "列出 Pull Request",
|
|
739
|
+
[...STATE_OPTS, ...clientOpts()],
|
|
740
|
+
[{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" }]),
|
|
741
|
+
},
|
|
742
|
+
push: {
|
|
743
|
+
run: (client, o) => cmdPush(client, o),
|
|
744
|
+
spec: spec("用法:gitee push [--name 名字] [-d 简介] [--public]", "当前目录建仓并推送",
|
|
745
|
+
[{ long: "--name", dest: "name", arg: "value", help: "仓库名(默认取目录名)" },
|
|
746
|
+
{ short: "-d", long: "--description", dest: "description", arg: "value", help: "仓库简介" },
|
|
747
|
+
{ long: "--public", dest: "public", help: "推送后设为公开" },
|
|
748
|
+
...clientOpts()]),
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const TOP_HELP = `gitee — Gitee 仓库管理命令行工具
|
|
753
|
+
|
|
754
|
+
用法:gitee <子命令> [参数]
|
|
755
|
+
|
|
756
|
+
子命令:
|
|
757
|
+
set / unset 内部配置管理(token / api / proxy / timeout)
|
|
758
|
+
whoami 查看当前 token 对应的账号
|
|
759
|
+
list 列出我的仓库
|
|
760
|
+
info <repo> 查看仓库信息
|
|
761
|
+
create 新建仓库
|
|
762
|
+
public/private 切换仓库可见性
|
|
763
|
+
delete 删除仓库
|
|
764
|
+
issues / prs 列出 Issue / Pull Request
|
|
765
|
+
issue Issue 操作族:view / create / edit / open / close / comment
|
|
766
|
+
(无动作词时默认 view;顶层 close/reopen 等价 issue close/open)
|
|
767
|
+
close/reopen 关闭 / 重开 Issue
|
|
768
|
+
push 当前目录建仓并推送
|
|
769
|
+
|
|
770
|
+
用 \`gitee <子命令> -h\` 查看各子命令详情。`;
|
|
771
|
+
|
|
772
|
+
async function main() {
|
|
773
|
+
const argv = process.argv.slice(2);
|
|
774
|
+
const cmd = argv[0];
|
|
775
|
+
if (!cmd) {
|
|
776
|
+
console.error(`错误:缺少子命令\n\n${TOP_HELP}`);
|
|
777
|
+
process.exit(EXIT_USAGE);
|
|
778
|
+
}
|
|
779
|
+
if (cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
780
|
+
console.log(TOP_HELP);
|
|
781
|
+
process.exit(EXIT_OK);
|
|
782
|
+
}
|
|
783
|
+
const def = SUBCOMMANDS[cmd];
|
|
784
|
+
if (!def) {
|
|
785
|
+
console.error(`错误:未知子命令 ${JSON.stringify(cmd)}\n\n${TOP_HELP}`);
|
|
786
|
+
process.exit(EXIT_USAGE);
|
|
787
|
+
}
|
|
788
|
+
if (cmd === "-h") process.exit(EXIT_OK);
|
|
789
|
+
|
|
790
|
+
// issue 动作族:argv[1] 必须是动作词,切换到对应 spec(剩余 argv 从 2 起解析)
|
|
791
|
+
let spec = def.spec;
|
|
792
|
+
let action = null;
|
|
793
|
+
if (def.actions) {
|
|
794
|
+
if (argv[1] === undefined) {
|
|
795
|
+
console.error(`错误:缺少 issue 动作\n\n${ISSUE_FAMILY_HELP}`);
|
|
796
|
+
process.exit(EXIT_USAGE);
|
|
797
|
+
}
|
|
798
|
+
action = def.actions[argv[1]] || null;
|
|
799
|
+
if (!action) {
|
|
800
|
+
console.error(`错误:未知 issue 动作 ${JSON.stringify(argv[1])}\n\n${ISSUE_FAMILY_HELP}`);
|
|
801
|
+
process.exit(EXIT_USAGE);
|
|
802
|
+
}
|
|
803
|
+
spec = action.spec;
|
|
804
|
+
}
|
|
805
|
+
const { opts } = parse(spec, action ? argv.slice(2) : argv.slice(1));
|
|
806
|
+
if (def.noAuth) {
|
|
807
|
+
process.exit(await def.run(opts));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const cfg = loadConfig(CONFIG_PATH, CONFIG_KEYS);
|
|
811
|
+
const [tokenRaw] = resolveValue(opts.token, cfg.token, "GITEE_TOKEN", "");
|
|
812
|
+
const token = String(tokenRaw || "").trim();
|
|
813
|
+
if (!token) {
|
|
814
|
+
console.error("错误:未设置 Gitee 私人令牌,请先执行:gitee set token 你的令牌"
|
|
815
|
+
+ "(或 export GITEE_TOKEN=你的令牌)");
|
|
816
|
+
process.exit(EXIT_AUTH);
|
|
817
|
+
}
|
|
818
|
+
const [apiRaw] = resolveValue(opts.api, cfg.api, "GITEE_API", DEFAULT_API);
|
|
819
|
+
const api = apiRaw.replace(/\/+$/, "");
|
|
820
|
+
const [proxy] = resolveValue(opts.proxy, cfg.proxy, "GITEE_PROXY", null);
|
|
821
|
+
const [timeout] = resolveValue(opts.timeout, cfg.timeout, null, DEFAULT_TIMEOUT);
|
|
822
|
+
const client = new Client(token, api, proxy, timeout);
|
|
823
|
+
try {
|
|
824
|
+
process.exit(await (action ? action.run(client, opts) : def.run(client, opts)));
|
|
825
|
+
} catch (e) {
|
|
826
|
+
if (e instanceof GiteeError) {
|
|
827
|
+
console.error(`错误:${e.message}`);
|
|
828
|
+
process.exit(EXIT_API);
|
|
829
|
+
}
|
|
830
|
+
console.error(`错误:${e.message}`);
|
|
831
|
+
process.exit(EXIT_API);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
main().catch((e) => {
|
|
836
|
+
console.error(`错误:${e.message}`);
|
|
837
|
+
process.exit(EXIT_API);
|
|
838
|
+
});
|