@cc123hh/gitee-cli 0.3.0 → 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.
Files changed (3) hide show
  1. package/README.md +21 -3
  2. package/bin/gitee.js +296 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -163,12 +163,24 @@ gitee push --public -d "我的小工具"
163
163
  ### Issue / Pull Request
164
164
 
165
165
  ```bash
166
- gitee issues emb-cli # 打开中的 Issue
166
+ gitee issues emb-cli # 列出 Issue
167
167
  gitee issues emb-cli -s all -n 50
168
+ gitee issue view emb-cli IKVADU # 查看详情
169
+ gitee issue create emb-cli -t "发现bug" -b "复现步骤" -l bug -a 用户名 # 创建
170
+ gitee issue edit emb-cli IKVADU -t "新标题" -b "新正文" -l bug,docs # 修改
171
+ gitee issue edit emb-cli IKVADU -f 正文.md # 正文从 UTF-8 文件读(与 -b 互斥)
172
+ gitee issue close emb-cli IKVADU # 关闭(open 为重开)
173
+ gitee issue comment emb-cli IKVADU -m "已修复" # 加评论
174
+ gitee close emb-cli IKVADU # 顶层快捷:关闭 / 重开
175
+ gitee reopen emb-cli IKVADU
168
176
  gitee prs emb-cli
169
177
  ```
170
178
 
171
- `repo` 参数写 `仓库名` 会自动补上你的用户名,也可以写完整的 `owner/repo`。
179
+ `repo` 参数写 `仓库名` 会自动补上你的用户名,也可以写完整的 `owner/repo`;
180
+ Issue 相关命令省略 `repo` 时取当前目录同名仓库,只写编号也能定位
181
+ (如 `gitee issue view IKVADU`)。Issue 编号是 Gitee 生成的字符串标识
182
+ (如 `IKVADU`),带不带 `#` 前缀都可以(shell 里 `#` 要加引号)。
183
+ Gitee 无删除 Issue 的接口(已实测 405),`issue delete` 不提供。
172
184
 
173
185
  ## 📋 参数
174
186
 
@@ -176,7 +188,13 @@ gitee prs emb-cli
176
188
  |---|---|---|
177
189
  | `-n/--limit` | list / issues / prs | 返回条数 |
178
190
  | `--json` | list | 输出原始 JSON |
179
- | `-s/--state` | issues / prs | `open` / `closed` / `all` |
191
+ | `-s/--state` | issues / prs / issue | 列表筛选 `open`/`closed`/`all`;`issue edit` 修改状态 `open`/`progressing`/`closed` |
192
+ | `-t/--title` | issue create / edit | 标题(create 必填) |
193
+ | `-b/--body` | issue create / edit | 正文(与 `-f` 互斥) |
194
+ | `-f/--file` | issue create / edit / comment | 从 UTF-8 文件读正文/评论 |
195
+ | `-l/--labels` | issue create / edit | 标签,逗号分隔 |
196
+ | `-a/--assignee` | issue create / edit | 负责人 username |
197
+ | `-m/--message` | issue comment | 评论内容 |
180
198
  | `-y/--yes` | delete | 跳过确认 |
181
199
  | `-d/--description` | create / push | 仓库简介 |
182
200
  | `--public` | create / push | 目标可见性 |
package/bin/gitee.js CHANGED
@@ -314,6 +314,189 @@ async function cmdPrs(client, opts) {
314
314
  return EXIT_OK;
315
315
  }
316
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
+
317
500
  function runGit(args, cwd) {
318
501
  const r = spawnSync("git", args, { cwd, encoding: "utf8" });
319
502
  return { code: r.status, out: r.stdout || "", err: r.stderr || "" };
@@ -395,6 +578,82 @@ const STATE_OPTS = [
395
578
  { short: "-n", long: "--limit", dest: "limit", arg: "int", default: 20, help: "条数(默认 20)" },
396
579
  ];
397
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
+
398
657
  const SUBCOMMANDS = {
399
658
  set: {
400
659
  run: (opts) => cmdSetConfig(opts),
@@ -457,6 +716,23 @@ const SUBCOMMANDS = {
457
716
  [...STATE_OPTS, ...clientOpts()],
458
717
  [{ dest: "repo", required: false, help: "仓库名或 owner/repo(默认当前目录同名仓库)" }]),
459
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
+ },
460
736
  prs: {
461
737
  run: (client, o) => cmdPrs(client, o),
462
738
  spec: spec("用法:gitee prs [<repo>] [-s 状态] [-n 条数]", "列出 Pull Request",
@@ -486,6 +762,9 @@ const TOP_HELP = `gitee — Gitee 仓库管理命令行工具
486
762
  public/private 切换仓库可见性
487
763
  delete 删除仓库
488
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
489
768
  push 当前目录建仓并推送
490
769
 
491
770
  用 \`gitee <子命令> -h\` 查看各子命令详情。`;
@@ -508,7 +787,22 @@ async function main() {
508
787
  }
509
788
  if (cmd === "-h") process.exit(EXIT_OK);
510
789
 
511
- const { opts } = parse(def.spec, argv.slice(1));
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));
512
806
  if (def.noAuth) {
513
807
  process.exit(await def.run(opts));
514
808
  }
@@ -527,7 +821,7 @@ async function main() {
527
821
  const [timeout] = resolveValue(opts.timeout, cfg.timeout, null, DEFAULT_TIMEOUT);
528
822
  const client = new Client(token, api, proxy, timeout);
529
823
  try {
530
- process.exit(await def.run(client, opts));
824
+ process.exit(await (action ? action.run(client, opts) : def.run(client, opts)));
531
825
  } catch (e) {
532
826
  if (e instanceof GiteeError) {
533
827
  console.error(`错误:${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cc123hh/gitee-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Gitee 仓库管理命令行工具——纯 Node 零依赖实现",
5
5
  "bin": {
6
6
  "gitee": "bin/gitee.js"