ai-project-manage-cli 8.1.24 → 8.2.19

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/dist/index.js CHANGED
@@ -16,15 +16,6 @@ function resolveClientMachineId(cfg) {
16
16
  function resolveApiKey(cfg) {
17
17
  return (cfg.apiKey ?? cfg.token ?? "").trim();
18
18
  }
19
- function stripTrailingSlash(raw) {
20
- return raw.trim().replace(/\/+$/, "");
21
- }
22
- function readConfigSocketServer(cfg) {
23
- const fromNew = typeof cfg.socketServer === "string" ? cfg.socketServer.trim().replace(/\/+$/, "") : "";
24
- if (fromNew) return fromNew;
25
- const fromLegacy = typeof cfg.wsUrl === "string" ? cfg.wsUrl.trim().replace(/\/+$/, "") : "";
26
- return fromLegacy;
27
- }
28
19
  function tryReadApmConfig() {
29
20
  try {
30
21
  const raw = readFileSync(APM_CONFIG_PATH, "utf8");
@@ -40,12 +31,10 @@ function tryReadApmConfig() {
40
31
  if (!apiKey) return null;
41
32
  const cfg = v;
42
33
  const clientMachineId = resolveClientMachineId(cfg);
43
- const socketServer = readConfigSocketServer(cfg);
44
34
  return {
45
35
  baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
46
36
  apiKey,
47
- ...clientMachineId ? { clientMachineId } : {},
48
- ...socketServer ? { socketServer } : {}
37
+ ...clientMachineId ? { clientMachineId } : {}
49
38
  };
50
39
  } catch {
51
40
  return null;
@@ -58,80 +47,11 @@ function defaultApmConfig() {
58
47
  apiKey: ""
59
48
  };
60
49
  }
61
- function assertHttpBase(raw, label) {
62
- const u = stripTrailingSlash(raw);
63
- if (!u.startsWith("https://") && !u.startsWith("http://")) {
64
- throw new Error(`${label} \u5FC5\u987B\u4EE5 http:// \u6216 https:// \u5F00\u5934`);
65
- }
66
- return new URL(u).origin;
67
- }
68
- function normalizeSocketBase(raw) {
69
- const u = stripTrailingSlash(raw);
70
- if (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("wss://") || u.startsWith("ws://")) {
71
- return new URL(u).origin;
72
- }
73
- throw new Error(
74
- "socket server \u5FC5\u987B\u4EE5 http://\u3001https://\u3001ws:// \u6216 wss:// \u5F00\u5934"
75
- );
76
- }
77
- function httpBaseToWsOrigin(httpOrWsBase) {
78
- const u = stripTrailingSlash(httpOrWsBase);
79
- if (u.startsWith("wss://") || u.startsWith("ws://")) return u;
50
+ function httpBaseToWsOrigin(httpBase) {
51
+ const u = httpBase.trim().replace(/\/+$/, "");
80
52
  if (u.startsWith("https://")) return "wss://" + u.slice("https://".length);
81
53
  if (u.startsWith("http://")) return "ws://" + u.slice("http://".length);
82
- throw new Error("socket server \u5FC5\u987B\u4EE5 http(s):// \u6216 ws(s):// \u5F00\u5934");
83
- }
84
- function splitHistoricalServer(server) {
85
- const url = new URL(assertHttpBase(server, "server"));
86
- const socketServer = url.origin;
87
- if (url.port) {
88
- url.port = "";
89
- return { baseUrl: url.origin, socketServer };
90
- }
91
- return { baseUrl: url.origin, socketServer: url.origin };
92
- }
93
- function resolveApmEndpoints(input = {}) {
94
- const serverOpt = stripTrailingSlash(
95
- input.server?.trim() || process.env.APM_SERVER?.trim() || ""
96
- );
97
- const socketOpt = stripTrailingSlash(
98
- input.socketServer?.trim() || process.env.APM_SOCKET_SERVER?.trim() || ""
99
- );
100
- const configBase = stripTrailingSlash(input.configBaseUrl?.trim() || "");
101
- const configSocket = stripTrailingSlash(
102
- input.configSocketServer?.trim() || ""
103
- );
104
- if (serverOpt && socketOpt) {
105
- return {
106
- baseUrl: assertHttpBase(serverOpt, "server"),
107
- socketServer: normalizeSocketBase(socketOpt)
108
- };
109
- }
110
- if (serverOpt) {
111
- return splitHistoricalServer(serverOpt);
112
- }
113
- if (configBase && socketOpt) {
114
- return {
115
- baseUrl: assertHttpBase(configBase, "baseUrl"),
116
- socketServer: normalizeSocketBase(socketOpt)
117
- };
118
- }
119
- if (configBase && configSocket) {
120
- return {
121
- baseUrl: assertHttpBase(configBase, "baseUrl"),
122
- socketServer: normalizeSocketBase(configSocket)
123
- };
124
- }
125
- if (configBase) {
126
- return splitHistoricalServer(configBase);
127
- }
128
- if (socketOpt) {
129
- return {
130
- baseUrl: assertHttpBase(DEFAULT_BASE_URL, "server"),
131
- socketServer: normalizeSocketBase(socketOpt)
132
- };
133
- }
134
- return splitHistoricalServer(DEFAULT_BASE_URL);
54
+ throw new Error("baseUrl \u5FC5\u987B\u4EE5 http:// \u6216 https:// \u5F00\u5934");
135
55
  }
136
56
  async function ensureApmConfig() {
137
57
  const existing = await tryReadApmConfig();
@@ -143,25 +63,20 @@ async function ensureApmConfig() {
143
63
  async function writeApmConfig(cfg) {
144
64
  const clientMachineId = resolveClientMachineId(cfg);
145
65
  const apiKey = resolveApiKey(cfg);
146
- const socketServer = stripTrailingSlash(
147
- cfg.socketServer?.trim() || cfg.wsUrl?.trim() || ""
148
- );
149
66
  const normalized = {
150
67
  baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
151
68
  apiKey,
152
- ...clientMachineId ? { clientMachineId } : {},
153
- ...socketServer ? { socketServer } : {}
69
+ ...clientMachineId ? { clientMachineId } : {}
154
70
  };
155
71
  mkdirSync(APM_CONFIG_DIR, { recursive: true });
156
72
  writeFileSync(APM_CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", {
157
73
  mode: 384
158
74
  });
159
75
  }
160
- function buildAgentWsUrl(socketServer, apiKey) {
161
- const url = new URL(httpBaseToWsOrigin(socketServer));
162
- url.pathname = "/ws/agent";
163
- url.search = new URLSearchParams({ apiKey }).toString();
164
- return url.toString();
76
+ function buildAgentWsUrl(httpBase, apiKey) {
77
+ const origin = httpBaseToWsOrigin(httpBase);
78
+ const q = new URLSearchParams({ apiKey });
79
+ return `${origin}/ws/agent?${q.toString()}`;
165
80
  }
166
81
 
167
82
  // src/commands/sync-webide-agent.ts
@@ -289,10 +204,38 @@ var requestConfig = {
289
204
  method: "POST",
290
205
  path: "/cli/webide/pull-requests/merge"
291
206
  }),
207
+ getProjectDocumentManifest: defineEndpoint({
208
+ method: "GET",
209
+ path: "/cli/project-documents/manifest"
210
+ }),
211
+ listProjectDocuments: defineEndpoint({
212
+ method: "GET",
213
+ path: "/cli/project-documents"
214
+ }),
215
+ upsertProjectDocument: defineEndpoint({
216
+ method: "PUT",
217
+ path: "/cli/project-documents/upsert"
218
+ }),
219
+ removeProjectDocument: defineEndpoint({
220
+ method: "DELETE",
221
+ path: "/cli/project-documents"
222
+ }),
292
223
  getApmLogStorage: defineEndpoint({
293
224
  method: "GET",
294
225
  path: "/cli/apm-log-storage"
295
226
  }),
227
+ projectPackageUpdateStatus: defineEndpoint({
228
+ method: "PUT",
229
+ path: "/cli/project-packages/status"
230
+ }),
231
+ projectPackageAppendContent: defineEndpoint({
232
+ method: "PUT",
233
+ path: "/cli/project-packages/content"
234
+ }),
235
+ projectPackageEnsureContent: defineEndpoint({
236
+ method: "PUT",
237
+ path: "/cli/project-packages/ensure-content"
238
+ }),
296
239
  cursorStoreUpsertAgent: defineEndpoint({
297
240
  method: "PUT",
298
241
  path: "/cli/cursor-store/agents"
@@ -407,6 +350,9 @@ async function ensureLoggedConfig() {
407
350
  }
408
351
  return cfg;
409
352
  }
353
+ async function ensureDirExists(dir) {
354
+ mkdirSync2(dir, { recursive: true });
355
+ }
410
356
  async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
411
357
  const dir = workspaceApmDir(cwd);
412
358
  const fsDir = toFsPath(dir);
@@ -507,20 +453,15 @@ async function runInit() {
507
453
  // src/commands/login.ts
508
454
  import { ApiError } from "listpage-http";
509
455
  async function runLogin(opts) {
510
- const endpoints = resolveApmEndpoints({
511
- server: opts.server,
512
- socketServer: opts.socketServer
513
- });
456
+ const baseUrl = (opts.server?.trim() || process.env.AI_PM_SERVER?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
514
457
  const apiKey = (opts.apiKey?.trim() || process.env.APM_API_KEY?.trim() || "").replace(/^Bearer\s+/i, "");
515
458
  if (!apiKey.startsWith("cm_")) {
516
459
  console.error("[apm] \u8BF7\u63D0\u4F9B\u6709\u6548\u7684 API Key\uFF08cm_ \u5F00\u5934\uFF09");
517
- console.error(
518
- "[apm] \u7528\u6CD5: apm login --api-key cm_xxx [--server URL] [--socket-server URL]"
519
- );
460
+ console.error("[apm] \u7528\u6CD5: apm login --api-key cm_xxx [--server URL]");
520
461
  process.exit(1);
521
462
  }
522
463
  const api = createApmApiClient({
523
- baseUrl: endpoints.baseUrl,
464
+ baseUrl,
524
465
  clientMachineId: "",
525
466
  apiKey
526
467
  });
@@ -545,8 +486,7 @@ async function runLogin(opts) {
545
486
  process.exit(1);
546
487
  }
547
488
  const cfg = {
548
- baseUrl: endpoints.baseUrl,
549
- socketServer: endpoints.socketServer,
489
+ baseUrl,
550
490
  clientMachineId,
551
491
  apiKey
552
492
  };
@@ -557,8 +497,7 @@ async function runLogin(opts) {
557
497
  {
558
498
  clientMachineId: cfg.clientMachineId,
559
499
  clientName: data.clientName,
560
- baseUrl: cfg.baseUrl,
561
- socketServer: cfg.socketServer
500
+ baseUrl: cfg.baseUrl
562
501
  },
563
502
  null,
564
503
  2
@@ -773,10 +712,6 @@ function reexecConnectAfterUpdate(options) {
773
712
  if (server) {
774
713
  args.push("--server", server);
775
714
  }
776
- const socketServer = options.socketServer?.trim();
777
- if (socketServer) {
778
- args.push("--socket-server", socketServer);
779
- }
780
715
  const result = spawnSync(process.execPath, args, {
781
716
  stdio: "inherit",
782
717
  env: { ...process.env, [APM_SKIP_SELF_UPDATE_ENV]: "1" }
@@ -819,11 +754,363 @@ async function runUpdateSkills() {
819
754
  await syncWorkspaceSkills(resolveWorkdirPath());
820
755
  }
821
756
 
757
+ // src/utils/project-documents.ts
758
+ import {
759
+ existsSync as existsSync5,
760
+ readdirSync as readdirSync3,
761
+ readFileSync as readFileSync4,
762
+ rmSync,
763
+ writeFileSync as writeFileSync3
764
+ } from "fs";
765
+ import { createHash } from "crypto";
766
+ import { dirname as dirname2, join as join6, relative, sep } from "path";
767
+
768
+ // src/utils/git.ts
769
+ import { execFile as execFile2 } from "child_process";
770
+ import { promisify as promisify2 } from "util";
771
+
772
+ // src/utils/git-remote.ts
773
+ import { execFile } from "child_process";
774
+ import { promisify } from "util";
775
+ var execFileAsync = promisify(execFile);
776
+
777
+ // src/utils/git.ts
778
+ var execFileAsync2 = promisify2(execFile2);
779
+ function gitExecEnv() {
780
+ const existing = process.env.GIT_SSH_COMMAND?.trim();
781
+ const acceptNew = "-o StrictHostKeyChecking=accept-new";
782
+ let gitSshCommand;
783
+ if (!existing) {
784
+ gitSshCommand = `ssh ${acceptNew}`;
785
+ } else if (/\bStrictHostKeyChecking=/.test(existing)) {
786
+ gitSshCommand = existing;
787
+ } else {
788
+ gitSshCommand = `${existing} ${acceptNew}`;
789
+ }
790
+ return { ...process.env, GIT_SSH_COMMAND: gitSshCommand };
791
+ }
792
+ async function execGit(cwd, args, quiet = false) {
793
+ try {
794
+ const { stdout, stderr } = await execFileAsync2("git", args, {
795
+ cwd,
796
+ env: gitExecEnv(),
797
+ encoding: "utf8",
798
+ maxBuffer: 10 * 1024 * 1024
799
+ });
800
+ if (!quiet && stderr.trim()) {
801
+ process.stderr.write(stderr);
802
+ }
803
+ return stdout;
804
+ } catch (err) {
805
+ const e = err;
806
+ const detail = (e.stderr ?? e.message ?? String(err)).trim();
807
+ throw new Error(
808
+ `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
809
+ );
810
+ }
811
+ }
812
+ async function getCurrentBranch(cwd) {
813
+ return (await execGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
814
+ }
815
+ async function resolveGitRepoRoot(cwd) {
816
+ return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
817
+ }
818
+ var FEATURE_BRANCH_RE = /^feat\/task-(.+)$/;
819
+ function taskIdFromFeatureBranch(branch) {
820
+ const name = branch.trim().replace(/^origin\//, "");
821
+ const match = name.match(FEATURE_BRANCH_RE);
822
+ const id = match?.[1]?.trim() ?? "";
823
+ return id || null;
824
+ }
825
+
826
+ // src/utils/project-documents.ts
827
+ var MANIFEST_FILE = "manifest.json";
828
+ function normalizeProjectIdForPath(projectId) {
829
+ const id = projectId.trim();
830
+ if (!id) {
831
+ throw new Error("projectId \u4E0D\u80FD\u4E3A\u7A7A");
832
+ }
833
+ if (id.includes("..") || id.includes("/") || id.includes("\\") || id.includes("\0")) {
834
+ throw new Error(`\u975E\u6CD5 projectId: ${projectId}`);
835
+ }
836
+ return id;
837
+ }
838
+ function projectDocumentsDir(apmRoot, projectId) {
839
+ const id = normalizeProjectIdForPath(projectId);
840
+ return join6(apmRoot ?? workspaceApmDir(), "project", id);
841
+ }
842
+ function projectDocumentLocalPath(apmRoot, projectId, documentPath) {
843
+ const normalized = normalizeLocalDocumentPath(documentPath);
844
+ return join6(
845
+ projectDocumentsDir(apmRoot, projectId),
846
+ ...normalized.split("/")
847
+ );
848
+ }
849
+ function normalizeLocalDocumentPath(path) {
850
+ const trimmed = path.trim().replace(/\\/g, "/");
851
+ if (!trimmed || trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) {
852
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path}`);
853
+ }
854
+ const segments = trimmed.split("/").filter(Boolean);
855
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
856
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path}`);
857
+ }
858
+ return segments.join("/");
859
+ }
860
+ function hashLocalFileContent(content) {
861
+ return createHash("sha256").update(content, "utf8").digest("hex");
862
+ }
863
+ function readLocalManifest(apmRoot, projectId) {
864
+ const manifestPath = join6(
865
+ projectDocumentsDir(apmRoot, projectId),
866
+ MANIFEST_FILE
867
+ );
868
+ if (!existsSync5(manifestPath)) {
869
+ return null;
870
+ }
871
+ try {
872
+ return JSON.parse(
873
+ readFileSync4(manifestPath, "utf8")
874
+ );
875
+ } catch {
876
+ return null;
877
+ }
878
+ }
879
+ function listLocalDocumentPaths(apmRoot, projectId) {
880
+ const root = projectDocumentsDir(apmRoot, projectId);
881
+ if (!existsSync5(root)) {
882
+ return [];
883
+ }
884
+ const paths = [];
885
+ const walk = (dir) => {
886
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
887
+ const abs = join6(dir, entry.name);
888
+ if (entry.isDirectory()) {
889
+ walk(abs);
890
+ continue;
891
+ }
892
+ if (entry.isFile() && entry.name === MANIFEST_FILE) {
893
+ continue;
894
+ }
895
+ const rel = relative(root, abs).split(sep).join("/");
896
+ paths.push(rel);
897
+ }
898
+ };
899
+ walk(root);
900
+ return paths.sort();
901
+ }
902
+ function diffManifestPaths(remote, local) {
903
+ const remoteMap = new Map(
904
+ (remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
905
+ );
906
+ const localMap = new Map(
907
+ (local?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
908
+ );
909
+ const download = [];
910
+ for (const [path, hash] of remoteMap) {
911
+ if (localMap.get(path) !== hash) {
912
+ download.push(path);
913
+ }
914
+ }
915
+ const deleteLocal = [];
916
+ for (const path of localMap.keys()) {
917
+ if (!remoteMap.has(path)) {
918
+ deleteLocal.push(path);
919
+ }
920
+ }
921
+ return { download, deleteLocal };
922
+ }
923
+ async function resolveProjectIdForSync(api, workdirPath, options) {
924
+ const explicit = options?.projectId?.trim();
925
+ if (explicit) {
926
+ return { projectId: explicit, diagnostic: null };
927
+ }
928
+ try {
929
+ const gitRoot = await resolveGitRepoRoot(workdirPath);
930
+ const current = await getCurrentBranch(gitRoot);
931
+ const taskId = taskIdFromFeatureBranch(current);
932
+ if (!taskId) {
933
+ return {
934
+ projectId: null,
935
+ diagnostic: `\u5F53\u524D\u5206\u652F ${current} \u4E0D\u662F feat/task-*\uFF0C\u65E0\u6CD5\u89E3\u6790\u6240\u5C5E\u9879\u76EE\u3002
936
+ \u8BF7\u5728\u7279\u6027\u5206\u652F\u4E0A\u6267\u884C\uFF0C\u6216\u663E\u5F0F\u6307\u5B9A projectId\u3002`
937
+ };
938
+ }
939
+ const baseline = await api.cli.projectBaseBranch({ taskId });
940
+ const projectId = baseline.projectId?.trim() || null;
941
+ if (!projectId) {
942
+ return {
943
+ projectId: null,
944
+ diagnostic: `\u4EFB\u52A1 ${taskId} \u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
945
+ };
946
+ }
947
+ return { projectId, diagnostic: null };
948
+ } catch (err) {
949
+ const detail = err instanceof Error ? err.message : String(err);
950
+ return {
951
+ projectId: null,
952
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
953
+ };
954
+ }
955
+ }
956
+ async function syncProjectDocumentsPull(workdirPath, apmDir, options) {
957
+ const empty = {
958
+ synced: false,
959
+ projectId: null,
960
+ downloaded: 0,
961
+ deleted: 0
962
+ };
963
+ const cfg = await tryReadApmConfig();
964
+ if (!cfg || !resolveApiKey(cfg)) {
965
+ console.log(
966
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
967
+ );
968
+ return empty;
969
+ }
970
+ const api = createApmApiClient(cfg);
971
+ const { projectId, diagnostic } = await resolveProjectIdForSync(
972
+ api,
973
+ workdirPath,
974
+ options
975
+ );
976
+ if (!projectId) {
977
+ console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
978
+ ${diagnostic ?? ""}`);
979
+ return empty;
980
+ }
981
+ const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
982
+ const docsDir = projectDocumentsDir(targetApmDir, projectId);
983
+ await ensureDirExists(docsDir);
984
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest(
985
+ { projectId }
986
+ );
987
+ if (!remoteManifest) {
988
+ console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
989
+ return { ...empty, projectId };
990
+ }
991
+ const localManifest = readLocalManifest(targetApmDir, projectId);
992
+ const { download, deleteLocal } = diffManifestPaths(
993
+ remoteManifest,
994
+ localManifest
995
+ );
996
+ let downloaded = 0;
997
+ if (download.length > 0) {
998
+ const { list } = await api.cli.listProjectDocuments({
999
+ projectId,
1000
+ paths: download.join(",")
1001
+ });
1002
+ for (const doc of list) {
1003
+ const absPath = toFsPath(
1004
+ projectDocumentLocalPath(targetApmDir, projectId, doc.path)
1005
+ );
1006
+ await ensureDirExists(dirname2(absPath));
1007
+ writeFileSync3(absPath, doc.content, "utf8");
1008
+ downloaded += 1;
1009
+ }
1010
+ }
1011
+ let deleted = 0;
1012
+ for (const path of deleteLocal) {
1013
+ const absPath = toFsPath(
1014
+ projectDocumentLocalPath(targetApmDir, projectId, path)
1015
+ );
1016
+ if (existsSync5(absPath)) {
1017
+ rmSync(absPath, { force: true });
1018
+ deleted += 1;
1019
+ }
1020
+ }
1021
+ writeFileSync3(
1022
+ toFsPath(join6(docsDir, MANIFEST_FILE)),
1023
+ `${JSON.stringify(remoteManifest, null, 2)}
1024
+ `,
1025
+ "utf8"
1026
+ );
1027
+ console.log(
1028
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: projectId=${projectId} \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted} \u2192 .apm/project/${projectId}/`
1029
+ );
1030
+ return {
1031
+ synced: true,
1032
+ projectId,
1033
+ downloaded,
1034
+ deleted
1035
+ };
1036
+ }
1037
+ async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot, options) {
1038
+ const api = createApmApiClient(cfg);
1039
+ const { projectId } = await resolveProjectIdForSync(
1040
+ api,
1041
+ workdirPath,
1042
+ options
1043
+ );
1044
+ if (!projectId) {
1045
+ return 0;
1046
+ }
1047
+ const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
1048
+ const localPaths = listLocalDocumentPaths(targetApmDir, projectId);
1049
+ if (localPaths.length === 0) {
1050
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1051
+ return 0;
1052
+ }
1053
+ const remoteManifest = (await api.cli.getProjectDocumentManifest({ projectId })).manifest ?? null;
1054
+ const remoteHashByPath = new Map(
1055
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1056
+ );
1057
+ const remoteDescriptionByPath = new Map(
1058
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.description])
1059
+ );
1060
+ let synced = 0;
1061
+ for (const path of localPaths) {
1062
+ const absPath = toFsPath(
1063
+ projectDocumentLocalPath(targetApmDir, projectId, path)
1064
+ );
1065
+ const content = readFileSync4(absPath, "utf8");
1066
+ const contentHash = hashLocalFileContent(content);
1067
+ if (remoteHashByPath.get(path) === contentHash) {
1068
+ continue;
1069
+ }
1070
+ await api.cli.upsertProjectDocument({
1071
+ projectId,
1072
+ path,
1073
+ content,
1074
+ description: remoteDescriptionByPath.get(path) ?? void 0
1075
+ });
1076
+ synced += 1;
1077
+ console.log(`[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: ${path}`);
1078
+ }
1079
+ if (synced === 0) {
1080
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1081
+ }
1082
+ return synced;
1083
+ }
1084
+
1085
+ // src/commands/sync-project-documents.ts
1086
+ async function runSyncProjectDocuments(options) {
1087
+ const workdir = resolveWorkdirPath();
1088
+ const apmRoot = workspaceApmDir(workdir);
1089
+ const doPush = Boolean(options.push);
1090
+ const doPull = Boolean(options.pull) || !doPush;
1091
+ if (doPull) {
1092
+ await syncProjectDocumentsPull(workdir, apmRoot);
1093
+ }
1094
+ if (doPush) {
1095
+ const cfg = await tryReadApmConfig();
1096
+ if (!cfg) {
1097
+ throw new Error("[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8BF7\u5148\u6267\u884C apm login");
1098
+ }
1099
+ await syncProjectDocumentsPush(cfg, workdir, apmRoot);
1100
+ }
1101
+ }
1102
+
822
1103
  // src/commands/connect.ts
823
1104
  import WebSocket from "ws";
824
- import { setTimeout as delay2 } from "node:timers/promises";
1105
+ import { setTimeout as delay } from "node:timers/promises";
825
1106
 
826
1107
  // src/ws/protocol.ts
1108
+ function isWebIdeStartProjectTarget(value) {
1109
+ return value === "frontend" || value === "backend" || value === "mobile";
1110
+ }
1111
+ function isWebIdePackageTarget(value) {
1112
+ return value === "frontend" || value === "backend";
1113
+ }
827
1114
  function nonEmptyString(v) {
828
1115
  return typeof v === "string" && v.trim().length > 0;
829
1116
  }
@@ -834,6 +1121,18 @@ function parseAgentWsMessage(raw) {
834
1121
  return null;
835
1122
  }
836
1123
  }
1124
+ function serializeAgentWsMessage(msg) {
1125
+ return JSON.stringify(msg);
1126
+ }
1127
+ function validateHeartbeat(o) {
1128
+ if (o.type !== "heartbeat") {
1129
+ return { ok: false, reason: "\u671F\u671B heartbeat" };
1130
+ }
1131
+ if (!nonEmptyString(o.userId)) {
1132
+ return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
1133
+ }
1134
+ return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
1135
+ }
837
1136
  function validateWebIdeMessagePush(o) {
838
1137
  if (o.type !== "webide-message") {
839
1138
  return { ok: false, reason: "\u671F\u671B webide-message" };
@@ -912,21 +1211,143 @@ function validateSteer(o) {
912
1211
  }
913
1212
  };
914
1213
  }
915
- function validateAgentWsMessage(value, _kind = "outbound") {
1214
+ function validateWebIdeStopTerminals(o) {
1215
+ if (o.type !== "webide-stop-terminals") {
1216
+ return { ok: false, reason: "\u671F\u671B webide-stop-terminals" };
1217
+ }
1218
+ if (!nonEmptyString(o.taskId)) {
1219
+ return { ok: false, reason: "webide-stop-terminals \u7F3A\u5C11 taskId" };
1220
+ }
1221
+ return {
1222
+ ok: true,
1223
+ data: {
1224
+ type: "webide-stop-terminals",
1225
+ taskId: o.taskId.trim()
1226
+ }
1227
+ };
1228
+ }
1229
+ function validateWebIdeStartProjectPush(o) {
1230
+ if (o.type !== "webide-start-project") {
1231
+ return { ok: false, reason: "\u671F\u671B webide-start-project" };
1232
+ }
1233
+ if (!nonEmptyString(o.taskId)) {
1234
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 taskId" };
1235
+ }
1236
+ if (!nonEmptyString(o.messageId)) {
1237
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 messageId" };
1238
+ }
1239
+ if (!isWebIdeStartProjectTarget(o.target)) {
1240
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11\u6709\u6548 target" };
1241
+ }
1242
+ if (!nonEmptyString(o.content)) {
1243
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 content" };
1244
+ }
1245
+ if (!nonEmptyString(o.model)) {
1246
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 model" };
1247
+ }
1248
+ if (!nonEmptyString(o.apiKey)) {
1249
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 apiKey" };
1250
+ }
1251
+ if (!nonEmptyString(o.workdir)) {
1252
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 workdir" };
1253
+ }
1254
+ if (!nonEmptyString(o.user)) {
1255
+ return { ok: false, reason: "webide-start-project \u7F3A\u5C11 user" };
1256
+ }
1257
+ return {
1258
+ ok: true,
1259
+ data: {
1260
+ type: "webide-start-project",
1261
+ taskId: o.taskId.trim(),
1262
+ messageId: o.messageId.trim(),
1263
+ target: o.target,
1264
+ restart: o.restart === true,
1265
+ content: o.content,
1266
+ model: o.model.trim(),
1267
+ apiKey: o.apiKey.trim(),
1268
+ workdir: o.workdir.trim(),
1269
+ user: o.user.trim()
1270
+ }
1271
+ };
1272
+ }
1273
+ function validateWebIdePackagePush(o) {
1274
+ if (o.type !== "webide-package") {
1275
+ return { ok: false, reason: "\u671F\u671B webide-package" };
1276
+ }
1277
+ if (!nonEmptyString(o.packageId)) {
1278
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 packageId" };
1279
+ }
1280
+ if (!nonEmptyString(o.messageId)) {
1281
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 messageId" };
1282
+ }
1283
+ if (!nonEmptyString(o.projectId)) {
1284
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 projectId" };
1285
+ }
1286
+ if (!isWebIdePackageTarget(o.target)) {
1287
+ return { ok: false, reason: "webide-package \u7F3A\u5C11\u6709\u6548 target" };
1288
+ }
1289
+ if (!nonEmptyString(o.baseBranch)) {
1290
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 baseBranch" };
1291
+ }
1292
+ if (!nonEmptyString(o.content)) {
1293
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 content" };
1294
+ }
1295
+ if (!nonEmptyString(o.model)) {
1296
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 model" };
1297
+ }
1298
+ if (!nonEmptyString(o.apiKey)) {
1299
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 apiKey" };
1300
+ }
1301
+ if (!nonEmptyString(o.workdir)) {
1302
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 workdir" };
1303
+ }
1304
+ if (!nonEmptyString(o.user)) {
1305
+ return { ok: false, reason: "webide-package \u7F3A\u5C11 user" };
1306
+ }
1307
+ return {
1308
+ ok: true,
1309
+ data: {
1310
+ type: "webide-package",
1311
+ packageId: o.packageId.trim(),
1312
+ messageId: o.messageId.trim(),
1313
+ projectId: o.projectId.trim(),
1314
+ target: o.target,
1315
+ baseBranch: o.baseBranch.trim().replace(/^origin\//, ""),
1316
+ content: o.content,
1317
+ model: o.model.trim(),
1318
+ apiKey: o.apiKey.trim(),
1319
+ workdir: o.workdir.trim(),
1320
+ user: o.user.trim()
1321
+ }
1322
+ };
1323
+ }
1324
+ function validateAgentWsMessage(value, kind) {
916
1325
  if (typeof value !== "object" || value === null) {
917
1326
  return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
918
1327
  }
919
1328
  const o = value;
920
1329
  const type = o.type;
921
- if (type !== "webide-message" && type !== "cancel" && type !== "steer") {
1330
+ if (type !== "heartbeat" && type !== "webide-message" && type !== "cancel" && type !== "steer" && type !== "webide-start-project" && type !== "webide-stop-terminals" && type !== "webide-package") {
922
1331
  return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
923
1332
  }
1333
+ if (kind === "heartbeat" || type === "heartbeat") {
1334
+ return validateHeartbeat(o);
1335
+ }
924
1336
  if (type === "cancel") {
925
1337
  return validateCancel(o);
926
1338
  }
927
1339
  if (type === "steer") {
928
1340
  return validateSteer(o);
929
1341
  }
1342
+ if (type === "webide-start-project") {
1343
+ return validateWebIdeStartProjectPush(o);
1344
+ }
1345
+ if (type === "webide-stop-terminals") {
1346
+ return validateWebIdeStopTerminals(o);
1347
+ }
1348
+ if (type === "webide-package") {
1349
+ return validateWebIdePackagePush(o);
1350
+ }
930
1351
  return validateWebIdeMessagePush(o);
931
1352
  }
932
1353
 
@@ -961,7 +1382,7 @@ function handleSteerAction(payload, ctx) {
961
1382
  }
962
1383
 
963
1384
  // src/commands/connect/core/worker/worker.ts
964
- import { dirname as dirname2, join as join6 } from "path";
1385
+ import { dirname as dirname3, join as join7 } from "path";
965
1386
  import { fileURLToPath as fileURLToPath2 } from "url";
966
1387
  import { Worker } from "node:worker_threads";
967
1388
 
@@ -987,8 +1408,8 @@ function resolveWebIdeWorkerHeapMb(env = process.env, totalMemoryBytes = totalme
987
1408
  }
988
1409
 
989
1410
  // src/commands/connect/core/worker/worker.ts
990
- var workerFile = join6(
991
- dirname2(fileURLToPath2(import.meta.url)),
1411
+ var workerFile = join7(
1412
+ dirname3(fileURLToPath2(import.meta.url)),
992
1413
  "worker-script.js"
993
1414
  );
994
1415
  var ActionWorker = class {
@@ -1007,8 +1428,8 @@ var ActionWorker = class {
1007
1428
  }
1008
1429
  this.msg = msg;
1009
1430
  this.apmClient = createApmApiClient(cfg);
1010
- this.promise = new Promise((resolve5, reject) => {
1011
- this.resolve = resolve5;
1431
+ this.promise = new Promise((resolve3, reject) => {
1432
+ this.resolve = resolve3;
1012
1433
  this.reject = reject;
1013
1434
  });
1014
1435
  this.worker = new Worker(workerFile, {
@@ -1064,6 +1485,14 @@ var ActionWorker = class {
1064
1485
  failIfStillQueued = async (reason) => {
1065
1486
  if (this.status !== "pending") return;
1066
1487
  try {
1488
+ if (this.msg.type === "webide-package") {
1489
+ await this.apmClient.cli.projectPackageUpdateStatus({
1490
+ id: this.msg.packageId,
1491
+ status: "CANCELLED",
1492
+ error: reason
1493
+ });
1494
+ return;
1495
+ }
1067
1496
  await this.apmClient.cli.webideSetMessageError({
1068
1497
  id: this.msg.messageId,
1069
1498
  error: reason,
@@ -1136,9 +1565,191 @@ function handleWebIdeMessageAction(payload, ctx) {
1136
1565
  trackActionTask(ctx, task);
1137
1566
  }
1138
1567
 
1568
+ // src/commands/connect/actions/start-project.ts
1569
+ function handleStartProjectAction(payload, ctx) {
1570
+ const perMessageController = new AbortController();
1571
+ ctx.activeRuns.set(payload.messageId, perMessageController);
1572
+ if (ctx.pendingCancels.has(payload.messageId)) {
1573
+ ctx.activeRuns.delete(payload.messageId);
1574
+ ctx.pendingCancels.delete(payload.messageId);
1575
+ const cancelTask = (async () => {
1576
+ try {
1577
+ const api = createApmApiClient(ctx.cfg);
1578
+ await api.cli.webideUpdateMessageStatus({
1579
+ id: payload.messageId,
1580
+ status: "CANCELLED"
1581
+ });
1582
+ } catch (err) {
1583
+ console.error(
1584
+ `[apm] webide-start-project \u9884\u53D6\u6D88\u5199\u7EC8\u6001\u5931\u8D25 messageId=${payload.messageId}:`,
1585
+ err instanceof Error ? err.message : err
1586
+ );
1587
+ }
1588
+ })();
1589
+ trackActionTask(ctx, cancelTask);
1590
+ return;
1591
+ }
1592
+ const task = (async () => {
1593
+ const handle = new ActionWorker(payload);
1594
+ registerRunSteer(ctx, payload.messageId, handle);
1595
+ const onAbort = () => handle.cancel(payload.messageId);
1596
+ perMessageController.signal.addEventListener("abort", onAbort, {
1597
+ once: true
1598
+ });
1599
+ try {
1600
+ await handle.done;
1601
+ } finally {
1602
+ perMessageController.signal.removeEventListener("abort", onAbort);
1603
+ unregisterRunSteer(ctx, payload.messageId);
1604
+ ctx.activeRuns.delete(payload.messageId);
1605
+ }
1606
+ })();
1607
+ trackActionTask(ctx, task);
1608
+ }
1609
+
1610
+ // src/commands/connect/mcps/pty-session-mcp.ts
1611
+ var DEFAULT_PTY_SESSION_PORT = "7788";
1612
+ function resolvePtySessionBaseUrl(env = process.env) {
1613
+ const explicit = env.LOCAL_TERMINAL_URL?.trim();
1614
+ if (explicit) return explicit.replace(/\/+$/, "");
1615
+ const port = env.LOCAL_TERMINAL_PORT?.trim() || DEFAULT_PTY_SESSION_PORT;
1616
+ return `http://127.0.0.1:${port}`;
1617
+ }
1618
+ function asSessionList(data) {
1619
+ if (Array.isArray(data)) return data;
1620
+ if (data && typeof data === "object") {
1621
+ const sessions = data.sessions;
1622
+ if (Array.isArray(sessions)) return sessions;
1623
+ }
1624
+ return [];
1625
+ }
1626
+ async function requestPtyJson(url, token, init) {
1627
+ const res = await fetch(url, {
1628
+ ...init,
1629
+ headers: {
1630
+ Authorization: `Bearer ${token}`,
1631
+ ...init?.headers ?? {}
1632
+ }
1633
+ });
1634
+ if (!res.ok) {
1635
+ throw new Error(`pty-session HTTP ${res.status}`);
1636
+ }
1637
+ try {
1638
+ return await res.json();
1639
+ } catch {
1640
+ return null;
1641
+ }
1642
+ }
1643
+ async function killAllPtySessions(cfg) {
1644
+ const token = resolveApiKey(cfg);
1645
+ if (!token) return 0;
1646
+ const baseUrl = resolvePtySessionBaseUrl();
1647
+ let body;
1648
+ try {
1649
+ body = await requestPtyJson(`${baseUrl}/sessions`, token);
1650
+ } catch (err) {
1651
+ console.warn(
1652
+ "[apm] \u5217\u51FA PTY \u4F1A\u8BDD\u5931\u8D25:",
1653
+ err instanceof Error ? err.message : err
1654
+ );
1655
+ return 0;
1656
+ }
1657
+ const payload = body;
1658
+ if (payload && typeof payload === "object" && payload.ok === false) {
1659
+ return 0;
1660
+ }
1661
+ const data = payload && typeof payload === "object" && "data" in payload ? payload.data : body;
1662
+ const sessions = asSessionList(data);
1663
+ let killed = 0;
1664
+ for (const session of sessions) {
1665
+ const id = session.id?.trim();
1666
+ if (!id) continue;
1667
+ if (session.running === false) continue;
1668
+ try {
1669
+ await requestPtyJson(
1670
+ `${baseUrl}/sessions/${encodeURIComponent(id)}`,
1671
+ token,
1672
+ {
1673
+ method: "DELETE"
1674
+ }
1675
+ );
1676
+ killed += 1;
1677
+ } catch (err) {
1678
+ console.warn(
1679
+ `[apm] \u7ED3\u675F PTY \u4F1A\u8BDD\u5931\u8D25 id=${id}:`,
1680
+ err instanceof Error ? err.message : err
1681
+ );
1682
+ }
1683
+ }
1684
+ return killed;
1685
+ }
1686
+
1687
+ // src/commands/connect/actions/stop-terminals.ts
1688
+ function handleStopTerminalsAction(payload, ctx) {
1689
+ const task = (async () => {
1690
+ try {
1691
+ const killed = await killAllPtySessions(ctx.cfg);
1692
+ console.log(
1693
+ `[apm] webide-stop-terminals taskId=${payload.taskId} killed=${killed}`
1694
+ );
1695
+ } catch (err) {
1696
+ console.warn(
1697
+ `[apm] webide-stop-terminals \u5931\u8D25 taskId=${payload.taskId}:`,
1698
+ err instanceof Error ? err.message : err
1699
+ );
1700
+ }
1701
+ })();
1702
+ trackActionTask(ctx, task);
1703
+ }
1704
+
1705
+ // src/commands/connect/actions/package.ts
1706
+ function handleWebIdePackageAction(payload, ctx) {
1707
+ const perMessageController = new AbortController();
1708
+ ctx.activeRuns.set(payload.messageId, perMessageController);
1709
+ if (ctx.pendingCancels.has(payload.messageId)) {
1710
+ ctx.activeRuns.delete(payload.messageId);
1711
+ ctx.pendingCancels.delete(payload.messageId);
1712
+ const cancelTask = (async () => {
1713
+ try {
1714
+ const api = createApmApiClient(ctx.cfg);
1715
+ await api.cli.projectPackageUpdateStatus({
1716
+ id: payload.packageId,
1717
+ status: "CANCELLED",
1718
+ error: "\u5DF2\u53D6\u6D88"
1719
+ });
1720
+ } catch (err) {
1721
+ console.error(
1722
+ `[apm] webide-package \u9884\u53D6\u6D88\u5199\u7EC8\u6001\u5931\u8D25 packageId=${payload.packageId}:`,
1723
+ err instanceof Error ? err.message : err
1724
+ );
1725
+ }
1726
+ })();
1727
+ trackActionTask(ctx, cancelTask);
1728
+ return;
1729
+ }
1730
+ const task = (async () => {
1731
+ const handle = new ActionWorker(payload);
1732
+ registerRunSteer(ctx, payload.messageId, handle);
1733
+ const onAbort = () => handle.cancel(payload.messageId);
1734
+ perMessageController.signal.addEventListener("abort", onAbort, {
1735
+ once: true
1736
+ });
1737
+ try {
1738
+ await handle.done;
1739
+ } finally {
1740
+ perMessageController.signal.removeEventListener("abort", onAbort);
1741
+ unregisterRunSteer(ctx, payload.messageId);
1742
+ ctx.activeRuns.delete(payload.messageId);
1743
+ }
1744
+ })();
1745
+ trackActionTask(ctx, task);
1746
+ }
1747
+
1139
1748
  // src/commands/connect/actions/index.ts
1140
1749
  function dispatchConnectAction(payload, ctx) {
1141
1750
  switch (payload.type) {
1751
+ case "heartbeat":
1752
+ return;
1142
1753
  case "cancel":
1143
1754
  handleCancelAction(payload, ctx);
1144
1755
  return;
@@ -1148,22 +1759,28 @@ function dispatchConnectAction(payload, ctx) {
1148
1759
  case "webide-message":
1149
1760
  handleWebIdeMessageAction(payload, ctx);
1150
1761
  return;
1762
+ case "webide-start-project":
1763
+ handleStartProjectAction(payload, ctx);
1764
+ return;
1765
+ case "webide-stop-terminals":
1766
+ handleStopTerminalsAction(payload, ctx);
1767
+ return;
1768
+ case "webide-package":
1769
+ handleWebIdePackageAction(payload, ctx);
1770
+ return;
1151
1771
  }
1152
1772
  }
1153
1773
 
1154
1774
  // src/commands/connect-lock.ts
1155
1775
  import {
1156
- existsSync as existsSync5,
1776
+ existsSync as existsSync6,
1157
1777
  mkdirSync as mkdirSync4,
1158
- readFileSync as readFileSync4,
1778
+ readFileSync as readFileSync5,
1159
1779
  unlinkSync as unlinkSync2,
1160
- writeFileSync as writeFileSync3
1780
+ writeFileSync as writeFileSync4
1161
1781
  } from "fs";
1162
- import { setTimeout as delay } from "node:timers/promises";
1163
- import { join as join7 } from "path";
1164
- var CONNECT_LOCK_PATH = join7(APM_CONFIG_DIR, "connect.lock");
1165
- var ACQUIRE_RETRY_MS = 5e3;
1166
- var ACQUIRE_RETRY_INTERVAL_MS = 200;
1782
+ import { join as join8 } from "path";
1783
+ var CONNECT_LOCK_PATH = join8(APM_CONFIG_DIR, "connect.lock");
1167
1784
  function isProcessAlive(pid) {
1168
1785
  if (!Number.isInteger(pid) || pid <= 0) return false;
1169
1786
  try {
@@ -1174,9 +1791,9 @@ function isProcessAlive(pid) {
1174
1791
  }
1175
1792
  }
1176
1793
  function readConnectLock() {
1177
- if (!existsSync5(CONNECT_LOCK_PATH)) return null;
1794
+ if (!existsSync6(CONNECT_LOCK_PATH)) return null;
1178
1795
  try {
1179
- const raw = readFileSync4(CONNECT_LOCK_PATH, "utf8");
1796
+ const raw = readFileSync5(CONNECT_LOCK_PATH, "utf8");
1180
1797
  const parsed = JSON.parse(raw);
1181
1798
  if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string") {
1182
1799
  return null;
@@ -1198,44 +1815,27 @@ function pruneStaleConnectLock() {
1198
1815
  } catch {
1199
1816
  }
1200
1817
  }
1201
- function writeConnectLock() {
1818
+ function acquireConnectLock() {
1819
+ pruneStaleConnectLock();
1820
+ const existing = readConnectLock();
1821
+ if (existing && isProcessAlive(existing.pid)) {
1822
+ if (existing.pid === process.pid) {
1823
+ return;
1824
+ }
1825
+ console.error(`[apm] \u5DF2\u6709 apm connect \u5728\u8FD0\u884C (pid=${existing.pid})`);
1826
+ process.exit(1);
1827
+ }
1202
1828
  mkdirSync4(APM_CONFIG_DIR, { recursive: true });
1203
1829
  const lock = {
1204
1830
  pid: process.pid,
1205
1831
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1206
1832
  };
1207
- writeFileSync3(
1833
+ writeFileSync4(
1208
1834
  CONNECT_LOCK_PATH,
1209
1835
  JSON.stringify(lock, null, 2) + "\n",
1210
1836
  "utf8"
1211
1837
  );
1212
1838
  }
1213
- async function acquireConnectLock() {
1214
- const deadline = Date.now() + ACQUIRE_RETRY_MS;
1215
- let loggedWait = false;
1216
- for (; ; ) {
1217
- pruneStaleConnectLock();
1218
- const existing = readConnectLock();
1219
- if (!existing || !isProcessAlive(existing.pid)) {
1220
- writeConnectLock();
1221
- return;
1222
- }
1223
- if (existing.pid === process.pid) {
1224
- return;
1225
- }
1226
- if (Date.now() >= deadline) {
1227
- console.error(`[apm] \u5DF2\u6709 apm connect \u5728\u8FD0\u884C (pid=${existing.pid})`);
1228
- process.exit(1);
1229
- }
1230
- if (!loggedWait) {
1231
- console.log(
1232
- `[apm] \u7B49\u5F85\u65E7 connect \u91CA\u653E\u9501 (pid=${existing.pid})\u2026`
1233
- );
1234
- loggedWait = true;
1235
- }
1236
- await delay(ACQUIRE_RETRY_INTERVAL_MS);
1237
- }
1238
- }
1239
1839
  function releaseConnectLock() {
1240
1840
  const lock = readConnectLock();
1241
1841
  if (lock?.pid !== process.pid) return;
@@ -1246,20 +1846,36 @@ function releaseConnectLock() {
1246
1846
  }
1247
1847
 
1248
1848
  // src/commands/connect.ts
1849
+ var HEARTBEAT_MS = 3e4;
1249
1850
  var RECONNECT_MIN_MS = 1e3;
1250
1851
  var RECONNECT_MAX_MS = 6e4;
1251
1852
  var SHUTDOWN_DRAIN_MS = 3e3;
1252
1853
  function interruptibleSleep(ms, signal) {
1253
1854
  if (signal.aborted) return Promise.resolve();
1254
- return new Promise((resolve5) => {
1255
- const timer = setTimeout(resolve5, ms);
1855
+ return new Promise((resolve3) => {
1856
+ const timer = setTimeout(resolve3, ms);
1256
1857
  const onAbort = () => {
1257
1858
  clearTimeout(timer);
1258
- resolve5();
1859
+ resolve3();
1259
1860
  };
1260
1861
  signal.addEventListener("abort", onAbort, { once: true });
1261
1862
  });
1262
1863
  }
1864
+ function startHeartbeat(ws, clientMachineId) {
1865
+ const send = () => {
1866
+ if (ws.readyState === WebSocket.OPEN) {
1867
+ ws.send(
1868
+ serializeAgentWsMessage({
1869
+ type: "heartbeat",
1870
+ userId: clientMachineId
1871
+ })
1872
+ );
1873
+ }
1874
+ };
1875
+ send();
1876
+ const timer = setInterval(send, HEARTBEAT_MS);
1877
+ return () => clearInterval(timer);
1878
+ }
1263
1879
  function attachWsHandlers(ws, ctx, onOpen) {
1264
1880
  const { isShuttingDown } = ctx;
1265
1881
  ws.on("open", () => {
@@ -1274,6 +1890,9 @@ function attachWsHandlers(ws, ctx, onOpen) {
1274
1890
  console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
1275
1891
  return;
1276
1892
  }
1893
+ if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
1894
+ return;
1895
+ }
1277
1896
  const validated = validateAgentWsMessage(parsed, "outbound");
1278
1897
  if (!validated.ok) {
1279
1898
  console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
@@ -1283,13 +1902,15 @@ function attachWsHandlers(ws, ctx, onOpen) {
1283
1902
  });
1284
1903
  }
1285
1904
  function connectOnce(url, ctx, connectionAbort, onConnected) {
1286
- return new Promise((resolve5, reject) => {
1905
+ return new Promise((resolve3, reject) => {
1287
1906
  const ws = new WebSocket(url);
1907
+ let stopHeartbeat;
1288
1908
  let settled = false;
1289
1909
  const finish = (fn) => {
1290
1910
  if (settled) return;
1291
1911
  settled = true;
1292
1912
  connectionAbort.removeEventListener("abort", onConnectionAbort);
1913
+ stopHeartbeat?.();
1293
1914
  fn();
1294
1915
  };
1295
1916
  const onConnectionAbort = () => {
@@ -1298,7 +1919,10 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
1298
1919
  }
1299
1920
  };
1300
1921
  connectionAbort.addEventListener("abort", onConnectionAbort);
1301
- attachWsHandlers(ws, ctx, onConnected);
1922
+ attachWsHandlers(ws, ctx, () => {
1923
+ stopHeartbeat = startHeartbeat(ws, ctx.clientMachineId);
1924
+ onConnected();
1925
+ });
1302
1926
  ws.on("close", (code, reason) => {
1303
1927
  console.log(
1304
1928
  `[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
@@ -1307,7 +1931,7 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
1307
1931
  finish(() => reject(new Error("shutdown")));
1308
1932
  return;
1309
1933
  }
1310
- finish(resolve5);
1934
+ finish(resolve3);
1311
1935
  });
1312
1936
  ws.on("error", (err) => {
1313
1937
  console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
@@ -1321,26 +1945,33 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
1321
1945
  });
1322
1946
  });
1323
1947
  }
1948
+ function resolveConnectServerOption(options) {
1949
+ const fromOption = options.server?.trim();
1950
+ if (fromOption) {
1951
+ return fromOption.replace(/\/+$/, "");
1952
+ }
1953
+ const fromEnv = process.env.AI_PM_SERVER?.trim();
1954
+ if (fromEnv) {
1955
+ return fromEnv.replace(/\/+$/, "");
1956
+ }
1957
+ return void 0;
1958
+ }
1324
1959
  async function runConnect(options) {
1325
1960
  const { didUpdate } = await runUpdate({ quietIfCurrent: true });
1326
1961
  if (didUpdate) {
1327
1962
  reexecConnectAfterUpdate(options);
1328
1963
  }
1329
1964
  const cfg = await ensureLoggedConfig();
1330
- const endpoints = resolveApmEndpoints({
1331
- server: options.server,
1332
- socketServer: options.socketServer,
1333
- configBaseUrl: cfg.baseUrl,
1334
- configSocketServer: cfg.socketServer
1335
- });
1336
- cfg.baseUrl = endpoints.baseUrl;
1337
- cfg.socketServer = endpoints.socketServer;
1965
+ const serverOverride = resolveConnectServerOption(options);
1966
+ if (serverOverride) {
1967
+ cfg.baseUrl = serverOverride;
1968
+ }
1338
1969
  const clientMachineId = resolveClientMachineId(cfg);
1339
1970
  if (!clientMachineId) {
1340
1971
  console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
1341
1972
  process.exit(1);
1342
1973
  }
1343
- await acquireConnectLock();
1974
+ acquireConnectLock();
1344
1975
  process.on("exit", releaseConnectLock);
1345
1976
  let shuttingDown = false;
1346
1977
  let shutdownPromise = null;
@@ -1367,11 +1998,11 @@ async function runConnect(options) {
1367
1998
  lifecycleAbort.abort();
1368
1999
  currentConnectionAbort?.abort();
1369
2000
  shutdownAbort.abort();
1370
- releaseConnectLock();
1371
2001
  try {
1372
- await Promise.race([Promise.all(activeTasks), delay2(drainMs)]);
2002
+ await Promise.race([Promise.all(activeTasks), delay(drainMs)]);
1373
2003
  } catch {
1374
2004
  }
2005
+ releaseConnectLock();
1375
2006
  process.exit(code);
1376
2007
  };
1377
2008
  const shutdown = (code = 0) => {
@@ -1393,12 +2024,8 @@ async function runConnect(options) {
1393
2024
  lifecycleAbort.signal,
1394
2025
  currentConnectionAbort.signal
1395
2026
  ]);
1396
- const url = buildAgentWsUrl(
1397
- cfg.socketServer ?? cfg.baseUrl,
1398
- resolveApiKey(cfg)
1399
- );
1400
- const visible = url.replace(/([?&]apiKey=)[^&]+/, "$1***");
1401
- console.log(`[apm] \u8FDE\u63A5 ${visible} \u2026`);
2027
+ const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
2028
+ console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
1402
2029
  try {
1403
2030
  await connectOnce(url, connectCtx, connectionSignal, () => {
1404
2031
  reconnectDelay = RECONNECT_MIN_MS;
@@ -1430,592 +2057,18 @@ async function runConnect(options) {
1430
2057
  }
1431
2058
  }
1432
2059
 
1433
- // src/utils/home-repository.ts
1434
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1435
- import { join as join8, resolve as resolve3 } from "path";
1436
- var DEFAULT_REPOS_MANIFEST = "/home/repository.json";
1437
- function isSimpleChildDirName(name) {
1438
- if (!name || name === "." || name === "..") return false;
1439
- if (name.includes("/") || name.includes("\\")) return false;
1440
- return true;
1441
- }
1442
- function resolveReposManifestPath(explicit) {
1443
- const fromOpt = explicit?.trim() || "";
1444
- if (fromOpt) return fromOpt;
1445
- const fromEnv = process.env.APM_REPOS_MANIFEST?.trim() || "";
1446
- if (fromEnv) return fromEnv;
1447
- return null;
1448
- }
1449
- function resolveCliReposManifestPath(explicit) {
1450
- return resolveReposManifestPath(explicit) ?? DEFAULT_REPOS_MANIFEST;
1451
- }
1452
- function loadHomeRepositoryEntries(manifestPath2) {
1453
- const label = manifestPath2.trim();
1454
- if (!label) {
1455
- throw new Error("[apm] \u591A\u4ED3\u6E05\u5355\u8DEF\u5F84\u4E3A\u7A7A");
1456
- }
1457
- const path = toFsPath(label);
1458
- if (!existsSync6(path)) {
1459
- throw new Error(`[apm] \u672A\u627E\u5230\u591A\u4ED3\u6E05\u5355 ${label}`);
1460
- }
1461
- let parsed;
1462
- try {
1463
- parsed = JSON.parse(readFileSync5(path, "utf8"));
1464
- } catch (err) {
1465
- const detail = err instanceof Error ? err.message : String(err);
1466
- throw new Error(`[apm] \u89E3\u6790 ${label} \u5931\u8D25: ${detail}`);
1467
- }
1468
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1469
- throw new Error(
1470
- `[apm] ${label} \u987B\u4E3A\u5BF9\u8C61\uFF1Akey=\u4E0B\u7EA7\u76EE\u5F55\uFF0Cvalue=git \u5730\u5740`
1471
- );
1472
- }
1473
- const entries = [];
1474
- for (const [rawKey, rawVal] of Object.entries(
1475
- parsed
1476
- )) {
1477
- const pathKey = rawKey.trim();
1478
- if (!isSimpleChildDirName(pathKey)) {
1479
- throw new Error(
1480
- `[apm] ${label} \u975E\u6CD5\u76EE\u5F55 key: ${JSON.stringify(rawKey)}\uFF08\u987B\u4E3A\u5F53\u524D\u4E0B\u7EA7\u76EE\u5F55\u540D\uFF09`
1481
- );
1482
- }
1483
- if (typeof rawVal !== "string" || !rawVal.trim()) {
1484
- throw new Error(
1485
- `[apm] ${label}[${pathKey}] \u987B\u4E3A\u975E\u7A7A git \u5730\u5740\u5B57\u7B26\u4E32`
1486
- );
1487
- }
1488
- entries.push({ path: pathKey, remoteUrl: rawVal.trim() });
1489
- }
1490
- entries.sort((a, b) => a.path.localeCompare(b.path));
1491
- if (entries.length === 0) {
1492
- throw new Error(`[apm] ${label} \u4E3A\u7A7A\uFF0C\u65E0\u4ED3\u5E93\u53EF\u786E\u8BA4`);
1493
- }
1494
- return entries;
1495
- }
1496
- function resolveHomeRepositoryAbsolutePaths(workdirInput, entries) {
1497
- const workdir = resolveWorkdirPath(workdirInput);
1498
- return entries.map((entry) => resolve3(join8(workdir, entry.path)));
1499
- }
1500
-
1501
- // src/utils/git.ts
1502
- import { execFile } from "child_process";
1503
- import { dirname as dirname3, join as join9 } from "path";
1504
- import { fileURLToPath as fileURLToPath3 } from "url";
1505
- import { promisify } from "util";
1506
- var execFileAsync = promisify(execFile);
1507
- var GIT_NETWORK_TIMEOUT_MS = 9e4;
1508
- var GIT_PUSH_TIMEOUT_MS = 12e4;
1509
- var GIT_LOCAL_TIMEOUT_MS = 6e4;
1510
- var GIT_NETWORK_CMDS = /* @__PURE__ */ new Set([
1511
- "fetch",
1512
- "push",
1513
- "ls-remote",
1514
- "clone",
1515
- "pull"
1516
- ]);
1517
- function mergeGitSshCommand(existing) {
1518
- const base = existing?.trim() || "ssh";
1519
- const extras = [];
1520
- if (!/\bStrictHostKeyChecking=/.test(base)) {
1521
- extras.push("-o", "StrictHostKeyChecking=accept-new");
1522
- }
1523
- if (!/\bBatchMode=/.test(base)) {
1524
- extras.push("-o", "BatchMode=yes");
1525
- }
1526
- if (!/\bConnectTimeout=/.test(base)) {
1527
- extras.push("-o", "ConnectTimeout=20");
1528
- }
1529
- if (!/\bServerAliveInterval=/.test(base)) {
1530
- extras.push("-o", "ServerAliveInterval=10");
1531
- }
1532
- if (!/\bServerAliveCountMax=/.test(base)) {
1533
- extras.push("-o", "ServerAliveCountMax=3");
1534
- }
1535
- return extras.length ? `${base} ${extras.join(" ")}` : base;
1536
- }
1537
- function gitExecEnv() {
1538
- return {
1539
- ...process.env,
1540
- GIT_SSH_COMMAND: mergeGitSshCommand(process.env.GIT_SSH_COMMAND),
1541
- // 避免 credential helper / askpass 在无 TTY 时挂起
1542
- GIT_TERMINAL_PROMPT: "0"
1543
- };
1544
- }
1545
- function defaultGitTimeoutMs(args) {
1546
- const cmd = args[0] ?? "";
1547
- if (cmd === "push") return GIT_PUSH_TIMEOUT_MS;
1548
- if (GIT_NETWORK_CMDS.has(cmd)) return GIT_NETWORK_TIMEOUT_MS;
1549
- return GIT_LOCAL_TIMEOUT_MS;
1550
- }
1551
- async function execGit(cwd, args, quietOrOptions = false) {
1552
- const options = typeof quietOrOptions === "boolean" ? { quiet: quietOrOptions } : quietOrOptions;
1553
- const quiet = options.quiet ?? false;
1554
- const timeoutMs = options.timeoutMs === void 0 ? defaultGitTimeoutMs(args) : options.timeoutMs;
1555
- try {
1556
- const { stdout, stderr } = await execFileAsync("git", args, {
1557
- cwd,
1558
- env: gitExecEnv(),
1559
- encoding: "utf8",
1560
- maxBuffer: 10 * 1024 * 1024,
1561
- ...timeoutMs > 0 ? { timeout: timeoutMs, killSignal: "SIGTERM" } : {}
1562
- });
1563
- if (!quiet && stderr.trim()) {
1564
- process.stderr.write(stderr);
1565
- }
1566
- return stdout;
1567
- } catch (err) {
1568
- const e = err;
1569
- if (e.killed || e.signal === "SIGTERM" || e.code === "ETIMEDOUT") {
1570
- throw new Error(
1571
- `[apm] git ${args.join(" ")} \u8D85\u65F6\uFF08>${timeoutMs}ms\uFF09\uFF0C\u8BF7\u68C0\u67E5\u5230\u8FDC\u7A0B\u7684\u7F51\u7EDC/SSH\uFF1B\u53EF\u7A0D\u540E\u91CD\u8BD5`
1572
- );
1573
- }
1574
- const detail = (e.stderr ?? e.message ?? String(err)).trim();
1575
- throw new Error(
1576
- `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
1577
- );
1578
- }
1579
- }
1580
- async function ensureGitRepo(cwd) {
1581
- await execGit(cwd, ["rev-parse", "--git-dir"], true);
1582
- }
1583
- async function resolveGitRepoRoot(cwd) {
1584
- return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
1585
- }
1586
- function resolveGitScript(name) {
1587
- return join9(dirname3(fileURLToPath3(import.meta.url)), "scripts", name);
1588
- }
1589
- async function runGitScript(name, options) {
1590
- const skipCode = options.skipCode ?? 2;
1591
- const timeoutMs = options.timeoutMs ?? GIT_NETWORK_TIMEOUT_MS;
1592
- const script = resolveGitScript(name);
1593
- const args = [script, ...options.args ?? []];
1594
- try {
1595
- const { stdout, stderr } = await execFileAsync("bash", args, {
1596
- cwd: options.cwd,
1597
- env: gitExecEnv(),
1598
- encoding: "utf8",
1599
- maxBuffer: 10 * 1024 * 1024,
1600
- timeout: timeoutMs,
1601
- killSignal: "SIGTERM"
1602
- });
1603
- if (stdout.trim()) process.stdout.write(stdout);
1604
- if (stderr.trim()) process.stderr.write(stderr);
1605
- return true;
1606
- } catch (err) {
1607
- const e = err;
1608
- if (e.stdout?.trim()) process.stdout.write(e.stdout);
1609
- if (e.stderr?.trim()) process.stderr.write(e.stderr);
1610
- if (e.code === skipCode) {
1611
- return false;
1612
- }
1613
- if (e.killed || e.signal === "SIGTERM" || e.code === "ETIMEDOUT") {
1614
- throw new Error(
1615
- `[apm] ${name} \u8D85\u65F6\uFF08>${timeoutMs}ms\uFF09\uFF0C\u8BF7\u68C0\u67E5\u5230\u8FDC\u7A0B\u7684\u7F51\u7EDC/SSH\uFF1B\u53EF\u7A0D\u540E\u91CD\u8BD5`
1616
- );
1617
- }
1618
- const detail = (e.stderr ?? e.message ?? String(err)).trim();
1619
- throw new Error(`[apm] ${name} \u5931\u8D25${detail ? `: ${detail}` : ""}`);
1620
- }
1621
- }
1622
- async function commitWorkingTreeIfDirty(cwd, message) {
1623
- await ensureGitRepo(cwd);
1624
- return runGitScript("commit_if_dirty.sh", {
1625
- cwd,
1626
- args: message?.trim() ? [message.trim()] : [],
1627
- timeoutMs: GIT_LOCAL_TIMEOUT_MS
1628
- });
1629
- }
1630
- async function ensureFeatureBranch(cwd, branch, baselineBranch) {
1631
- await runGitScript("ensure_feature_branch.sh", {
1632
- cwd,
1633
- args: [branch, baselineBranch],
1634
- timeoutMs: GIT_PUSH_TIMEOUT_MS
1635
- });
1636
- }
1637
- async function pushCurrentBranch(cwd) {
1638
- return runGitScript("push_code.sh", {
1639
- cwd,
1640
- timeoutMs: GIT_PUSH_TIMEOUT_MS
1641
- });
1642
- }
1643
-
1644
- // src/utils/workspace-repos.ts
1645
- import {
1646
- existsSync as existsSync7,
1647
- mkdirSync as mkdirSync5,
1648
- readFileSync as readFileSync6,
1649
- readdirSync as readdirSync3,
1650
- statSync as statSync3,
1651
- writeFileSync as writeFileSync4
1652
- } from "fs";
1653
- import { basename, dirname as dirname4, join as join10, relative, resolve as resolve4 } from "path";
1654
-
1655
- // src/utils/git-remote.ts
1656
- import { execFile as execFile2 } from "child_process";
1657
- import { promisify as promisify2 } from "util";
1658
- var execFileAsync2 = promisify2(execFile2);
1659
-
1660
- // src/utils/workspace-repos.ts
1661
- var WORKSPACE_REPOS_MANIFEST = "workspace-repos.json";
1662
- var WORKSPACE_REPOS_VERSION = 1;
1663
- function manifestPath(workdir) {
1664
- return join10(workspaceApmDir(workdir), WORKSPACE_REPOS_MANIFEST);
1665
- }
1666
- function absoluteRepoPath(workdir, entry) {
1667
- return resolve4(workdir, entry.path);
1668
- }
1669
- function toRelativeRepoPath(workdir, repoAbs) {
1670
- return relative(workdir, repoAbs).replace(/\\/g, "/");
1671
- }
1672
- function normalizeRepoEntry(raw) {
1673
- if (!raw || typeof raw !== "object") return null;
1674
- const o = raw;
1675
- if (typeof o.path !== "string" || !o.path.trim()) return null;
1676
- const path = o.path.trim().replace(/\\/g, "/");
1677
- if (path === "." || path.includes("/")) return null;
1678
- const entry = { path };
1679
- if (typeof o.remoteUrl === "string" && o.remoteUrl.trim()) {
1680
- entry.remoteUrl = o.remoteUrl.trim();
1681
- } else if (o.remoteUrl === null) {
1682
- entry.remoteUrl = null;
1683
- }
1684
- return entry;
1685
- }
1686
- function readManifest(workdir) {
1687
- const path = toFsPath(manifestPath(workdir));
1688
- if (!existsSync7(path)) {
1689
- return null;
1690
- }
1691
- try {
1692
- const raw = JSON.parse(
1693
- readFileSync6(path, "utf8")
1694
- );
1695
- if (raw?.version !== WORKSPACE_REPOS_VERSION) {
1696
- return null;
1697
- }
1698
- if (raw.kind !== "multi") {
1699
- return null;
1700
- }
1701
- if (!Array.isArray(raw.repos) || raw.repos.length === 0) {
1702
- return null;
1703
- }
1704
- if (typeof raw.workdir !== "string" || !raw.workdir.trim()) {
1705
- return null;
1706
- }
1707
- const repos = raw.repos.map((item) => normalizeRepoEntry(item)).filter((item) => item != null);
1708
- if (repos.length === 0) {
1709
- return null;
1710
- }
1711
- return {
1712
- version: WORKSPACE_REPOS_VERSION,
1713
- kind: "multi",
1714
- workdir: raw.workdir,
1715
- repos,
1716
- scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt.trim() ? raw.scannedAt : (/* @__PURE__ */ new Date()).toISOString()
1717
- };
1718
- } catch {
1719
- return null;
1720
- }
1721
- }
1722
- function loadWorkspaceReposCache(workdirInput) {
1723
- const workdir = resolveWorkdirPath(workdirInput);
1724
- const cached = readManifest(workdir);
1725
- if (!cached) {
1726
- throw new Error(
1727
- `[apm] \u672A\u627E\u5230\u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 .apm/${WORKSPACE_REPOS_MANIFEST}\uFF0C\u8BF7\u5148\u6267\u884C\u4ED3\u5E93\u68C0\u6D4B`
1728
- );
1729
- }
1730
- if (normalizeWorkdirPath(cached.workdir) !== normalizeWorkdirPath(workdir)) {
1731
- throw new Error(
1732
- `[apm] \u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 workdir \u4E0D\u5339\u914D\uFF1A\u7F13\u5B58=${cached.workdir} \u5F53\u524D=${workdir}\uFF0C\u8BF7\u5F3A\u5236\u91CD\u65B0\u626B\u63CF`
1733
- );
1734
- }
1735
- if (!Array.isArray(cached.repos) || cached.repos.length === 0) {
1736
- throw new Error(`[apm] \u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58\u65E0\u6548\uFF08repos \u4E3A\u7A7A\uFF09\uFF0C\u8BF7\u5F3A\u5236\u91CD\u65B0\u626B\u63CF`);
1737
- }
1738
- console.log(
1739
- `[apm] \u8BFB\u53D6\u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 repos=${cached.repos.map((r) => r.path).join(", ")}`
1740
- );
1741
- return {
1742
- ...cached,
1743
- workdir
1744
- };
1745
- }
1746
- function resolveWorkspaceRepoAbsolutePaths(manifest) {
1747
- return manifest.repos.map(
1748
- (entry) => absoluteRepoPath(manifest.workdir, entry)
1749
- );
1750
- }
1751
- function formatRepoLabel(workdir, repoAbs) {
1752
- const rel = toRelativeRepoPath(resolveWorkdirPath(workdir), repoAbs);
1753
- return rel || basename(repoAbs) || repoAbs;
1754
- }
1755
-
1756
- // src/commands/branch.ts
1757
- var TASK_BRANCH_PREFIX = "feat/task-";
1758
- async function syncTaskRepos(workdir, options = {}) {
1759
- const opts = typeof options === "string" ? { message: options } : options;
1760
- const workdirPath = resolveWorkdirPath(workdir);
1761
- const repoRoots = resolveTaskBranchRepoRoots(
1762
- workdirPath,
1763
- opts.reposManifest
1764
- );
1765
- let committed = 0;
1766
- let commitSkipped = 0;
1767
- let pushed = 0;
1768
- let pushSkipped = 0;
1769
- const failed = [];
1770
- const message = opts.message;
1771
- for (const gitRoot of repoRoots) {
1772
- const label = formatRepoLabel(workdirPath, gitRoot);
1773
- try {
1774
- const didCommit = await commitWorkingTreeIfDirty(gitRoot, message);
1775
- if (didCommit) {
1776
- committed += 1;
1777
- console.log(`[apm] \u5DF2\u63D0\u4EA4 @ ${label}`);
1778
- } else {
1779
- commitSkipped += 1;
1780
- }
1781
- } catch (err) {
1782
- const detail = err instanceof Error ? err.message : String(err);
1783
- console.error(`[apm] \u4ED3\u5E93 ${label} \u63D0\u4EA4\u5931\u8D25\uFF08\u7EE7\u7EED\u5176\u5B83\u4ED3\uFF09: ${detail}`);
1784
- failed.push(label);
1785
- continue;
1786
- }
1787
- try {
1788
- const didPush = await pushCurrentBranch(gitRoot);
1789
- if (didPush) {
1790
- pushed += 1;
1791
- console.log(`[apm] \u5DF2 push @ ${label}`);
1792
- } else {
1793
- pushSkipped += 1;
1794
- }
1795
- } catch (err) {
1796
- const detail = err instanceof Error ? err.message : String(err);
1797
- console.error(`[apm] \u4ED3\u5E93 ${label} push \u5931\u8D25\uFF08\u7EE7\u7EED\u5176\u5B83\u4ED3\uFF09: ${detail}`);
1798
- failed.push(label);
1799
- }
1800
- }
1801
- return {
1802
- committed,
1803
- commitSkipped,
1804
- pushed,
1805
- pushSkipped,
1806
- failed,
1807
- repos: repoRoots
1808
- };
1809
- }
1810
- function branchNameForTask(taskId) {
1811
- const id = taskId.trim();
1812
- if (!id) {
1813
- throw new Error("[apm] \u4EFB\u52A1 ID \u4E0D\u80FD\u4E3A\u7A7A");
1814
- }
1815
- if (/[\s/\\]/.test(id)) {
1816
- throw new Error(
1817
- "[apm] \u4EFB\u52A1 ID \u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u6216\u8DEF\u5F84\u5206\u9694\u7B26\uFF0C\u8BF7\u4F7F\u7528\u5B57\u6BCD\u3001\u6570\u5B57\u3001._- \u7B49"
1818
- );
1819
- }
1820
- return `${TASK_BRANCH_PREFIX}${id}`;
1821
- }
1822
- async function ensureTaskFeatureBranch(branch, projectId, options) {
1823
- const cwd = options.cwd ?? process.cwd();
1824
- const gitRoot = await resolveGitRepoRoot(cwd);
1825
- await ensureFeatureBranch(gitRoot, branch, projectId);
1826
- return branch;
1827
- }
1828
- function resolveTaskBranchRepoRoots(workdirPath, reposManifest) {
1829
- const manifestPath2 = resolveReposManifestPath(reposManifest);
1830
- if (manifestPath2) {
1831
- const entries = loadHomeRepositoryEntries(manifestPath2);
1832
- const roots2 = resolveHomeRepositoryAbsolutePaths(workdirPath, entries);
1833
- console.log(
1834
- `[apm] \u8BFB\u53D6\u5916\u90E8\u591A\u4ED3\u6E05\u5355 ${manifestPath2} repos=${entries.map((e) => `${e.path}\u2192${e.remoteUrl}`).join(", ")}`
1835
- );
1836
- return roots2;
1837
- }
1838
- const workspace = loadWorkspaceReposCache(workdirPath);
1839
- const roots = resolveWorkspaceRepoAbsolutePaths(workspace);
1840
- console.log(
1841
- `[apm] \u8BFB\u53D6\u5DE5\u4F5C\u533A\u4ED3\u5E93\u6E05\u5355 repos=${workspace.repos.map(
1842
- (r) => r.remoteUrl ? `${r.path}\u2192${r.remoteUrl}` : r.path
1843
- ).join(", ")}`
1844
- );
1845
- return roots;
1846
- }
1847
- async function runTaskBranch(taskId, projectId, options = {}) {
1848
- const trimmedTaskId = taskId.trim();
1849
- if (!trimmedTaskId) {
1850
- throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
1851
- }
1852
- const trimmedProjectId = projectId.trim();
1853
- if (!trimmedProjectId) {
1854
- throw new Error("[apm] projectId\uFF08\u9879\u76EE ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
1855
- }
1856
- if (/[\s\\]/.test(trimmedProjectId) || trimmedProjectId.includes("..")) {
1857
- throw new Error("[apm] \u9879\u76EE ID \u975E\u6CD5");
1858
- }
1859
- const cwd = options.cwd ?? process.cwd();
1860
- const workdirPath = resolveWorkdirPath(cwd);
1861
- const repoRoots = resolveTaskBranchRepoRoots(
1862
- workdirPath,
1863
- options.reposManifest
1864
- );
1865
- if (repoRoots.length === 0) {
1866
- throw new Error("[apm] \u5DE5\u4F5C\u533A\u4ED3\u5E93\u6E05\u5355\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u786E\u8BA4\u7279\u6027\u5206\u652F");
1867
- }
1868
- const branch = branchNameForTask(trimmedTaskId);
1869
- const repoLabels = repoRoots.map(
1870
- (gitRoot) => formatRepoLabel(workdirPath, gitRoot)
1871
- );
1872
- console.log(
1873
- `[apm] \u591A\u4ED3\u786E\u8BA4\u7279\u6027\u5206\u652F ${branch} projectId=${trimmedProjectId} repos=${repoLabels.join(",")}`
1874
- );
1875
- for (const gitRoot of repoRoots) {
1876
- const label = formatRepoLabel(workdirPath, gitRoot);
1877
- console.log(
1878
- `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (projectId=${trimmedProjectId})`
1879
- );
1880
- try {
1881
- await ensureTaskFeatureBranch(branch, trimmedProjectId, {
1882
- ...options,
1883
- cwd: gitRoot
1884
- });
1885
- } catch (err) {
1886
- const detail = err instanceof Error ? err.message : String(err);
1887
- throw new Error(
1888
- `[apm] \u4ED3\u5E93 ${label} \u786E\u8BA4\u7279\u6027\u5206\u652F\u5931\u8D25\uFF08\u591A\u4ED3\u4EFB\u4E00\u5931\u8D25\u5219\u6574\u6B21\u5931\u8D25\uFF09: ${detail}`
1889
- );
1890
- }
1891
- }
1892
- return {
1893
- branch,
1894
- projectId: trimmedProjectId,
1895
- kind: "multi",
1896
- repos: repoRoots
1897
- };
1898
- }
1899
-
1900
- // src/commands/ensure-task-branch.ts
1901
- async function runEnsureTaskBranchCli(options = {}) {
1902
- const workdir = resolveWorkdirPath(options.cwd ?? process.cwd());
1903
- const taskId = options.taskId?.trim() || "";
1904
- const projectId = options.projectId?.trim() || "";
1905
- if (!taskId) {
1906
- throw new Error("[apm] \u7F3A\u5C11 taskId\uFF1A\u8BF7\u4F20\u5165\u53C2\u6570");
1907
- }
1908
- if (!projectId) {
1909
- throw new Error("[apm] \u7F3A\u5C11 projectId\uFF1A\u8BF7\u4F20\u5165\u53C2\u6570");
1910
- }
1911
- const result = await runTaskBranch(taskId, projectId, {
1912
- cwd: workdir,
1913
- reposManifest: resolveCliReposManifestPath(options.reposManifest)
1914
- });
1915
- console.log(
1916
- `[apm] \u7279\u6027\u5206\u652F\u5DF2\u5C31\u7EEA\uFF08\u5168\u90E8 ${result.repos.length} \u4E2A\u4ED3\uFF09 branch=${result.branch} projectId=${result.projectId}`
1917
- );
1918
- }
1919
-
1920
- // src/utils/user-prompt-cache.ts
1921
- import { createHash } from "crypto";
1922
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
1923
- import { join as join11 } from "path";
1924
- import { tmpdir } from "os";
1925
- var CACHE_ROOT = join11(tmpdir(), "apm-hooks");
1926
- var PROMPT_FILE = "user-prompt.txt";
1927
- var COMMIT_SUBJECT_MAX_LEN = 100;
1928
- function workdirKey(workdir) {
1929
- return createHash("sha256").update(resolveWorkdirPath(workdir)).digest("hex").slice(0, 16);
1930
- }
1931
- function userPromptCachePath(workdirInput) {
1932
- const workdir = resolveWorkdirPath(workdirInput);
1933
- return join11(CACHE_ROOT, workdirKey(workdir), PROMPT_FILE);
1934
- }
1935
- function formatUserPromptAsCommitMessage(prompt) {
1936
- const oneLine = prompt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((l) => l.trim()).find((l) => l.length > 0);
1937
- if (!oneLine) return "";
1938
- const cleaned = oneLine.replace(/\s+/g, " ").trim();
1939
- if (cleaned.length <= COMMIT_SUBJECT_MAX_LEN) return cleaned;
1940
- return `${cleaned.slice(0, COMMIT_SUBJECT_MAX_LEN - 1)}\u2026`;
1941
- }
1942
- function loadUserPromptCommitMessage(workdirInput) {
1943
- const path = toFsPath(userPromptCachePath(workdirInput));
1944
- if (!existsSync8(path)) return null;
1945
- try {
1946
- const raw = readFileSync7(path, "utf8");
1947
- const msg = formatUserPromptAsCommitMessage(raw);
1948
- return msg || null;
1949
- } catch {
1950
- return null;
1951
- }
1952
- }
1953
-
1954
- // src/commands/commit-task-repos.ts
1955
- function resolveCommitTaskMessage(workdir, explicit) {
1956
- const fromOpt = explicit?.trim() ?? "";
1957
- if (fromOpt) return fromOpt;
1958
- const fromPrompt = loadUserPromptCommitMessage(workdir);
1959
- if (fromPrompt) return fromPrompt;
1960
- const taskId = process.env.APM_TASK_ID?.trim() || "";
1961
- const projectId = process.env.APM_PROJECT_ID?.trim() || "";
1962
- if (taskId) {
1963
- let branch = "";
1964
- try {
1965
- branch = branchNameForTask(taskId);
1966
- } catch {
1967
- branch = `feat/task-${taskId}`;
1968
- }
1969
- if (projectId) {
1970
- return `chore(apm): sync ${branch} (project ${projectId})`;
1971
- }
1972
- return `chore(apm): sync ${branch}`;
1973
- }
1974
- if (projectId) {
1975
- return `chore(apm): sync workspace (project ${projectId})`;
1976
- }
1977
- return void 0;
1978
- }
1979
- async function runCommitTaskReposCli(options = {}) {
1980
- const workdir = resolveWorkdirPath(options.cwd ?? process.cwd());
1981
- const message = resolveCommitTaskMessage(workdir, options.message);
1982
- if (message) {
1983
- console.log(`[apm] commit message: ${message}`);
1984
- }
1985
- const result = await syncTaskRepos(workdir, {
1986
- message,
1987
- reposManifest: resolveCliReposManifestPath(options.reposManifest)
1988
- });
1989
- console.log(
1990
- `[apm] \u591A\u4ED3\u6536\u5C3E\u5B8C\u6210 committed=${result.committed} commitSkipped=${result.commitSkipped} pushed=${result.pushed} pushSkipped=${result.pushSkipped} failed=${result.failed.length} repos=${result.repos.length}`
1991
- );
1992
- if (result.failed.length > 0) {
1993
- throw new Error(
1994
- `[apm] \u4EE5\u4E0B\u4ED3\u5E93 commit/push \u5931\u8D25: ${result.failed.join(", ")}`
1995
- );
1996
- }
1997
- }
1998
-
1999
2060
  // src/index.ts
2000
2061
  function buildProgram() {
2001
2062
  const program = new Command();
2002
2063
  program.name("apm").description(
2003
2064
  `\u6BD4\u90BB\u661F\u56FE\u547D\u4EE4\u884C\uFF08WebIDE \u5DE5\u4F5C\u533A\u4E0E\u7814\u53D1\u81EA\u52A8\u5316\uFF09\u3002
2004
- \u5730\u5740\u53EF\u7528 --server / --socket-server\uFF0C\u6216\u73AF\u5883\u53D8\u91CF APM_SERVER / APM_SOCKET_SERVER\uFF1B\u672A\u8BBE\u7F6E\u65F6\u9ED8\u8BA4 ${DEFAULT_BASE_URL}\u3002`
2065
+ \u672A\u4F20 --server \u65F6\u4F18\u5148\u4F7F\u7528\u73AF\u5883\u53D8\u91CF AI_PM_SERVER\uFF0C\u5426\u5219\u9ED8\u8BA4 ${DEFAULT_BASE_URL}\u3002`
2005
2066
  ).version(readCliVersion(), "-V, --version", "\u663E\u793A\u7248\u672C\u53F7").helpOption("-h, --help", "\u663E\u793A\u5E2E\u52A9").showHelpAfterError(true);
2006
2067
  program.command("login").description(
2007
2068
  "\u9A8C\u8BC1 API Key \u5E76\u5199\u5165 ~/.config/apm/config.json\uFF08GET /api/v1/cli/me\uFF09"
2008
- ).option("--api-key <key>", "\u8FDC\u7A0B\u4E3B\u673A API Key\uFF08cm_ \u5F00\u5934\uFF09").option(
2009
- "--server <url>",
2010
- "API \u6839\u5730\u5740\uFF1B\u82E5\u53EA\u4F20\u6B64\u9879\u4E14\u5E26\u7AEF\u53E3\uFF0C\u5219\u7AEF\u53E3\u89C6\u4E3A Socket \u7AEF\u53E3\uFF08API \u53BB\u6389\u7AEF\u53E3\uFF09"
2011
- ).option(
2012
- "--socket-server <url>",
2013
- "Agent WebSocket \u6839\u5730\u5740\uFF08\u53EF\u4E0E API \u4E0D\u540C\uFF09\uFF0C\u4F8B\u5982 wss://ws.example.com"
2014
- ).action(
2015
- async (opts) => {
2016
- await runLogin(opts);
2017
- }
2018
- );
2069
+ ).option("--api-key <key>", "\u8FDC\u7A0B\u4E3B\u673A API Key\uFF08cm_ \u5F00\u5934\uFF09").option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u4F8B\u5982 http://127.0.0.1:3000").action(async (opts) => {
2070
+ await runLogin(opts);
2071
+ });
2019
2072
  program.command("init").description("\u5728\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6309 WebIDE \u5DE5\u4F5C\u533A\u521D\u59CB\u5316\uFF1A\u4ECE\u5E73\u53F0\u5199\u5165\u6307\u5357\u4E0E\u89C4\u5219").action(async () => {
2020
2073
  await runInit();
2021
2074
  });
@@ -2032,52 +2085,15 @@ function buildProgram() {
2032
2085
  program.command("update-skills").description("\u4ECE\u5E73\u53F0\u540C\u6B65 .apm/ \u4E0B\u7684 WebIDE \u6307\u5357\u4E0E\u89C4\u5219").action(async () => {
2033
2086
  await runUpdateSkills();
2034
2087
  });
2035
- program.command("ensure-task-branch").description(
2036
- "\u6309\u591A\u4ED3\u6E05\u5355\u786E\u4FDD\u5404\u4ED3\u4F4D\u4E8E feat/task-<\u4EFB\u52A1ID>\uFF08\u4E0D\u5B58\u5728\u5219\u57FA\u4E8E\u9879\u76EE\u5206\u652F\u4ECE\u8FDC\u7A0B\u521B\u5EFA\uFF09\u3002ACP \u4E0D\u4F1A\u8DD1 Cursor hooks\uFF0C\u5F00\u59CB\u4EFB\u52A1\u65F6\u7531\u670D\u52A1\u7AEF\u6216\u624B\u52A8\u8C03\u7528\u672C\u547D\u4EE4"
2037
- ).argument("<taskId>", "\u4EFB\u52A1 ID\uFF08\u521B\u5EFA/\u5207\u6362\u5206\u652F\uFF09").argument(
2038
- "<projectId>",
2039
- "\u9879\u76EE ID\uFF08\u4EC5\u521B\u5EFA\u65F6\u7528\uFF1B\u89C4\u8303\u4E0E\u9879\u76EE\u5206\u652F\u540C\u540D\uFF09"
2040
- ).option("--cwd <dir>", "\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option(
2041
- "--repos <file>",
2042
- "\u591A\u4ED3\u6E05\u5355 JSON\uFF08key=\u4E0B\u7EA7\u76EE\u5F55\uFF0Cvalue=git \u5730\u5740\uFF09\uFF1B\u7701\u7565\u65F6\u8BFB APM_REPOS_MANIFEST\uFF0C\u518D\u9ED8\u8BA4 /home/repository.json"
2043
- ).action(
2044
- async (taskId, projectId, opts) => {
2045
- await runEnsureTaskBranchCli({
2046
- taskId,
2047
- projectId,
2048
- cwd: opts.cwd,
2049
- reposManifest: opts.repos
2050
- });
2051
- }
2052
- );
2053
- program.command("commit-task-repos").description(
2054
- "\u6267\u884C\u7ED3\u675F\u6536\u5C3E\uFF1A\u591A\u4ED3\u6E05\u5355\u5404\u4ED3\u6709\u53D8\u66F4\u624D commit\uFF0C\u518D push\uFF08\u65E0\u53D8\u66F4\u4E0D\u7A7A\u63D0\u4EA4\uFF09"
2055
- ).option("--cwd <dir>", "\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option(
2056
- "--repos <file>",
2057
- "\u591A\u4ED3\u6E05\u5355 JSON\uFF1B\u7701\u7565\u65F6\u8BFB APM_REPOS_MANIFEST\uFF0C\u518D\u9ED8\u8BA4 /home/repository.json"
2058
- ).option(
2059
- "-m, --message <msg>",
2060
- "commit message\uFF1B\u7701\u7565\u5219\u4F18\u5148\u7528\u6237\u95EE\u9898\u7F13\u5B58\uFF0C\u5426\u5219\u6309 APM_TASK_ID / APM_PROJECT_ID \u751F\u6210"
2061
- ).action(async (opts) => {
2062
- await runCommitTaskReposCli({
2063
- cwd: opts.cwd,
2064
- reposManifest: opts.repos,
2065
- message: opts.message
2066
- });
2088
+ program.command("sync-project-documents").description(
2089
+ "\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/project/<\u9879\u76EEID>/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
2090
+ ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/project/<\u9879\u76EEID>/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/project/<\u9879\u76EEID>/").action(async (opts) => {
2091
+ await runSyncProjectDocuments(opts);
2067
2092
  });
2068
2093
  program.command("connect").description(
2069
2094
  "\u8FDE\u63A5\u524D\u68C0\u6D4B\u5E76\u66F4\u65B0\u5230\u5F53\u524D\u5927\u7248\u672C\u6700\u65B0 CLI\uFF0C\u518D\u8FDE\u63A5\u5E73\u53F0 WebSocket\uFF08/ws/agent\uFF09"
2070
- ).option(
2071
- "--server <url>",
2072
- "API \u6839\u5730\u5740\uFF1B\u82E5\u53EA\u4F20\u6B64\u9879\u4E14\u5E26\u7AEF\u53E3\uFF0C\u5219\u7AEF\u53E3\u89C6\u4E3A Socket \u7AEF\u53E3\uFF08\u8986\u76D6 config / APM_SERVER\uFF09"
2073
- ).option(
2074
- "--socket-server <url>",
2075
- "Agent WebSocket \u6839\u5730\u5740\uFF08\u8986\u76D6 config / APM_SOCKET_SERVER\uFF09"
2076
- ).action(async (opts) => {
2077
- await runConnect({
2078
- server: opts.server,
2079
- socketServer: opts.socketServer
2080
- });
2095
+ ).option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u8986\u76D6 config \u4E2D\u7684 baseUrl").action(async (opts) => {
2096
+ await runConnect({ server: opts.server });
2081
2097
  });
2082
2098
  return program;
2083
2099
  }