ai-project-manage-cli 6.0.51 → 6.0.52

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 (2) hide show
  1. package/dist/index.js +184 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -407,6 +407,10 @@ var requestConfig = {
407
407
  method: "GET",
408
408
  path: "/cli/tasks/branch-baseline"
409
409
  }),
410
+ listSessionsForBranchCleanup: defineEndpoint({
411
+ method: "GET",
412
+ path: "/cli/sessions/branch-cleanup"
413
+ }),
410
414
  workspaceBaseline: defineEndpoint({
411
415
  method: "GET",
412
416
  path: "/cli/workspaces/baseline"
@@ -634,6 +638,7 @@ async function runLogin(opts) {
634
638
  import { execFile as execFile2 } from "child_process";
635
639
  import { promisify as promisify2 } from "util";
636
640
  var execFileAsync2 = promisify2(execFile2);
641
+ var SESSION_BRANCH_PREFIX = "feat/session-";
637
642
  function branchNameForSession(sessionId) {
638
643
  const id = sessionId.trim();
639
644
  if (!id) {
@@ -644,7 +649,15 @@ function branchNameForSession(sessionId) {
644
649
  "[apm] \u4F1A\u8BDD ID \u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u6216\u8DEF\u5F84\u5206\u9694\u7B26\uFF0C\u8BF7\u4F7F\u7528\u5B57\u6BCD\u3001\u6570\u5B57\u3001._- \u7B49"
645
650
  );
646
651
  }
647
- return `feat/session-${id}`;
652
+ return `${SESSION_BRANCH_PREFIX}${id}`;
653
+ }
654
+ function sessionIdFromBranchName(branch) {
655
+ const name = branch.trim().replace(/^origin\//, "");
656
+ if (!name.startsWith(SESSION_BRANCH_PREFIX)) {
657
+ return null;
658
+ }
659
+ const sessionId = name.slice(SESSION_BRANCH_PREFIX.length).trim();
660
+ return sessionId || null;
648
661
  }
649
662
  async function execGit(cwd, args, quiet) {
650
663
  try {
@@ -797,6 +810,171 @@ async function runBranch(sessionId, options = {}) {
797
810
  return ensureFeatureBranch(branch, baselineBranch, options);
798
811
  }
799
812
 
813
+ // src/commands/clean-branches.ts
814
+ import { execFile as execFile3 } from "child_process";
815
+ import { promisify as promisify3 } from "util";
816
+ var execFileAsync3 = promisify3(execFile3);
817
+ async function execGit2(cwd, args, quiet) {
818
+ try {
819
+ const { stdout, stderr } = await execFileAsync3("git", args, {
820
+ cwd,
821
+ encoding: "utf8",
822
+ maxBuffer: 10 * 1024 * 1024
823
+ });
824
+ if (!quiet && stderr.trim()) {
825
+ process.stderr.write(stderr);
826
+ }
827
+ return stdout;
828
+ } catch (err) {
829
+ const e = err;
830
+ const detail = (e.stderr ?? e.message ?? String(err)).trim();
831
+ throw new Error(
832
+ `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
833
+ );
834
+ }
835
+ }
836
+ async function ensureGitRepo2(cwd) {
837
+ await execGit2(cwd, ["rev-parse", "--git-dir"], true);
838
+ }
839
+ async function getCurrentBranch2(cwd) {
840
+ return (await execGit2(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
841
+ }
842
+ async function resolveDefaultBranch(cwd) {
843
+ try {
844
+ const ref = (await execGit2(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], true)).trim();
845
+ const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
846
+ if (match?.[1]) {
847
+ return match[1];
848
+ }
849
+ } catch {
850
+ }
851
+ const out = await execGit2(cwd, ["remote", "show", "origin"], true);
852
+ const headLine = out.split(/\r?\n/).find((line) => line.includes("HEAD branch"));
853
+ const branch = headLine?.split(":").pop()?.trim();
854
+ if (branch) {
855
+ return branch;
856
+ }
857
+ throw new Error("[apm] \u65E0\u6CD5\u89E3\u6790 origin \u9ED8\u8BA4\u5206\u652F\uFF0C\u8BF7\u5148\u6267\u884C git fetch origin");
858
+ }
859
+ async function listLocalSessionBranches(cwd) {
860
+ const out = await execGit2(
861
+ cwd,
862
+ [
863
+ "for-each-ref",
864
+ "--format=%(refname:short)",
865
+ "refs/heads/",
866
+ SESSION_BRANCH_PREFIX + "*"
867
+ ],
868
+ true
869
+ );
870
+ return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
871
+ }
872
+ async function listRemoteSessionBranches(cwd) {
873
+ const out = await execGit2(
874
+ cwd,
875
+ [
876
+ "for-each-ref",
877
+ "--format=%(refname:short)",
878
+ "refs/remotes/origin/",
879
+ SESSION_BRANCH_PREFIX + "*"
880
+ ],
881
+ true
882
+ );
883
+ return out.split(/\r?\n/).map((line) => line.trim().replace(/^origin\//, "")).filter(Boolean);
884
+ }
885
+ async function localBranchExists2(cwd, branch) {
886
+ try {
887
+ await execGit2(
888
+ cwd,
889
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
890
+ true
891
+ );
892
+ return true;
893
+ } catch {
894
+ return false;
895
+ }
896
+ }
897
+ async function remoteBranchExists(cwd, branch) {
898
+ const out = await execGit2(
899
+ cwd,
900
+ ["ls-remote", "--heads", "origin", branch],
901
+ true
902
+ );
903
+ return out.trim().length > 0;
904
+ }
905
+ function reasonForCleanup(sessionId, sessionStatusById) {
906
+ if (!sessionStatusById.has(sessionId)) {
907
+ return "\u6C9F\u901A\u7FA4\u4E0D\u5728\u4EFB\u52A1\u5217\u8868\u4E2D";
908
+ }
909
+ if (sessionStatusById.get(sessionId) === "COMPLETED") {
910
+ return "\u5173\u8054\u4EFB\u52A1\u5DF2\u5B8C\u6210";
911
+ }
912
+ return "\u4FDD\u7559";
913
+ }
914
+ async function runCleanBranches(options = {}) {
915
+ const cwd = options.cwd ?? process.cwd();
916
+ const dryRun = options.dryRun ?? false;
917
+ await ensureGitRepo2(cwd);
918
+ await execGit2(cwd, ["fetch", "--prune", "origin"], true);
919
+ const cfg = await ensureLoggedConfig();
920
+ const api = createApmApiClient(cfg);
921
+ const { sessions } = await api.cli.listSessionsForBranchCleanup({});
922
+ const sessionStatusById = new Map(
923
+ sessions.map((item) => [item.sessionId, item.taskStatus])
924
+ );
925
+ const branchNames = /* @__PURE__ */ new Set([
926
+ ...await listLocalSessionBranches(cwd),
927
+ ...await listRemoteSessionBranches(cwd)
928
+ ]);
929
+ if (branchNames.size === 0) {
930
+ console.log("[apm] \u672A\u53D1\u73B0 feat/session-* \u5206\u652F");
931
+ return;
932
+ }
933
+ const toDelete = [...branchNames].map((branch) => {
934
+ const sessionId = sessionIdFromBranchName(branch);
935
+ if (!sessionId) {
936
+ return null;
937
+ }
938
+ const reason = reasonForCleanup(sessionId, sessionStatusById);
939
+ if (reason === "\u4FDD\u7559") {
940
+ return null;
941
+ }
942
+ return { branch, sessionId, reason };
943
+ }).filter((item) => item != null).sort((a, b) => a.branch.localeCompare(b.branch));
944
+ if (toDelete.length === 0) {
945
+ console.log("[apm] \u6CA1\u6709\u9700\u8981\u6E05\u7406\u7684 feat/session-* \u5206\u652F");
946
+ return;
947
+ }
948
+ let currentBranch = await getCurrentBranch2(cwd);
949
+ let defaultBranch = null;
950
+ for (const item of toDelete) {
951
+ const { branch, sessionId, reason } = item;
952
+ const label = `${branch} (${sessionId}: ${reason})`;
953
+ if (dryRun) {
954
+ console.log(`[apm] [dry-run] \u5C06\u5220\u9664 ${label}`);
955
+ continue;
956
+ }
957
+ if (currentBranch === branch) {
958
+ defaultBranch ??= await resolveDefaultBranch(cwd);
959
+ await execGit2(cwd, ["checkout", defaultBranch], true);
960
+ currentBranch = defaultBranch;
961
+ }
962
+ if (await localBranchExists2(cwd, branch)) {
963
+ await execGit2(cwd, ["branch", "-D", branch], true);
964
+ console.log(`[apm] \u5DF2\u5220\u9664\u672C\u5730\u5206\u652F ${branch}`);
965
+ }
966
+ if (await remoteBranchExists(cwd, branch)) {
967
+ await execGit2(cwd, ["push", "origin", "--delete", branch], true);
968
+ console.log(`[apm] \u5DF2\u5220\u9664\u8FDC\u7A0B\u5206\u652F origin/${branch}`);
969
+ }
970
+ }
971
+ if (dryRun) {
972
+ console.log(`[apm] [dry-run] \u5171 ${toDelete.length} \u4E2A\u5206\u652F\u5F85\u6E05\u7406`);
973
+ } else {
974
+ console.log(`[apm] \u5DF2\u6E05\u7406 ${toDelete.length} \u4E2A feat/session-* \u5206\u652F`);
975
+ }
976
+ }
977
+
800
978
  // src/commands/pull.ts
801
979
  import { writeFileSync as writeFileSync7 } from "fs";
802
980
  import { join as join8 } from "path";
@@ -3683,6 +3861,11 @@ function buildProgram() {
3683
3861
  ).action(async (sessionId, opts) => {
3684
3862
  await runBranch(sessionId, { message: opts.message });
3685
3863
  });
3864
+ program.command("clean-branches").description(
3865
+ "\u6E05\u7406\u672C\u5730\u4E0E\u8FDC\u7A0B feat/session-* \u5206\u652F\uFF1A\u6C9F\u901A\u7FA4\u4E0D\u5728\u4EFB\u52A1\u5217\u8868\u4E2D\uFF0C\u6216\u5173\u8054\u4EFB\u52A1\u5DF2\u5B8C\u6210\u65F6\u5220\u9664"
3866
+ ).option("--dry-run", "\u4EC5\u5217\u51FA\u5C06\u88AB\u5220\u9664\u7684\u5206\u652F\uFF0C\u4E0D\u5B9E\u9645\u6267\u884C").action(async (opts) => {
3867
+ await runCleanBranches({ dryRun: opts.dryRun });
3868
+ });
3686
3869
  program.command("create-pr").description(
3687
3870
  "\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u7684\u4F1A\u8BDD\u7279\u6027\u5206\u652F\u521B\u5EFA PR\uFF08\u6807\u9898\u81EA\u52A8\u52A0 [AI] \u6807\u8BC6\uFF1B\u8FDC\u7A0B\u521B\u5EFA\u5931\u8D25\u65F6\u5E73\u53F0\u6570\u636E\u56DE\u6EDA\uFF09"
3688
3871
  ).requiredOption("--session <sessionId>", "\u6C9F\u901A\u7FA4 ID").requiredOption("--title <title>", "PR \u6807\u9898").option("--content <content>", "PR \u6B63\u6587\uFF08Markdown\uFF09", "").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.51",
3
+ "version": "6.0.52",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,