ai-project-manage-cli 6.0.104 → 6.0.106

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 +496 -357
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -535,26 +535,194 @@ async function tryReadGitOriginUrl(cwd) {
535
535
  }
536
536
  }
537
537
 
538
+ // src/git-utils.ts
539
+ import { execFile as execFile2 } from "child_process";
540
+ import { promisify as promisify2 } from "util";
541
+ var execFileAsync2 = promisify2(execFile2);
542
+ async function execGit(cwd, args, quiet = false) {
543
+ try {
544
+ const { stdout, stderr } = await execFileAsync2("git", args, {
545
+ cwd,
546
+ encoding: "utf8",
547
+ maxBuffer: 10 * 1024 * 1024
548
+ });
549
+ if (!quiet && stderr.trim()) {
550
+ process.stderr.write(stderr);
551
+ }
552
+ return stdout;
553
+ } catch (err) {
554
+ const e = err;
555
+ const detail = (e.stderr ?? e.message ?? String(err)).trim();
556
+ throw new Error(
557
+ `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
558
+ );
559
+ }
560
+ }
561
+ async function isGitRepo(cwd) {
562
+ try {
563
+ await execGit(cwd, ["rev-parse", "--git-dir"], true);
564
+ return true;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+ async function ensureGitRepo(cwd) {
570
+ await execGit(cwd, ["rev-parse", "--git-dir"], true);
571
+ }
572
+ async function getCurrentBranch(cwd) {
573
+ return (await execGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
574
+ }
575
+ async function isWorkingTreeDirty(cwd) {
576
+ const out = await execGit(cwd, ["status", "--porcelain"], true);
577
+ return out.trim().length > 0;
578
+ }
579
+ async function remoteBranchExists(cwd, branch) {
580
+ const out = await execGit(
581
+ cwd,
582
+ ["ls-remote", "--heads", "origin", branch],
583
+ true
584
+ );
585
+ return out.trim().length > 0;
586
+ }
587
+ async function resolveGitRepoRoot(cwd) {
588
+ return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
589
+ }
590
+ async function ensureRemoteBaselineBranch(cwd, baselineBranch) {
591
+ await execGit(cwd, ["fetch", "origin", baselineBranch], true);
592
+ if (await remoteBranchExists(cwd, baselineBranch)) {
593
+ return;
594
+ }
595
+ throw new Error(
596
+ `[apm] \u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F origin/${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
597
+ );
598
+ }
599
+ async function hasUpstream(cwd) {
600
+ try {
601
+ await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
602
+ return true;
603
+ } catch {
604
+ return false;
605
+ }
606
+ }
607
+ var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
608
+ async function commitAndPushGitignore(workdir) {
609
+ if (!await isGitRepo(workdir)) {
610
+ console.log("[apm] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u8BF7\u624B\u52A8\u63D0\u4EA4 .gitignore");
611
+ return;
612
+ }
613
+ await execGit(workdir, ["add", "--", ".gitignore"]);
614
+ await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
615
+ console.log(`[apm] \u5DF2\u63D0\u4EA4 .gitignore: ${GITIGNORE_COMMIT_MESSAGE}`);
616
+ const originUrl = await tryReadGitOriginUrl(workdir);
617
+ if (!originUrl) {
618
+ console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
619
+ return;
620
+ }
621
+ if (await hasUpstream(workdir)) {
622
+ await execGit(workdir, ["push"]);
623
+ } else {
624
+ await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
625
+ }
626
+ console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
627
+ }
628
+
629
+ // src/baseline-resolve.ts
630
+ function formatBaselineDiagnostic(workdirPath, baselineWorkdirPath, diagnostic) {
631
+ return diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baselineWorkdirPath}\uFF09
632
+ \u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
633
+ }
634
+ async function matchRepositoryByGitRemote(api, workdirPath) {
635
+ const gitRoot = await resolveGitRepoRoot(workdirPath);
636
+ const gitUrl = await tryReadGitOriginUrl(gitRoot);
637
+ if (!gitUrl) {
638
+ return null;
639
+ }
640
+ const matched = await api.cli.matchRepository({ url: gitUrl });
641
+ const repositoryId = matched.repositoryId?.trim();
642
+ const defaultBranch = matched.defaultBranch?.trim();
643
+ if (!repositoryId || !defaultBranch) {
644
+ return null;
645
+ }
646
+ console.log(
647
+ `[apm] \u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u672A\u5339\u914D\uFF0C\u5DF2\u901A\u8FC7 git remote \u5173\u8054\u4ED3\u5E93: ${gitUrl}`
648
+ );
649
+ return { repositoryId, defaultBranch };
650
+ }
651
+ async function resolveWorkspaceBaseline(api, workdirPath) {
652
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
653
+ const repositoryId = baseline.repositoryId?.trim();
654
+ const defaultBranch = baseline.defaultBranch?.trim();
655
+ if (repositoryId && defaultBranch) {
656
+ return {
657
+ repositoryId,
658
+ defaultBranch,
659
+ workdirPath: baseline.workdirPath,
660
+ matchedViaGitRemote: false
661
+ };
662
+ }
663
+ const viaGit = await matchRepositoryByGitRemote(api, workdirPath);
664
+ if (viaGit) {
665
+ return {
666
+ ...viaGit,
667
+ workdirPath: baseline.workdirPath,
668
+ matchedViaGitRemote: true
669
+ };
670
+ }
671
+ throw new Error(
672
+ `[apm] ${formatBaselineDiagnostic(
673
+ workdirPath,
674
+ baseline.workdirPath,
675
+ baseline.diagnostic
676
+ )}`
677
+ );
678
+ }
679
+ async function resolveBranchBaseline(api, sessionId, workdirPath) {
680
+ const baseline = await api.cli.branchBaseline({ sessionId, workdirPath });
681
+ const repositoryId = baseline.repositoryId?.trim();
682
+ const defaultBranch = baseline.defaultBranch?.trim();
683
+ if (repositoryId && defaultBranch) {
684
+ return {
685
+ sessionId: baseline.sessionId,
686
+ taskId: baseline.taskId,
687
+ repositoryId,
688
+ defaultBranch,
689
+ workdirPath: baseline.workdirPath,
690
+ matchedViaGitRemote: false
691
+ };
692
+ }
693
+ const viaGit = await matchRepositoryByGitRemote(api, workdirPath);
694
+ if (viaGit) {
695
+ return {
696
+ sessionId: baseline.sessionId,
697
+ taskId: baseline.taskId,
698
+ ...viaGit,
699
+ workdirPath: baseline.workdirPath,
700
+ matchedViaGitRemote: true
701
+ };
702
+ }
703
+ throw new Error(
704
+ `[apm] ${formatBaselineDiagnostic(
705
+ workdirPath,
706
+ baseline.workdirPath,
707
+ baseline.diagnostic
708
+ )}`
709
+ );
710
+ }
711
+
538
712
  // src/deployment-config-sync.ts
539
713
  var TEMPLATE_HINT = "\u4FDD\u7559\u6A21\u677F .apm/apm.config.json";
540
714
  var SYNC_HINT = "\u767B\u8BB0\u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u3001\u7ED1\u5B9A\u4ED3\u5E93\u540E\uFF0C\u53EF\u6267\u884C: apm sync-deploy-config";
541
715
  async function resolveRepositoryIdForSync(api, workdirPath) {
542
- const baseline = await api.cli.workspaceBaseline({ workdirPath });
543
- if (baseline.repositoryId) {
716
+ try {
717
+ const baseline = await resolveWorkspaceBaseline(api, workdirPath);
544
718
  return { repositoryId: baseline.repositoryId, diagnostic: null };
719
+ } catch (err) {
720
+ const detail = err instanceof Error ? err.message : String(err);
721
+ return {
722
+ repositoryId: null,
723
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
724
+ };
545
725
  }
546
- const gitUrl = await tryReadGitOriginUrl(workdirPath);
547
- if (gitUrl) {
548
- const matched = await api.cli.matchRepository({ url: gitUrl });
549
- if (matched.repositoryId) {
550
- console.log(
551
- `[apm] \u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u672A\u5339\u914D\uFF0C\u5DF2\u901A\u8FC7 git remote \u5173\u8054\u4ED3\u5E93: ${gitUrl}`
552
- );
553
- return { repositoryId: matched.repositoryId, diagnostic: null };
554
- }
555
- }
556
- const detail = baseline.diagnostic?.message ?? `\u5F53\u524D\u8DEF\u5F84\uFF08\u89C4\u8303\u5316\uFF1A${baseline.workdirPath}\uFF09\u672A\u5339\u914D\u5230\u5DF2\u7ED1\u5B9A\u4ED3\u5E93\u7684\u5DE5\u4F5C\u7A7A\u95F4\u3002`;
557
- return { repositoryId: null, diagnostic: detail };
558
726
  }
559
727
  async function syncRemoteDeploymentConfig(workdirPath, apmDir) {
560
728
  const cfg = await tryReadApmConfig();
@@ -816,85 +984,6 @@ async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
816
984
  return synced;
817
985
  }
818
986
 
819
- // src/git-utils.ts
820
- import { execFile as execFile2 } from "child_process";
821
- import { promisify as promisify2 } from "util";
822
- var execFileAsync2 = promisify2(execFile2);
823
- async function execGit(cwd, args, quiet = false) {
824
- try {
825
- const { stdout, stderr } = await execFileAsync2("git", args, {
826
- cwd,
827
- encoding: "utf8",
828
- maxBuffer: 10 * 1024 * 1024
829
- });
830
- if (!quiet && stderr.trim()) {
831
- process.stderr.write(stderr);
832
- }
833
- return stdout;
834
- } catch (err) {
835
- const e = err;
836
- const detail = (e.stderr ?? e.message ?? String(err)).trim();
837
- throw new Error(
838
- `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
839
- );
840
- }
841
- }
842
- async function isGitRepo(cwd) {
843
- try {
844
- await execGit(cwd, ["rev-parse", "--git-dir"], true);
845
- return true;
846
- } catch {
847
- return false;
848
- }
849
- }
850
- async function ensureGitRepo(cwd) {
851
- await execGit(cwd, ["rev-parse", "--git-dir"], true);
852
- }
853
- async function getCurrentBranch(cwd) {
854
- return (await execGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
855
- }
856
- async function isWorkingTreeDirty(cwd) {
857
- const out = await execGit(cwd, ["status", "--porcelain"], true);
858
- return out.trim().length > 0;
859
- }
860
- async function remoteBranchExists(cwd, branch) {
861
- const out = await execGit(
862
- cwd,
863
- ["ls-remote", "--heads", "origin", branch],
864
- true
865
- );
866
- return out.trim().length > 0;
867
- }
868
- async function hasUpstream(cwd) {
869
- try {
870
- await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
871
- return true;
872
- } catch {
873
- return false;
874
- }
875
- }
876
- var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
877
- async function commitAndPushGitignore(workdir) {
878
- if (!await isGitRepo(workdir)) {
879
- console.log("[apm] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u8BF7\u624B\u52A8\u63D0\u4EA4 .gitignore");
880
- return;
881
- }
882
- await execGit(workdir, ["add", "--", ".gitignore"]);
883
- await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
884
- console.log(`[apm] \u5DF2\u63D0\u4EA4 .gitignore: ${GITIGNORE_COMMIT_MESSAGE}`);
885
- const originUrl = await tryReadGitOriginUrl(workdir);
886
- if (!originUrl) {
887
- console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
888
- return;
889
- }
890
- if (await hasUpstream(workdir)) {
891
- await execGit(workdir, ["push"]);
892
- } else {
893
- await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
894
- }
895
- console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
896
- }
897
-
898
987
  // src/commands/init.ts
899
988
  async function ensureWorkspaceInitialized(workdir, options) {
900
989
  if (isWorkspaceApmInitialized(workdir)) {
@@ -1010,9 +1099,6 @@ async function runLogin(opts) {
1010
1099
  }
1011
1100
 
1012
1101
  // src/commands/branch.ts
1013
- import { execFile as execFile3 } from "child_process";
1014
- import { promisify as promisify3 } from "util";
1015
- var execFileAsync3 = promisify3(execFile3);
1016
1102
  var SESSION_BRANCH_PREFIX = "feat/session-";
1017
1103
  function branchNameForSession(sessionId) {
1018
1104
  const id = sessionId.trim();
@@ -1034,47 +1120,9 @@ function sessionIdFromBranchName(branch) {
1034
1120
  const sessionId = name.slice(SESSION_BRANCH_PREFIX.length).trim();
1035
1121
  return sessionId || null;
1036
1122
  }
1037
- async function execGit2(cwd, args, quiet) {
1038
- try {
1039
- const { stdout, stderr } = await execFileAsync3("git", args, {
1040
- cwd,
1041
- encoding: "utf8",
1042
- maxBuffer: 10 * 1024 * 1024
1043
- });
1044
- if (!quiet && stderr.trim()) {
1045
- process.stderr.write(stderr);
1046
- }
1047
- return stdout;
1048
- } catch (err) {
1049
- const e = err;
1050
- const detail = (e.stderr ?? e.message ?? String(err)).trim();
1051
- throw new Error(
1052
- `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
1053
- );
1054
- }
1055
- }
1056
- async function ensureGitRepo2(cwd) {
1057
- await execGit2(cwd, ["rev-parse", "--git-dir"], true);
1058
- }
1059
- async function getCurrentBranch2(cwd) {
1060
- const name = (await execGit2(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
1061
- return name;
1062
- }
1063
- async function isWorkingTreeDirty2(cwd) {
1064
- const out = await execGit2(cwd, ["status", "--porcelain"], true);
1065
- return out.trim().length > 0;
1066
- }
1067
- async function remoteHeadBranchExists(cwd, branch) {
1068
- const out = await execGit2(
1069
- cwd,
1070
- ["ls-remote", "--heads", "origin", branch],
1071
- true
1072
- );
1073
- return out.trim().length > 0;
1074
- }
1075
1123
  async function localBranchExists(cwd, branch) {
1076
1124
  try {
1077
- await execGit2(
1125
+ await execGit(
1078
1126
  cwd,
1079
1127
  ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
1080
1128
  true
@@ -1085,32 +1133,29 @@ async function localBranchExists(cwd, branch) {
1085
1133
  }
1086
1134
  }
1087
1135
  async function commitWorkingTreeIfDirty(cwd, message) {
1088
- await ensureGitRepo2(cwd);
1089
- if (!await isWorkingTreeDirty2(cwd)) {
1136
+ await ensureGitRepo(cwd);
1137
+ if (!await isWorkingTreeDirty(cwd)) {
1090
1138
  return false;
1091
1139
  }
1092
- const commitMessage = message?.trim() || `chore(apm): \u540C\u6B65\u5DE5\u4F5C\u533A (${await getCurrentBranch2(cwd)})`;
1093
- await execGit2(cwd, ["add", "-A"]);
1094
- await execGit2(cwd, ["commit", "-m", commitMessage]);
1140
+ const commitMessage = message?.trim() || `chore(apm): \u540C\u6B65\u5DE5\u4F5C\u533A (${await getCurrentBranch(cwd)})`;
1141
+ await execGit(cwd, ["add", "-A"]);
1142
+ await execGit(cwd, ["commit", "-m", commitMessage]);
1095
1143
  console.log(`[apm] \u5DF2\u63D0\u4EA4\u5DE5\u4F5C\u533A\u53D8\u66F4: ${commitMessage}`);
1096
1144
  return true;
1097
1145
  }
1098
1146
  async function ensureFeatureBranch(branch, baselineBranch, options) {
1099
1147
  const cwd = options.cwd ?? process.cwd();
1148
+ const gitRoot = await resolveGitRepoRoot(cwd);
1100
1149
  const commitMessage = options.message?.trim() || `chore(apm): \u540C\u6B65\u5DE5\u4F5C\u533A (${branch})`;
1101
- await ensureGitRepo2(cwd);
1102
- if (!await remoteHeadBranchExists(cwd, baselineBranch)) {
1103
- throw new Error(
1104
- `[apm] \u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F ${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
1105
- );
1106
- }
1107
- const current = await getCurrentBranch2(cwd);
1108
- const dirty = await isWorkingTreeDirty2(cwd);
1150
+ await ensureGitRepo(gitRoot);
1151
+ await ensureRemoteBaselineBranch(gitRoot, baselineBranch);
1152
+ const current = await getCurrentBranch(gitRoot);
1153
+ const dirty = await isWorkingTreeDirty(gitRoot);
1109
1154
  if (dirty) {
1110
1155
  if (current === branch) {
1111
- await commitWorkingTreeIfDirty(cwd, commitMessage);
1156
+ await commitWorkingTreeIfDirty(gitRoot, commitMessage);
1112
1157
  } else {
1113
- await execGit2(cwd, [
1158
+ await execGit(gitRoot, [
1114
1159
  "stash",
1115
1160
  "push",
1116
1161
  "-u",
@@ -1119,34 +1164,34 @@ async function ensureFeatureBranch(branch, baselineBranch, options) {
1119
1164
  ]);
1120
1165
  }
1121
1166
  }
1122
- const onTargetBranch = await getCurrentBranch2(cwd) === branch;
1167
+ const onTargetBranch = await getCurrentBranch(gitRoot) === branch;
1123
1168
  if (onTargetBranch) {
1124
- await execGit2(cwd, ["fetch", "origin", baselineBranch]);
1125
- await execGit2(cwd, ["merge", `origin/${baselineBranch}`, "--no-edit"]);
1169
+ await execGit(gitRoot, ["fetch", "origin", baselineBranch]);
1170
+ await execGit(gitRoot, ["merge", `origin/${baselineBranch}`, "--no-edit"]);
1126
1171
  } else {
1127
- const remoteExists = await remoteHeadBranchExists(cwd, branch);
1172
+ const remoteExists = await remoteBranchExists(gitRoot, branch);
1128
1173
  if (remoteExists) {
1129
- await execGit2(cwd, ["fetch", "origin", branch]);
1130
- await execGit2(cwd, ["checkout", "-B", branch, `origin/${branch}`]);
1131
- } else if (await localBranchExists(cwd, branch)) {
1132
- if (await getCurrentBranch2(cwd) !== branch) {
1133
- await execGit2(cwd, ["checkout", branch]);
1174
+ await execGit(gitRoot, ["fetch", "origin", branch]);
1175
+ await execGit(gitRoot, ["checkout", "-B", branch, `origin/${branch}`]);
1176
+ } else if (await localBranchExists(gitRoot, branch)) {
1177
+ if (await getCurrentBranch(gitRoot) !== branch) {
1178
+ await execGit(gitRoot, ["checkout", branch]);
1134
1179
  }
1135
1180
  console.log(`[apm] \u5206\u652F ${branch} \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u521B\u5EFA`);
1136
1181
  } else {
1137
- await execGit2(cwd, ["fetch", "origin", baselineBranch]);
1182
+ await execGit(gitRoot, ["fetch", "origin", baselineBranch]);
1138
1183
  try {
1139
- await execGit2(cwd, [
1184
+ await execGit(gitRoot, [
1140
1185
  "checkout",
1141
1186
  "-b",
1142
1187
  branch,
1143
1188
  `origin/${baselineBranch}`
1144
1189
  ]);
1145
- await execGit2(cwd, ["push", "-u", "origin", branch]);
1190
+ await execGit(gitRoot, ["push", "-u", "origin", branch]);
1146
1191
  } catch (err) {
1147
- if (await localBranchExists(cwd, branch)) {
1148
- if (await getCurrentBranch2(cwd) !== branch) {
1149
- await execGit2(cwd, ["checkout", branch]);
1192
+ if (await localBranchExists(gitRoot, branch)) {
1193
+ if (await getCurrentBranch(gitRoot) !== branch) {
1194
+ await execGit(gitRoot, ["checkout", branch]);
1150
1195
  }
1151
1196
  console.log(`[apm] \u5206\u652F ${branch} \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u521B\u5EFA`);
1152
1197
  } else {
@@ -1168,115 +1213,164 @@ async function runBranch(sessionId, options = {}) {
1168
1213
  const api = createApmApiClient(cfg);
1169
1214
  const cwd = options.cwd ?? process.cwd();
1170
1215
  const workdirPath = resolveWorkdirPath(cwd);
1171
- const baseline = await api.cli.branchBaseline({
1172
- sessionId: trimmedSessionId,
1216
+ const baseline = await resolveBranchBaseline(
1217
+ api,
1218
+ trimmedSessionId,
1173
1219
  workdirPath
1174
- });
1175
- if (!baseline.repositoryId) {
1176
- const detail = baseline.diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baseline.workdirPath}\uFF09
1177
- \u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
1178
- throw new Error(`[apm] ${detail}`);
1179
- }
1180
- const baselineBranch = (baseline.defaultBranch ?? "").trim();
1181
- if (!baselineBranch) {
1182
- throw new Error("[apm] \u5E73\u53F0\u8FD4\u56DE\u7684\u57FA\u7EBF\u5206\u652F\u540D\u4E3A\u7A7A");
1183
- }
1220
+ );
1184
1221
  const branch = branchNameForSession(trimmedSessionId);
1185
- return ensureFeatureBranch(branch, baselineBranch, options);
1222
+ return ensureFeatureBranch(branch, baseline.defaultBranch, options);
1186
1223
  }
1187
1224
 
1188
- // src/commands/clean-branches.ts
1189
- import { execFile as execFile4 } from "child_process";
1190
- import { promisify as promisify4 } from "util";
1191
- var execFileAsync4 = promisify4(execFile4);
1192
- async function execGit3(cwd, args, quiet) {
1193
- try {
1194
- const { stdout, stderr } = await execFileAsync4("git", args, {
1195
- cwd,
1196
- encoding: "utf8",
1197
- maxBuffer: 10 * 1024 * 1024
1198
- });
1199
- if (!quiet && stderr.trim()) {
1200
- process.stderr.write(stderr);
1201
- }
1202
- return stdout;
1203
- } catch (err) {
1204
- const e = err;
1205
- const detail = (e.stderr ?? e.message ?? String(err)).trim();
1206
- throw new Error(
1207
- `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
1208
- );
1225
+ // src/commands/session-branch-prune.ts
1226
+ var TASK_STATUS_LABEL = {
1227
+ OPEN: "\u5F85\u5F00\u59CB",
1228
+ IN_PROGRESS: "\u8FDB\u884C\u4E2D",
1229
+ COMPLETED: "\u5DF2\u5B8C\u6210",
1230
+ FAILED: "\u5DF2\u5931\u8D25",
1231
+ SUSPENDED: "\u5DF2\u6682\u505C"
1232
+ };
1233
+ function taskStatusLabel(status) {
1234
+ if (status === null) {
1235
+ return "\u5E73\u53F0\u65E0\u4EFB\u52A1";
1209
1236
  }
1237
+ return TASK_STATUS_LABEL[status] ?? status;
1210
1238
  }
1211
- async function ensureGitRepo3(cwd) {
1212
- await execGit3(cwd, ["rev-parse", "--git-dir"], true);
1239
+ function syncTagForBranch(hasLocal, hasRemote) {
1240
+ if (hasLocal && hasRemote) {
1241
+ return "\u53CC\u7AEF";
1242
+ }
1243
+ if (hasLocal) {
1244
+ return "\u4EC5\u672C\u5730";
1245
+ }
1246
+ return "\u4EC5\u8FDC\u7A0B";
1213
1247
  }
1214
- async function getCurrentBranch3(cwd) {
1215
- return (await execGit3(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
1248
+ function shouldDeleteMode1(input) {
1249
+ if (input.merged) {
1250
+ return { delete: true, reason: "\u5DF2\u5408\u5E76" };
1251
+ }
1252
+ if (input.taskStatus === null) {
1253
+ return { delete: true, reason: "\u5E73\u53F0\u65E0\u4EFB\u52A1" };
1254
+ }
1255
+ return { delete: false, reason: "" };
1216
1256
  }
1217
- async function resolveBaselineBranch(cwd, api) {
1218
- const workdirPath = resolveWorkdirPath(cwd);
1219
- const baseline = await api.cli.workspaceBaseline({ workdirPath });
1220
- if (!baseline.repositoryId) {
1221
- const detail = baseline.diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baseline.workdirPath}\uFF09
1222
- \u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
1223
- throw new Error(`[apm] ${detail}`);
1257
+ function shouldDeleteMode2(input) {
1258
+ const mode1 = shouldDeleteMode1(input);
1259
+ if (mode1.delete) {
1260
+ return mode1;
1224
1261
  }
1225
- const baselineBranch = (baseline.defaultBranch ?? "").trim();
1226
- if (!baselineBranch) {
1227
- throw new Error("[apm] \u5E73\u53F0\u8FD4\u56DE\u7684\u57FA\u7EBF\u5206\u652F\u540D\u4E3A\u7A7A");
1262
+ if (input.taskStatus === "COMPLETED") {
1263
+ return { delete: true, reason: "\u4EFB\u52A1\u5DF2\u5B8C\u6210" };
1228
1264
  }
1229
- await execGit3(cwd, ["fetch", "origin", baselineBranch], true);
1230
- if (!await remoteBranchExists2(cwd, baselineBranch)) {
1231
- throw new Error(
1232
- `[apm] \u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F origin/${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
1233
- );
1265
+ if (input.taskStatus === "FAILED") {
1266
+ return { delete: true, reason: "\u4EFB\u52A1\u5DF2\u5931\u8D25" };
1234
1267
  }
1235
- return baselineBranch;
1268
+ return { delete: false, reason: "" };
1236
1269
  }
1237
- async function isBranchBasedOnBaseline(cwd, branch, baselineBranch) {
1238
- const ref = await localBranchExists2(cwd, branch) ? branch : `origin/${branch}`;
1239
- try {
1240
- await execGit3(
1241
- cwd,
1242
- ["merge-base", "--is-ancestor", `origin/${baselineBranch}`, ref],
1243
- true
1244
- );
1245
- return true;
1246
- } catch {
1247
- return false;
1248
- }
1270
+ function buildSessionBranchRow(input) {
1271
+ const mode1 = shouldDeleteMode1(input);
1272
+ const mode2 = shouldDeleteMode2(input);
1273
+ return {
1274
+ branch: input.branch,
1275
+ sessionId: input.sessionId,
1276
+ hasLocal: input.hasLocal,
1277
+ hasRemote: input.hasRemote,
1278
+ syncTag: syncTagForBranch(input.hasLocal, input.hasRemote),
1279
+ merged: input.merged,
1280
+ taskStatus: input.taskStatus,
1281
+ taskLabel: taskStatusLabel(input.taskStatus),
1282
+ deleteMode1: mode1.delete,
1283
+ deleteMode2: mode2.delete,
1284
+ deleteReasonMode1: mode1.reason,
1285
+ deleteReasonMode2: mode2.reason
1286
+ };
1249
1287
  }
1250
- async function listCandidateSessionBranches(cwd, includeRemote) {
1251
- const names = new Set(await listLocalSessionBranches(cwd));
1252
- if (includeRemote) {
1253
- for (const branch of await listRemoteSessionBranches(cwd)) {
1254
- names.add(branch);
1255
- }
1288
+ function rowsForDeleteMode(rows, mode) {
1289
+ return rows.filter((row) => mode === 1 ? row.deleteMode1 : row.deleteMode2);
1290
+ }
1291
+ function yn(value) {
1292
+ return value ? "\u662F" : "\u2014";
1293
+ }
1294
+ function pad(value, width) {
1295
+ const chars = [...value];
1296
+ const len = chars.length;
1297
+ if (len >= width) {
1298
+ return value;
1256
1299
  }
1257
- return [...names].sort((a, b) => a.localeCompare(b));
1300
+ return value + " ".repeat(width - len);
1301
+ }
1302
+ function formatSessionBranchTable(meta, rows) {
1303
+ const lines = [
1304
+ `[apm] \u5DE5\u4F5C\u76EE\u5F55: ${meta.workdirPath}`,
1305
+ `[apm] Git \u6839\u76EE\u5F55: ${meta.gitRoot}`,
1306
+ `[apm] \u4ED3\u5E93 ID: ${meta.repositoryId}`,
1307
+ `[apm] \u57FA\u7EBF\u5206\u652F: ${meta.baselineBranch}`,
1308
+ ""
1309
+ ];
1310
+ if (rows.length === 0) {
1311
+ lines.push(
1312
+ `[apm] \u672A\u53D1\u73B0\u4ECE ${meta.baselineBranch} \u884D\u751F\u7684 feat/session-* \u5206\u652F\uFF08\u672C\u5730\u4E0E\u8FDC\u7A0B\uFF09`
1313
+ );
1314
+ return lines.join("\n");
1315
+ }
1316
+ const headers = [
1317
+ "\u5206\u652F",
1318
+ "\u672C\u5730",
1319
+ "\u8FDC\u7A0B",
1320
+ "\u540C\u6B65",
1321
+ "\u5DF2\u5408\u5E76",
1322
+ "\u4EFB\u52A1\u72B6\u6001",
1323
+ "\u6A21\u5F0F1",
1324
+ "\u6A21\u5F0F2"
1325
+ ];
1326
+ const widths = [36, 4, 4, 6, 6, 10, 6, 6];
1327
+ lines.push(headers.map((h, i) => pad(h, widths[i])).join(" "));
1328
+ lines.push(widths.map((w) => "-".repeat(w)).join(" "));
1329
+ for (const row of rows) {
1330
+ const cells = [
1331
+ row.branch,
1332
+ yn(row.hasLocal),
1333
+ yn(row.hasRemote),
1334
+ row.syncTag,
1335
+ yn(row.merged),
1336
+ row.taskLabel,
1337
+ row.deleteMode1 ? row.deleteReasonMode1 || "\u662F" : "\u2014",
1338
+ row.deleteMode2 ? row.deleteReasonMode2 || "\u662F" : "\u2014"
1339
+ ];
1340
+ lines.push(cells.map((c, i) => pad(c, widths[i])).join(" "));
1341
+ }
1342
+ const mode1Count = rows.filter((r) => r.deleteMode1).length;
1343
+ const mode2Count = rows.filter((r) => r.deleteMode2).length;
1344
+ lines.push("");
1345
+ lines.push(
1346
+ `[apm] \u5171 ${rows.length} \u4E2A\u5206\u652F\uFF1B\u6A21\u5F0F 1 \u53EF\u6E05\u7406 ${mode1Count} \u4E2A\uFF0C\u6A21\u5F0F 2 \u53EF\u6E05\u7406 ${mode2Count} \u4E2A`
1347
+ );
1348
+ lines.push("[apm] \u6A21\u5F0F 1\uFF1A\u5DF2\u5408\u5E76\u8FDB\u57FA\u7EBF\uFF0C\u6216\u5E73\u53F0\u5DF2\u65E0\u8BE5\u6C9F\u901A\u7FA4\u4EFB\u52A1");
1349
+ lines.push("[apm] \u6A21\u5F0F 2\uFF1A\u5728\u6A21\u5F0F 1 \u57FA\u7840\u4E0A\uFF0C\u53E6\u542B\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\u7684\u5206\u652F");
1350
+ lines.push("[apm] \u6267\u884C\u6E05\u7406: apm branch prune --mode 1|2 [--dry-run]");
1351
+ return lines.join("\n");
1258
1352
  }
1259
1353
  async function listLocalSessionBranches(cwd) {
1260
- const out = await execGit3(
1354
+ const out = await execGit(
1261
1355
  cwd,
1262
1356
  [
1263
1357
  "for-each-ref",
1264
1358
  "--format=%(refname:short)",
1265
1359
  "refs/heads/",
1266
- SESSION_BRANCH_PREFIX + "*"
1360
+ `${SESSION_BRANCH_PREFIX}*`
1267
1361
  ],
1268
1362
  true
1269
1363
  );
1270
1364
  return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1271
1365
  }
1272
1366
  async function listRemoteSessionBranches(cwd) {
1273
- const out = await execGit3(
1367
+ const out = await execGit(
1274
1368
  cwd,
1275
1369
  [
1276
1370
  "for-each-ref",
1277
1371
  "--format=%(refname:short)",
1278
1372
  "refs/remotes/origin/",
1279
- SESSION_BRANCH_PREFIX + "*"
1373
+ `${SESSION_BRANCH_PREFIX}*`
1280
1374
  ],
1281
1375
  true
1282
1376
  );
@@ -1284,7 +1378,7 @@ async function listRemoteSessionBranches(cwd) {
1284
1378
  }
1285
1379
  async function localBranchExists2(cwd, branch) {
1286
1380
  try {
1287
- await execGit3(
1381
+ await execGit(
1288
1382
  cwd,
1289
1383
  ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
1290
1384
  true
@@ -1294,29 +1388,25 @@ async function localBranchExists2(cwd, branch) {
1294
1388
  return false;
1295
1389
  }
1296
1390
  }
1297
- async function remoteBranchExists2(cwd, branch) {
1298
- const out = await execGit3(
1299
- cwd,
1300
- ["ls-remote", "--heads", "origin", branch],
1301
- true
1302
- );
1303
- return out.trim().length > 0;
1304
- }
1305
- function reasonForCleanup(sessionId, sessionStatusById) {
1306
- if (!sessionStatusById.has(sessionId)) {
1307
- return "\u6C9F\u901A\u7FA4\u4E0D\u5728\u4EFB\u52A1\u5217\u8868\u4E2D";
1308
- }
1309
- if (sessionStatusById.get(sessionId) === "COMPLETED") {
1310
- return "\u5173\u8054\u4EFB\u52A1\u5DF2\u5B8C\u6210";
1391
+ async function isBranchBasedOnBaseline(cwd, branch, baselineBranch) {
1392
+ const ref = await localBranchExists2(cwd, branch) ? branch : `origin/${branch}`;
1393
+ try {
1394
+ await execGit(
1395
+ cwd,
1396
+ ["merge-base", "--is-ancestor", `origin/${baselineBranch}`, ref],
1397
+ true
1398
+ );
1399
+ return true;
1400
+ } catch {
1401
+ return false;
1311
1402
  }
1312
- return "\u4FDD\u7559";
1313
1403
  }
1314
- async function isBranchMergedIntoDefault(cwd, branch, defaultBranch) {
1404
+ async function isBranchMergedIntoBaseline(cwd, branch, baselineBranch) {
1315
1405
  const ref = await localBranchExists2(cwd, branch) ? branch : `origin/${branch}`;
1316
1406
  try {
1317
- await execGit3(
1407
+ await execGit(
1318
1408
  cwd,
1319
- ["merge-base", "--is-ancestor", ref, `origin/${defaultBranch}`],
1409
+ ["merge-base", "--is-ancestor", ref, `origin/${baselineBranch}`],
1320
1410
  true
1321
1411
  );
1322
1412
  return true;
@@ -1324,79 +1414,129 @@ async function isBranchMergedIntoDefault(cwd, branch, defaultBranch) {
1324
1414
  return false;
1325
1415
  }
1326
1416
  }
1417
+ async function resolveSessionBranchScanMeta(cwd, api) {
1418
+ const workdirPath = resolveWorkdirPath(cwd);
1419
+ const baseline = await resolveWorkspaceBaseline(api, workdirPath);
1420
+ const gitRoot = await resolveGitRepoRoot(cwd);
1421
+ await ensureGitRepo(gitRoot);
1422
+ await execGit(gitRoot, ["fetch", "--prune", "origin"], true);
1423
+ await ensureRemoteBaselineBranch(gitRoot, baseline.defaultBranch);
1424
+ return {
1425
+ workdirPath,
1426
+ gitRoot,
1427
+ baselineBranch: baseline.defaultBranch,
1428
+ repositoryId: baseline.repositoryId
1429
+ };
1430
+ }
1431
+ async function scanSessionBranches(meta, sessionStatusById) {
1432
+ const { gitRoot, baselineBranch } = meta;
1433
+ const names = /* @__PURE__ */ new Set();
1434
+ for (const branch of await listLocalSessionBranches(gitRoot)) {
1435
+ names.add(branch);
1436
+ }
1437
+ for (const branch of await listRemoteSessionBranches(gitRoot)) {
1438
+ names.add(branch);
1439
+ }
1440
+ const rows = [];
1441
+ for (const branch of [...names].sort((a, b) => a.localeCompare(b))) {
1442
+ if (!await isBranchBasedOnBaseline(gitRoot, branch, baselineBranch)) {
1443
+ continue;
1444
+ }
1445
+ const sessionId = sessionIdFromBranchName(branch);
1446
+ if (!sessionId) {
1447
+ continue;
1448
+ }
1449
+ const hasLocal = await localBranchExists2(gitRoot, branch);
1450
+ const hasRemote = await remoteBranchExists(gitRoot, branch);
1451
+ const merged = await isBranchMergedIntoBaseline(
1452
+ gitRoot,
1453
+ branch,
1454
+ baselineBranch
1455
+ );
1456
+ const rawStatus = sessionStatusById.get(sessionId);
1457
+ const taskStatus = rawStatus ? rawStatus : null;
1458
+ rows.push(
1459
+ buildSessionBranchRow({
1460
+ branch,
1461
+ sessionId,
1462
+ hasLocal,
1463
+ hasRemote,
1464
+ merged,
1465
+ taskStatus
1466
+ })
1467
+ );
1468
+ }
1469
+ return rows;
1470
+ }
1471
+ async function deleteSessionBranch(gitRoot, baselineBranch, branch, currentBranchRef) {
1472
+ if (currentBranchRef.value === branch) {
1473
+ await execGit(gitRoot, ["checkout", baselineBranch], true);
1474
+ currentBranchRef.value = baselineBranch;
1475
+ }
1476
+ if (await localBranchExists2(gitRoot, branch)) {
1477
+ await execGit(gitRoot, ["branch", "-D", branch], true);
1478
+ console.log(`[apm] \u5DF2\u5220\u9664\u672C\u5730\u5206\u652F ${branch}`);
1479
+ }
1480
+ if (await remoteBranchExists(gitRoot, branch)) {
1481
+ await execGit(gitRoot, ["push", "origin", "--delete", branch], true);
1482
+ console.log(`[apm] \u5DF2\u5220\u9664\u8FDC\u7A0B\u5206\u652F origin/${branch}`);
1483
+ }
1484
+ }
1485
+ async function loadSessionStatusMap(api) {
1486
+ const { sessions } = await api.cli.listSessionsForBranchCleanup({});
1487
+ return new Map(sessions.map((item) => [item.sessionId, item.taskStatus]));
1488
+ }
1489
+ async function getGitCurrentBranch(gitRoot) {
1490
+ return getCurrentBranch(gitRoot);
1491
+ }
1492
+
1493
+ // src/commands/clean-branches.ts
1327
1494
  async function runCleanBranches(options = {}) {
1328
1495
  const cwd = options.cwd ?? process.cwd();
1329
- const dryRun = options.dryRun ?? false;
1330
- const includeRemote = options.includeRemote ?? false;
1331
- await ensureGitRepo3(cwd);
1332
- await execGit3(cwd, ["fetch", "--prune", "origin"], true);
1333
1496
  const cfg = await ensureLoggedConfig();
1334
1497
  const api = createApmApiClient(cfg);
1335
- const baselineBranch = await resolveBaselineBranch(cwd, api);
1336
- const { sessions } = await api.cli.listSessionsForBranchCleanup({});
1337
- const sessionStatusById = new Map(
1338
- sessions.map((item) => [item.sessionId, item.taskStatus])
1339
- );
1340
- const candidates = await listCandidateSessionBranches(cwd, includeRemote);
1341
- const branchNames = [];
1342
- for (const branch of candidates) {
1343
- if (await isBranchBasedOnBaseline(cwd, branch, baselineBranch)) {
1344
- branchNames.push(branch);
1345
- }
1346
- }
1347
- if (branchNames.length === 0) {
1348
- console.log(
1349
- `[apm] \u672A\u53D1\u73B0\u4ECE ${baselineBranch} \u884D\u751F\u7684\u672C\u5730 feat/session-* \u5206\u652F${includeRemote ? "\uFF08\u542B\u8FDC\u7A0B\uFF09" : ""}`
1350
- );
1498
+ const meta = await resolveSessionBranchScanMeta(cwd, api);
1499
+ const sessionStatusById = await loadSessionStatusMap(api);
1500
+ const rows = await scanSessionBranches(meta, sessionStatusById);
1501
+ console.log(formatSessionBranchTable(meta, rows));
1502
+ if (!options.mode) {
1351
1503
  return;
1352
1504
  }
1353
- const toDelete = [...branchNames].map((branch) => {
1354
- const sessionId = sessionIdFromBranchName(branch);
1355
- if (!sessionId) {
1356
- return null;
1357
- }
1358
- const reason = reasonForCleanup(sessionId, sessionStatusById);
1359
- if (reason === "\u4FDD\u7559") {
1360
- return null;
1361
- }
1362
- return { branch, sessionId, reason };
1363
- }).filter((item) => item != null).sort((a, b) => a.branch.localeCompare(b.branch));
1505
+ const toDelete = rowsForDeleteMode(rows, options.mode);
1364
1506
  if (toDelete.length === 0) {
1365
- console.log("[apm] \u6CA1\u6709\u9700\u8981\u6E05\u7406\u7684 feat/session-* \u5206\u652F");
1507
+ console.log(`[apm] \u6A21\u5F0F ${options.mode} \u4E0B\u6CA1\u6709\u9700\u8981\u6E05\u7406\u7684\u5206\u652F`);
1366
1508
  return;
1367
1509
  }
1368
- let currentBranch = await getCurrentBranch3(cwd);
1369
- for (const item of toDelete) {
1370
- const { branch, sessionId, reason } = item;
1371
- const label = `${branch} (${sessionId}: ${reason})`;
1372
- if (dryRun) {
1373
- const merged = await isBranchMergedIntoDefault(
1374
- cwd,
1375
- branch,
1376
- baselineBranch
1510
+ if (options.dryRun) {
1511
+ for (const row of toDelete) {
1512
+ const reason = options.mode === 1 ? row.deleteReasonMode1 : row.deleteReasonMode2;
1513
+ const parts = [
1514
+ row.hasLocal ? "\u672C\u5730" : null,
1515
+ row.hasRemote ? "\u8FDC\u7A0B" : null
1516
+ ].filter(Boolean).join("+");
1517
+ console.log(
1518
+ `[apm] [dry-run] \u6A21\u5F0F ${options.mode} \u5C06\u5220\u9664 ${row.branch}\uFF08${parts || "\u65E0"}\uFF1B${reason}\uFF09`
1377
1519
  );
1378
- const mergeTag = merged ? "\u5DF2\u5408\u5E76" : "\u672A\u5408\u5E76";
1379
- console.log(`[apm] [dry-run] \u5C06\u5220\u9664 ${label} [${mergeTag}]`);
1380
- continue;
1381
- }
1382
- if (currentBranch === branch) {
1383
- await execGit3(cwd, ["checkout", baselineBranch], true);
1384
- currentBranch = baselineBranch;
1385
- }
1386
- if (await localBranchExists2(cwd, branch)) {
1387
- await execGit3(cwd, ["branch", "-D", branch], true);
1388
- console.log(`[apm] \u5DF2\u5220\u9664\u672C\u5730\u5206\u652F ${branch}`);
1389
- }
1390
- if (await remoteBranchExists2(cwd, branch)) {
1391
- await execGit3(cwd, ["push", "origin", "--delete", branch], true);
1392
- console.log(`[apm] \u5DF2\u5220\u9664\u8FDC\u7A0B\u5206\u652F origin/${branch}`);
1393
1520
  }
1521
+ console.log(
1522
+ `[apm] [dry-run] \u6A21\u5F0F ${options.mode} \u5171 ${toDelete.length} \u4E2A\u5206\u652F\u5F85\u6E05\u7406`
1523
+ );
1524
+ return;
1394
1525
  }
1395
- if (dryRun) {
1396
- console.log(`[apm] [dry-run] \u5171 ${toDelete.length} \u4E2A\u5206\u652F\u5F85\u6E05\u7406`);
1397
- } else {
1398
- console.log(`[apm] \u5DF2\u6E05\u7406 ${toDelete.length} \u4E2A feat/session-* \u5206\u652F`);
1526
+ const currentBranchRef = {
1527
+ value: await getGitCurrentBranch(meta.gitRoot)
1528
+ };
1529
+ for (const row of toDelete) {
1530
+ await deleteSessionBranch(
1531
+ meta.gitRoot,
1532
+ meta.baselineBranch,
1533
+ row.branch,
1534
+ currentBranchRef
1535
+ );
1399
1536
  }
1537
+ console.log(
1538
+ `[apm] \u6A21\u5F0F ${options.mode} \u5DF2\u6E05\u7406 ${toDelete.length} \u4E2A feat/session-* \u5206\u652F`
1539
+ );
1400
1540
  }
1401
1541
 
1402
1542
  // src/commands/pull.ts
@@ -1649,7 +1789,7 @@ function isRuleUpToDate(entry, rule, dest) {
1649
1789
  }
1650
1790
  async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1651
1791
  const api = createApmApiClient(cfg);
1652
- const baseline = await api.cli.branchBaseline({ sessionId, workdirPath });
1792
+ const baseline = await resolveBranchBaseline(api, sessionId, workdirPath);
1653
1793
  const repositoryId = baseline.repositoryId;
1654
1794
  const rulesDir = join8(apmRoot ?? workspaceApmDir(workdirPath), "rules");
1655
1795
  await ensureDirExists(rulesDir);
@@ -2035,6 +2175,9 @@ import { basename as basename3 } from "path";
2035
2175
  // src/assumptions/local-validate.ts
2036
2176
  var NO_ASSUMPTIONS_RE = /无[,,]?\s*口径均有依据/;
2037
2177
  var BLOCK_HEADER_RE = /^####\s+(A\d+)\s*$/im;
2178
+ function formatAssumptionQuestionId(source, code) {
2179
+ return `${source}:${code}`;
2180
+ }
2038
2181
  var DEV_PATTERNS = [
2039
2182
  { pattern: /status\s*=/i, message: "\u5305\u542B status= \u7B49\u6280\u672F\u679A\u4E3E" },
2040
2183
  { pattern: /\.(java|vue|ts|tsx|jsx|sql)\b/i, message: "\u5305\u542B\u6587\u4EF6\u8DEF\u5F84" },
@@ -2064,7 +2207,7 @@ function parseField(block, label) {
2064
2207
  const value = match?.[1]?.trim();
2065
2208
  return value || void 0;
2066
2209
  }
2067
- function parseStructuredBlock(code, block) {
2210
+ function parseStructuredBlock(code, block, source) {
2068
2211
  const pmQuestion = parseField(block, "\u95EE") ?? parseField(block, "prompt");
2069
2212
  if (!pmQuestion) return null;
2070
2213
  const optionsSection = block.match(
@@ -2073,7 +2216,7 @@ function parseStructuredBlock(code, block) {
2073
2216
  const options = optionsSection ? parseOptionsBlock(optionsSection[1]) : [];
2074
2217
  return {
2075
2218
  code,
2076
- questionId: code,
2219
+ questionId: formatAssumptionQuestionId(source, code),
2077
2220
  pmQuestion,
2078
2221
  context: parseField(block, "\u573A\u666F"),
2079
2222
  options,
@@ -2104,7 +2247,7 @@ function parseAssumptionsDocument(markdown, _source) {
2104
2247
  for (const block of blocks) {
2105
2248
  const headerMatch = block.match(BLOCK_HEADER_RE);
2106
2249
  if (!headerMatch) continue;
2107
- const parsed = parseStructuredBlock(headerMatch[1], block);
2250
+ const parsed = parseStructuredBlock(headerMatch[1], block, _source);
2108
2251
  if (parsed) results.push(parsed);
2109
2252
  }
2110
2253
  return results;
@@ -3051,8 +3194,8 @@ var MinioClient = class {
3051
3194
  // src/commands/deploy/internal/deploy-artifact-minio.ts
3052
3195
  var APM_DEPLOYMENT_RUN_ID_ENV = "APM_DEPLOYMENT_RUN_ID";
3053
3196
  function formatDeployArtifactTimestamp(date = /* @__PURE__ */ new Date()) {
3054
- const pad = (n) => String(n).padStart(2, "0");
3055
- return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
3197
+ const pad2 = (n) => String(n).padStart(2, "0");
3198
+ return `${date.getFullYear()}${pad2(date.getMonth() + 1)}${pad2(date.getDate())}${pad2(date.getHours())}${pad2(date.getMinutes())}${pad2(date.getSeconds())}`;
3056
3199
  }
3057
3200
  function sanitizeDeployProjectName(name) {
3058
3201
  const trimmed = name.trim().replace(/\\/g, "/");
@@ -5251,19 +5394,10 @@ var LOG_PREFIX2 = "[apm] \u90E8\u7F72\u524D\u57FA\u7EBF\u540C\u6B65:";
5251
5394
  function formatGitError(error) {
5252
5395
  return error instanceof Error ? error.message : String(error);
5253
5396
  }
5254
- async function resolveBaselineBranch2(cwd, api) {
5397
+ async function resolveBaselineBranch(cwd, api) {
5255
5398
  const workdirPath = resolveWorkdirPath(cwd);
5256
- const baseline = await api.cli.workspaceBaseline({ workdirPath });
5257
- if (!baseline.repositoryId) {
5258
- const detail = baseline.diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baseline.workdirPath}\uFF09
5259
- \u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
5260
- throw new Error(detail);
5261
- }
5262
- const baselineBranch = (baseline.defaultBranch ?? "").trim();
5263
- if (!baselineBranch) {
5264
- throw new Error("\u5E73\u53F0\u8FD4\u56DE\u7684\u57FA\u7EBF\u5206\u652F\u540D\u4E3A\u7A7A");
5265
- }
5266
- return baselineBranch;
5399
+ const baseline = await resolveWorkspaceBaseline(api, workdirPath);
5400
+ return baseline.defaultBranch;
5267
5401
  }
5268
5402
  async function isMergeInProgress(cwd) {
5269
5403
  try {
@@ -5343,21 +5477,17 @@ async function syncDeployBaselineBeforeDeploy(cwd) {
5343
5477
  }
5344
5478
  await ensureGitRepo(cwd);
5345
5479
  const api = createApmApiClient(cfg);
5346
- const baselineBranch = await resolveBaselineBranch2(cwd, api);
5480
+ const baselineBranch = await resolveBaselineBranch(cwd, api);
5481
+ const gitRoot = await resolveGitRepoRoot(cwd);
5347
5482
  note(`${LOG_PREFIX2} \u57FA\u7EBF\u5206\u652F ${baselineBranch}`);
5348
5483
  note(`${LOG_PREFIX2} \u6B63\u5728 fetch origin ${baselineBranch}`);
5349
- await execGit(cwd, ["fetch", "origin", baselineBranch], true);
5350
- if (!await remoteBranchExists(cwd, baselineBranch)) {
5351
- throw new Error(
5352
- `\u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F origin/${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
5353
- );
5354
- }
5484
+ await ensureRemoteBaselineBranch(gitRoot, baselineBranch);
5355
5485
  note(`${LOG_PREFIX2} fetch \u5B8C\u6210`);
5356
- const currentBranch = await getCurrentBranch(cwd);
5486
+ const currentBranch = await getCurrentBranch(gitRoot);
5357
5487
  let stashed = false;
5358
- if (await isWorkingTreeDirty(cwd)) {
5488
+ if (await isWorkingTreeDirty(gitRoot)) {
5359
5489
  note(`${LOG_PREFIX2} \u68C0\u6D4B\u5230\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u5148 stash`);
5360
- await execGit(cwd, [
5490
+ await execGit(gitRoot, [
5361
5491
  "stash",
5362
5492
  "push",
5363
5493
  "-u",
@@ -5368,12 +5498,12 @@ async function syncDeployBaselineBeforeDeploy(cwd) {
5368
5498
  note(`${LOG_PREFIX2} \u5DF2 stash \u672A\u63D0\u4EA4\u6539\u52A8`);
5369
5499
  }
5370
5500
  const { merged, conflictRollback } = await mergeBaselineBranch(
5371
- cwd,
5501
+ gitRoot,
5372
5502
  baselineBranch,
5373
5503
  currentBranch,
5374
5504
  note
5375
5505
  );
5376
- const stashConflict = await restoreStashIfNeeded(cwd, stashed, note);
5506
+ const stashConflict = await restoreStashIfNeeded(gitRoot, stashed, note);
5377
5507
  if (conflictRollback || stashConflict) {
5378
5508
  note(`${LOG_PREFIX2} \u57FA\u7EBF\u540C\u6B65\u672A\u5B8C\u6210\uFF08\u5B58\u5728\u51B2\u7A81\u5E76\u5DF2\u56DE\u9000\uFF09\uFF0C\u7EE7\u7EED\u6267\u884C\u90E8\u7F72`);
5379
5509
  } else if (merged) {
@@ -8255,14 +8385,23 @@ function buildProgram() {
8255
8385
  });
8256
8386
  const branch = program.command("branch").description("\u4F1A\u8BDD\u5206\u652F feat/session-<sessionId>");
8257
8387
  branch.command("prune").description(
8258
- "\u6E05\u7406\u672C\u5730\u4E0E\u8FDC\u7A0B feat/session-* \u5206\u652F\uFF08\u7C7B\u4F3C git fetch --prune\uFF09\uFF1A\u6C9F\u901A\u7FA4\u4E0D\u5728\u4EFB\u52A1\u5217\u8868\u4E2D\uFF0C\u6216\u5173\u8054\u4EFB\u52A1\u5DF2\u5B8C\u6210\u65F6\u5220\u9664"
8259
- ).option("--dry-run", "\u4EC5\u5217\u51FA\u5C06\u88AB\u5220\u9664\u7684\u5206\u652F\uFF0C\u4E0D\u5B9E\u9645\u6267\u884C").option(
8260
- "-A, --all",
8261
- "\u5305\u542B\u4EC5\u5B58\u5728\u4E8E\u8FDC\u7A0B\u3001\u672C\u5730\u672A checkout \u7684 feat/session-* \u5206\u652F\uFF08\u9ED8\u8BA4\u4EC5\u5904\u7406\u672C\u5730\u5206\u652F\uFF09"
8262
- ).action(async (opts) => {
8388
+ "\u626B\u63CF feat/session-* \u5206\u652F\u5E76\u8F93\u51FA\u72B6\u6001\u8868\uFF1B\u52A0 --mode \u6309\u89C4\u5219\u6E05\u7406\u672C\u5730\u4E0E\u8FDC\u7A0B\u5206\u652F"
8389
+ ).option(
8390
+ "--mode <n>",
8391
+ "\u6E05\u7406\u6A21\u5F0F\uFF1A1=\u5DF2\u5408\u5E76\u6216\u5E73\u53F0\u65E0\u4EFB\u52A1\uFF1B2=\u6A21\u5F0F1+\u4EFB\u52A1\u5DF2\u5B8C\u6210/\u5DF2\u5931\u8D25"
8392
+ ).option("--dry-run", "\u914D\u5408 --mode \u4EC5\u9884\u89C8\u5C06\u88AB\u5220\u9664\u7684\u5206\u652F\uFF0C\u4E0D\u5B9E\u9645\u6267\u884C").action(async (opts) => {
8393
+ const modeRaw = opts.mode?.trim();
8394
+ let mode;
8395
+ if (modeRaw) {
8396
+ if (modeRaw !== "1" && modeRaw !== "2") {
8397
+ console.error("[apm] --mode \u4EC5\u652F\u6301 1 \u6216 2");
8398
+ process.exit(1);
8399
+ }
8400
+ mode = Number(modeRaw);
8401
+ }
8263
8402
  await runCleanBranches({
8264
8403
  dryRun: opts.dryRun,
8265
- includeRemote: opts.all
8404
+ mode
8266
8405
  });
8267
8406
  });
8268
8407
  branch.description("\u5207\u6362\u6216\u521B\u5EFA\u4F1A\u8BDD\u5206\u652F feat/session-<sessionId>").argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").option(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.104",
3
+ "version": "6.0.106",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,