ai-project-manage-cli 8.1.20 → 8.1.22
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/README.md +18 -6
- package/dist/index.js +769 -816
- package/dist/scripts/branch_has_changes.sh +53 -0
- package/dist/scripts/commit_if_dirty.sh +21 -0
- package/dist/scripts/ensure_baseline.sh +15 -0
- package/dist/scripts/ensure_feature_branch.sh +55 -0
- package/dist/scripts/push_code.sh +21 -0
- package/dist/worker-script.js +1507 -2711
- package/package.json +2 -2
- package/template/README.md +13 -0
- package/template/hooks/after-agent-commit-repos.cjs +112 -0
- package/template/hooks/before-prompt-prepare.cjs +206 -0
- package/template/hooks.json +18 -0
- package/template/mcp.json +18 -0
- package/template/skills/webide-run-retro/SKILL.md +0 -103
- package/template/skills/webide-run-retro/attribution.md +0 -26
- package/template/skills/webide-run-retro/scripts/inspect-run.mjs +0 -335
package/dist/index.js
CHANGED
|
@@ -16,6 +16,15 @@ 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
|
+
}
|
|
19
28
|
function tryReadApmConfig() {
|
|
20
29
|
try {
|
|
21
30
|
const raw = readFileSync(APM_CONFIG_PATH, "utf8");
|
|
@@ -31,10 +40,12 @@ function tryReadApmConfig() {
|
|
|
31
40
|
if (!apiKey) return null;
|
|
32
41
|
const cfg = v;
|
|
33
42
|
const clientMachineId = resolveClientMachineId(cfg);
|
|
43
|
+
const socketServer = readConfigSocketServer(cfg);
|
|
34
44
|
return {
|
|
35
45
|
baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
|
|
36
46
|
apiKey,
|
|
37
|
-
...clientMachineId ? { clientMachineId } : {}
|
|
47
|
+
...clientMachineId ? { clientMachineId } : {},
|
|
48
|
+
...socketServer ? { socketServer } : {}
|
|
38
49
|
};
|
|
39
50
|
} catch {
|
|
40
51
|
return null;
|
|
@@ -47,11 +58,80 @@ function defaultApmConfig() {
|
|
|
47
58
|
apiKey: ""
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
|
-
function
|
|
51
|
-
const u =
|
|
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;
|
|
52
80
|
if (u.startsWith("https://")) return "wss://" + u.slice("https://".length);
|
|
53
81
|
if (u.startsWith("http://")) return "ws://" + u.slice("http://".length);
|
|
54
|
-
throw new Error("
|
|
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);
|
|
55
135
|
}
|
|
56
136
|
async function ensureApmConfig() {
|
|
57
137
|
const existing = await tryReadApmConfig();
|
|
@@ -63,20 +143,25 @@ async function ensureApmConfig() {
|
|
|
63
143
|
async function writeApmConfig(cfg) {
|
|
64
144
|
const clientMachineId = resolveClientMachineId(cfg);
|
|
65
145
|
const apiKey = resolveApiKey(cfg);
|
|
146
|
+
const socketServer = stripTrailingSlash(
|
|
147
|
+
cfg.socketServer?.trim() || cfg.wsUrl?.trim() || ""
|
|
148
|
+
);
|
|
66
149
|
const normalized = {
|
|
67
150
|
baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
|
|
68
151
|
apiKey,
|
|
69
|
-
...clientMachineId ? { clientMachineId } : {}
|
|
152
|
+
...clientMachineId ? { clientMachineId } : {},
|
|
153
|
+
...socketServer ? { socketServer } : {}
|
|
70
154
|
};
|
|
71
155
|
mkdirSync(APM_CONFIG_DIR, { recursive: true });
|
|
72
156
|
writeFileSync(APM_CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", {
|
|
73
157
|
mode: 384
|
|
74
158
|
});
|
|
75
159
|
}
|
|
76
|
-
function buildAgentWsUrl(
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
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();
|
|
80
165
|
}
|
|
81
166
|
|
|
82
167
|
// src/commands/sync-webide-agent.ts
|
|
@@ -204,38 +289,10 @@ var requestConfig = {
|
|
|
204
289
|
method: "POST",
|
|
205
290
|
path: "/cli/webide/pull-requests/merge"
|
|
206
291
|
}),
|
|
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
|
-
}),
|
|
223
292
|
getApmLogStorage: defineEndpoint({
|
|
224
293
|
method: "GET",
|
|
225
294
|
path: "/cli/apm-log-storage"
|
|
226
295
|
}),
|
|
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
|
-
}),
|
|
239
296
|
cursorStoreUpsertAgent: defineEndpoint({
|
|
240
297
|
method: "PUT",
|
|
241
298
|
path: "/cli/cursor-store/agents"
|
|
@@ -350,9 +407,6 @@ async function ensureLoggedConfig() {
|
|
|
350
407
|
}
|
|
351
408
|
return cfg;
|
|
352
409
|
}
|
|
353
|
-
async function ensureDirExists(dir) {
|
|
354
|
-
mkdirSync2(dir, { recursive: true });
|
|
355
|
-
}
|
|
356
410
|
async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
|
|
357
411
|
const dir = workspaceApmDir(cwd);
|
|
358
412
|
const fsDir = toFsPath(dir);
|
|
@@ -453,15 +507,20 @@ async function runInit() {
|
|
|
453
507
|
// src/commands/login.ts
|
|
454
508
|
import { ApiError } from "listpage-http";
|
|
455
509
|
async function runLogin(opts) {
|
|
456
|
-
const
|
|
510
|
+
const endpoints = resolveApmEndpoints({
|
|
511
|
+
server: opts.server,
|
|
512
|
+
socketServer: opts.socketServer
|
|
513
|
+
});
|
|
457
514
|
const apiKey = (opts.apiKey?.trim() || process.env.APM_API_KEY?.trim() || "").replace(/^Bearer\s+/i, "");
|
|
458
515
|
if (!apiKey.startsWith("cm_")) {
|
|
459
516
|
console.error("[apm] \u8BF7\u63D0\u4F9B\u6709\u6548\u7684 API Key\uFF08cm_ \u5F00\u5934\uFF09");
|
|
460
|
-
console.error(
|
|
517
|
+
console.error(
|
|
518
|
+
"[apm] \u7528\u6CD5: apm login --api-key cm_xxx [--server URL] [--socket-server URL]"
|
|
519
|
+
);
|
|
461
520
|
process.exit(1);
|
|
462
521
|
}
|
|
463
522
|
const api = createApmApiClient({
|
|
464
|
-
baseUrl,
|
|
523
|
+
baseUrl: endpoints.baseUrl,
|
|
465
524
|
clientMachineId: "",
|
|
466
525
|
apiKey
|
|
467
526
|
});
|
|
@@ -486,7 +545,8 @@ async function runLogin(opts) {
|
|
|
486
545
|
process.exit(1);
|
|
487
546
|
}
|
|
488
547
|
const cfg = {
|
|
489
|
-
baseUrl,
|
|
548
|
+
baseUrl: endpoints.baseUrl,
|
|
549
|
+
socketServer: endpoints.socketServer,
|
|
490
550
|
clientMachineId,
|
|
491
551
|
apiKey
|
|
492
552
|
};
|
|
@@ -497,7 +557,8 @@ async function runLogin(opts) {
|
|
|
497
557
|
{
|
|
498
558
|
clientMachineId: cfg.clientMachineId,
|
|
499
559
|
clientName: data.clientName,
|
|
500
|
-
baseUrl: cfg.baseUrl
|
|
560
|
+
baseUrl: cfg.baseUrl,
|
|
561
|
+
socketServer: cfg.socketServer
|
|
501
562
|
},
|
|
502
563
|
null,
|
|
503
564
|
2
|
|
@@ -712,6 +773,10 @@ function reexecConnectAfterUpdate(options) {
|
|
|
712
773
|
if (server) {
|
|
713
774
|
args.push("--server", server);
|
|
714
775
|
}
|
|
776
|
+
const socketServer = options.socketServer?.trim();
|
|
777
|
+
if (socketServer) {
|
|
778
|
+
args.push("--socket-server", socketServer);
|
|
779
|
+
}
|
|
715
780
|
const result = spawnSync(process.execPath, args, {
|
|
716
781
|
stdio: "inherit",
|
|
717
782
|
env: { ...process.env, [APM_SKIP_SELF_UPDATE_ENV]: "1" }
|
|
@@ -754,363 +819,11 @@ async function runUpdateSkills() {
|
|
|
754
819
|
await syncWorkspaceSkills(resolveWorkdirPath());
|
|
755
820
|
}
|
|
756
821
|
|
|
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
|
-
|
|
1103
822
|
// src/commands/connect.ts
|
|
1104
823
|
import WebSocket from "ws";
|
|
1105
|
-
import { setTimeout as
|
|
824
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
1106
825
|
|
|
1107
826
|
// 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
|
-
}
|
|
1114
827
|
function nonEmptyString(v) {
|
|
1115
828
|
return typeof v === "string" && v.trim().length > 0;
|
|
1116
829
|
}
|
|
@@ -1121,18 +834,6 @@ function parseAgentWsMessage(raw) {
|
|
|
1121
834
|
return null;
|
|
1122
835
|
}
|
|
1123
836
|
}
|
|
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
|
-
}
|
|
1136
837
|
function validateWebIdeMessagePush(o) {
|
|
1137
838
|
if (o.type !== "webide-message") {
|
|
1138
839
|
return { ok: false, reason: "\u671F\u671B webide-message" };
|
|
@@ -1211,143 +912,21 @@ function validateSteer(o) {
|
|
|
1211
912
|
}
|
|
1212
913
|
};
|
|
1213
914
|
}
|
|
1214
|
-
function
|
|
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) {
|
|
915
|
+
function validateAgentWsMessage(value, _kind = "outbound") {
|
|
1325
916
|
if (typeof value !== "object" || value === null) {
|
|
1326
917
|
return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
|
|
1327
918
|
}
|
|
1328
919
|
const o = value;
|
|
1329
920
|
const type = o.type;
|
|
1330
|
-
if (type !== "
|
|
921
|
+
if (type !== "webide-message" && type !== "cancel" && type !== "steer") {
|
|
1331
922
|
return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
|
|
1332
923
|
}
|
|
1333
|
-
if (kind === "heartbeat" || type === "heartbeat") {
|
|
1334
|
-
return validateHeartbeat(o);
|
|
1335
|
-
}
|
|
1336
924
|
if (type === "cancel") {
|
|
1337
925
|
return validateCancel(o);
|
|
1338
926
|
}
|
|
1339
927
|
if (type === "steer") {
|
|
1340
928
|
return validateSteer(o);
|
|
1341
929
|
}
|
|
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
|
-
}
|
|
1351
930
|
return validateWebIdeMessagePush(o);
|
|
1352
931
|
}
|
|
1353
932
|
|
|
@@ -1382,7 +961,7 @@ function handleSteerAction(payload, ctx) {
|
|
|
1382
961
|
}
|
|
1383
962
|
|
|
1384
963
|
// src/commands/connect/core/worker/worker.ts
|
|
1385
|
-
import { dirname as
|
|
964
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
1386
965
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1387
966
|
import { Worker } from "node:worker_threads";
|
|
1388
967
|
|
|
@@ -1408,8 +987,8 @@ function resolveWebIdeWorkerHeapMb(env = process.env, totalMemoryBytes = totalme
|
|
|
1408
987
|
}
|
|
1409
988
|
|
|
1410
989
|
// src/commands/connect/core/worker/worker.ts
|
|
1411
|
-
var workerFile =
|
|
1412
|
-
|
|
990
|
+
var workerFile = join6(
|
|
991
|
+
dirname2(fileURLToPath2(import.meta.url)),
|
|
1413
992
|
"worker-script.js"
|
|
1414
993
|
);
|
|
1415
994
|
var ActionWorker = class {
|
|
@@ -1428,8 +1007,8 @@ var ActionWorker = class {
|
|
|
1428
1007
|
}
|
|
1429
1008
|
this.msg = msg;
|
|
1430
1009
|
this.apmClient = createApmApiClient(cfg);
|
|
1431
|
-
this.promise = new Promise((
|
|
1432
|
-
this.resolve =
|
|
1010
|
+
this.promise = new Promise((resolve5, reject) => {
|
|
1011
|
+
this.resolve = resolve5;
|
|
1433
1012
|
this.reject = reject;
|
|
1434
1013
|
});
|
|
1435
1014
|
this.worker = new Worker(workerFile, {
|
|
@@ -1485,14 +1064,6 @@ var ActionWorker = class {
|
|
|
1485
1064
|
failIfStillQueued = async (reason) => {
|
|
1486
1065
|
if (this.status !== "pending") return;
|
|
1487
1066
|
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
|
-
}
|
|
1496
1067
|
await this.apmClient.cli.webideSetMessageError({
|
|
1497
1068
|
id: this.msg.messageId,
|
|
1498
1069
|
error: reason,
|
|
@@ -1565,191 +1136,9 @@ function handleWebIdeMessageAction(payload, ctx) {
|
|
|
1565
1136
|
trackActionTask(ctx, task);
|
|
1566
1137
|
}
|
|
1567
1138
|
|
|
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
|
-
|
|
1748
1139
|
// src/commands/connect/actions/index.ts
|
|
1749
1140
|
function dispatchConnectAction(payload, ctx) {
|
|
1750
1141
|
switch (payload.type) {
|
|
1751
|
-
case "heartbeat":
|
|
1752
|
-
return;
|
|
1753
1142
|
case "cancel":
|
|
1754
1143
|
handleCancelAction(payload, ctx);
|
|
1755
1144
|
return;
|
|
@@ -1759,28 +1148,22 @@ function dispatchConnectAction(payload, ctx) {
|
|
|
1759
1148
|
case "webide-message":
|
|
1760
1149
|
handleWebIdeMessageAction(payload, ctx);
|
|
1761
1150
|
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;
|
|
1771
1151
|
}
|
|
1772
1152
|
}
|
|
1773
1153
|
|
|
1774
1154
|
// src/commands/connect-lock.ts
|
|
1775
1155
|
import {
|
|
1776
|
-
existsSync as
|
|
1156
|
+
existsSync as existsSync5,
|
|
1777
1157
|
mkdirSync as mkdirSync4,
|
|
1778
|
-
readFileSync as
|
|
1158
|
+
readFileSync as readFileSync4,
|
|
1779
1159
|
unlinkSync as unlinkSync2,
|
|
1780
|
-
writeFileSync as
|
|
1160
|
+
writeFileSync as writeFileSync3
|
|
1781
1161
|
} from "fs";
|
|
1782
|
-
import {
|
|
1783
|
-
|
|
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;
|
|
1784
1167
|
function isProcessAlive(pid) {
|
|
1785
1168
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1786
1169
|
try {
|
|
@@ -1791,9 +1174,9 @@ function isProcessAlive(pid) {
|
|
|
1791
1174
|
}
|
|
1792
1175
|
}
|
|
1793
1176
|
function readConnectLock() {
|
|
1794
|
-
if (!
|
|
1177
|
+
if (!existsSync5(CONNECT_LOCK_PATH)) return null;
|
|
1795
1178
|
try {
|
|
1796
|
-
const raw =
|
|
1179
|
+
const raw = readFileSync4(CONNECT_LOCK_PATH, "utf8");
|
|
1797
1180
|
const parsed = JSON.parse(raw);
|
|
1798
1181
|
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string") {
|
|
1799
1182
|
return null;
|
|
@@ -1815,27 +1198,44 @@ function pruneStaleConnectLock() {
|
|
|
1815
1198
|
} catch {
|
|
1816
1199
|
}
|
|
1817
1200
|
}
|
|
1818
|
-
function
|
|
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
|
-
}
|
|
1201
|
+
function writeConnectLock() {
|
|
1828
1202
|
mkdirSync4(APM_CONFIG_DIR, { recursive: true });
|
|
1829
1203
|
const lock = {
|
|
1830
1204
|
pid: process.pid,
|
|
1831
1205
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1832
1206
|
};
|
|
1833
|
-
|
|
1207
|
+
writeFileSync3(
|
|
1834
1208
|
CONNECT_LOCK_PATH,
|
|
1835
1209
|
JSON.stringify(lock, null, 2) + "\n",
|
|
1836
1210
|
"utf8"
|
|
1837
1211
|
);
|
|
1838
1212
|
}
|
|
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
|
+
}
|
|
1839
1239
|
function releaseConnectLock() {
|
|
1840
1240
|
const lock = readConnectLock();
|
|
1841
1241
|
if (lock?.pid !== process.pid) return;
|
|
@@ -1846,36 +1246,20 @@ function releaseConnectLock() {
|
|
|
1846
1246
|
}
|
|
1847
1247
|
|
|
1848
1248
|
// src/commands/connect.ts
|
|
1849
|
-
var HEARTBEAT_MS = 3e4;
|
|
1850
1249
|
var RECONNECT_MIN_MS = 1e3;
|
|
1851
1250
|
var RECONNECT_MAX_MS = 6e4;
|
|
1852
1251
|
var SHUTDOWN_DRAIN_MS = 3e3;
|
|
1853
1252
|
function interruptibleSleep(ms, signal) {
|
|
1854
1253
|
if (signal.aborted) return Promise.resolve();
|
|
1855
|
-
return new Promise((
|
|
1856
|
-
const timer = setTimeout(
|
|
1254
|
+
return new Promise((resolve5) => {
|
|
1255
|
+
const timer = setTimeout(resolve5, ms);
|
|
1857
1256
|
const onAbort = () => {
|
|
1858
1257
|
clearTimeout(timer);
|
|
1859
|
-
|
|
1258
|
+
resolve5();
|
|
1860
1259
|
};
|
|
1861
1260
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1862
1261
|
});
|
|
1863
1262
|
}
|
|
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
|
-
}
|
|
1879
1263
|
function attachWsHandlers(ws, ctx, onOpen) {
|
|
1880
1264
|
const { isShuttingDown } = ctx;
|
|
1881
1265
|
ws.on("open", () => {
|
|
@@ -1890,9 +1274,6 @@ function attachWsHandlers(ws, ctx, onOpen) {
|
|
|
1890
1274
|
console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
|
|
1891
1275
|
return;
|
|
1892
1276
|
}
|
|
1893
|
-
if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
1277
|
const validated = validateAgentWsMessage(parsed, "outbound");
|
|
1897
1278
|
if (!validated.ok) {
|
|
1898
1279
|
console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
|
|
@@ -1902,15 +1283,13 @@ function attachWsHandlers(ws, ctx, onOpen) {
|
|
|
1902
1283
|
});
|
|
1903
1284
|
}
|
|
1904
1285
|
function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
1905
|
-
return new Promise((
|
|
1286
|
+
return new Promise((resolve5, reject) => {
|
|
1906
1287
|
const ws = new WebSocket(url);
|
|
1907
|
-
let stopHeartbeat;
|
|
1908
1288
|
let settled = false;
|
|
1909
1289
|
const finish = (fn) => {
|
|
1910
1290
|
if (settled) return;
|
|
1911
1291
|
settled = true;
|
|
1912
1292
|
connectionAbort.removeEventListener("abort", onConnectionAbort);
|
|
1913
|
-
stopHeartbeat?.();
|
|
1914
1293
|
fn();
|
|
1915
1294
|
};
|
|
1916
1295
|
const onConnectionAbort = () => {
|
|
@@ -1919,10 +1298,7 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
|
1919
1298
|
}
|
|
1920
1299
|
};
|
|
1921
1300
|
connectionAbort.addEventListener("abort", onConnectionAbort);
|
|
1922
|
-
attachWsHandlers(ws, ctx,
|
|
1923
|
-
stopHeartbeat = startHeartbeat(ws, ctx.clientMachineId);
|
|
1924
|
-
onConnected();
|
|
1925
|
-
});
|
|
1301
|
+
attachWsHandlers(ws, ctx, onConnected);
|
|
1926
1302
|
ws.on("close", (code, reason) => {
|
|
1927
1303
|
console.log(
|
|
1928
1304
|
`[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
|
|
@@ -1931,7 +1307,7 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
|
1931
1307
|
finish(() => reject(new Error("shutdown")));
|
|
1932
1308
|
return;
|
|
1933
1309
|
}
|
|
1934
|
-
finish(
|
|
1310
|
+
finish(resolve5);
|
|
1935
1311
|
});
|
|
1936
1312
|
ws.on("error", (err) => {
|
|
1937
1313
|
console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
|
|
@@ -1945,33 +1321,26 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
|
1945
1321
|
});
|
|
1946
1322
|
});
|
|
1947
1323
|
}
|
|
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
|
-
}
|
|
1959
1324
|
async function runConnect(options) {
|
|
1960
1325
|
const { didUpdate } = await runUpdate({ quietIfCurrent: true });
|
|
1961
1326
|
if (didUpdate) {
|
|
1962
1327
|
reexecConnectAfterUpdate(options);
|
|
1963
1328
|
}
|
|
1964
1329
|
const cfg = await ensureLoggedConfig();
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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;
|
|
1969
1338
|
const clientMachineId = resolveClientMachineId(cfg);
|
|
1970
1339
|
if (!clientMachineId) {
|
|
1971
1340
|
console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
|
|
1972
1341
|
process.exit(1);
|
|
1973
1342
|
}
|
|
1974
|
-
acquireConnectLock();
|
|
1343
|
+
await acquireConnectLock();
|
|
1975
1344
|
process.on("exit", releaseConnectLock);
|
|
1976
1345
|
let shuttingDown = false;
|
|
1977
1346
|
let shutdownPromise = null;
|
|
@@ -1998,11 +1367,11 @@ async function runConnect(options) {
|
|
|
1998
1367
|
lifecycleAbort.abort();
|
|
1999
1368
|
currentConnectionAbort?.abort();
|
|
2000
1369
|
shutdownAbort.abort();
|
|
1370
|
+
releaseConnectLock();
|
|
2001
1371
|
try {
|
|
2002
|
-
await Promise.race([Promise.all(activeTasks),
|
|
1372
|
+
await Promise.race([Promise.all(activeTasks), delay2(drainMs)]);
|
|
2003
1373
|
} catch {
|
|
2004
1374
|
}
|
|
2005
|
-
releaseConnectLock();
|
|
2006
1375
|
process.exit(code);
|
|
2007
1376
|
};
|
|
2008
1377
|
const shutdown = (code = 0) => {
|
|
@@ -2024,8 +1393,12 @@ async function runConnect(options) {
|
|
|
2024
1393
|
lifecycleAbort.signal,
|
|
2025
1394
|
currentConnectionAbort.signal
|
|
2026
1395
|
]);
|
|
2027
|
-
const url = buildAgentWsUrl(
|
|
2028
|
-
|
|
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`);
|
|
2029
1402
|
try {
|
|
2030
1403
|
await connectOnce(url, connectCtx, connectionSignal, () => {
|
|
2031
1404
|
reconnectDelay = RECONNECT_MIN_MS;
|
|
@@ -2057,18 +1430,566 @@ async function runConnect(options) {
|
|
|
2057
1430
|
}
|
|
2058
1431
|
}
|
|
2059
1432
|
|
|
1433
|
+
// src/utils/git.ts
|
|
1434
|
+
import { execFile } from "child_process";
|
|
1435
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1436
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1437
|
+
import { promisify } from "util";
|
|
1438
|
+
var execFileAsync = promisify(execFile);
|
|
1439
|
+
var GIT_NETWORK_TIMEOUT_MS = 9e4;
|
|
1440
|
+
var GIT_PUSH_TIMEOUT_MS = 12e4;
|
|
1441
|
+
var GIT_LOCAL_TIMEOUT_MS = 6e4;
|
|
1442
|
+
var GIT_NETWORK_CMDS = /* @__PURE__ */ new Set([
|
|
1443
|
+
"fetch",
|
|
1444
|
+
"push",
|
|
1445
|
+
"ls-remote",
|
|
1446
|
+
"clone",
|
|
1447
|
+
"pull"
|
|
1448
|
+
]);
|
|
1449
|
+
function mergeGitSshCommand(existing) {
|
|
1450
|
+
const base = existing?.trim() || "ssh";
|
|
1451
|
+
const extras = [];
|
|
1452
|
+
if (!/\bStrictHostKeyChecking=/.test(base)) {
|
|
1453
|
+
extras.push("-o", "StrictHostKeyChecking=accept-new");
|
|
1454
|
+
}
|
|
1455
|
+
if (!/\bBatchMode=/.test(base)) {
|
|
1456
|
+
extras.push("-o", "BatchMode=yes");
|
|
1457
|
+
}
|
|
1458
|
+
if (!/\bConnectTimeout=/.test(base)) {
|
|
1459
|
+
extras.push("-o", "ConnectTimeout=20");
|
|
1460
|
+
}
|
|
1461
|
+
if (!/\bServerAliveInterval=/.test(base)) {
|
|
1462
|
+
extras.push("-o", "ServerAliveInterval=10");
|
|
1463
|
+
}
|
|
1464
|
+
if (!/\bServerAliveCountMax=/.test(base)) {
|
|
1465
|
+
extras.push("-o", "ServerAliveCountMax=3");
|
|
1466
|
+
}
|
|
1467
|
+
return extras.length ? `${base} ${extras.join(" ")}` : base;
|
|
1468
|
+
}
|
|
1469
|
+
function gitExecEnv() {
|
|
1470
|
+
return {
|
|
1471
|
+
...process.env,
|
|
1472
|
+
GIT_SSH_COMMAND: mergeGitSshCommand(process.env.GIT_SSH_COMMAND),
|
|
1473
|
+
// 避免 credential helper / askpass 在无 TTY 时挂起
|
|
1474
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function defaultGitTimeoutMs(args) {
|
|
1478
|
+
const cmd = args[0] ?? "";
|
|
1479
|
+
if (cmd === "push") return GIT_PUSH_TIMEOUT_MS;
|
|
1480
|
+
if (GIT_NETWORK_CMDS.has(cmd)) return GIT_NETWORK_TIMEOUT_MS;
|
|
1481
|
+
return GIT_LOCAL_TIMEOUT_MS;
|
|
1482
|
+
}
|
|
1483
|
+
async function execGit(cwd, args, quietOrOptions = false) {
|
|
1484
|
+
const options = typeof quietOrOptions === "boolean" ? { quiet: quietOrOptions } : quietOrOptions;
|
|
1485
|
+
const quiet = options.quiet ?? false;
|
|
1486
|
+
const timeoutMs = options.timeoutMs === void 0 ? defaultGitTimeoutMs(args) : options.timeoutMs;
|
|
1487
|
+
try {
|
|
1488
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
1489
|
+
cwd,
|
|
1490
|
+
env: gitExecEnv(),
|
|
1491
|
+
encoding: "utf8",
|
|
1492
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1493
|
+
...timeoutMs > 0 ? { timeout: timeoutMs, killSignal: "SIGTERM" } : {}
|
|
1494
|
+
});
|
|
1495
|
+
if (!quiet && stderr.trim()) {
|
|
1496
|
+
process.stderr.write(stderr);
|
|
1497
|
+
}
|
|
1498
|
+
return stdout;
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
const e = err;
|
|
1501
|
+
if (e.killed || e.signal === "SIGTERM" || e.code === "ETIMEDOUT") {
|
|
1502
|
+
throw new Error(
|
|
1503
|
+
`[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`
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const detail = (e.stderr ?? e.message ?? String(err)).trim();
|
|
1507
|
+
throw new Error(
|
|
1508
|
+
`[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
async function ensureGitRepo(cwd) {
|
|
1513
|
+
await execGit(cwd, ["rev-parse", "--git-dir"], true);
|
|
1514
|
+
}
|
|
1515
|
+
async function resolveGitRepoRoot(cwd) {
|
|
1516
|
+
return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
|
|
1517
|
+
}
|
|
1518
|
+
function resolveGitScript(name) {
|
|
1519
|
+
return join8(dirname3(fileURLToPath3(import.meta.url)), "scripts", name);
|
|
1520
|
+
}
|
|
1521
|
+
async function runGitScript(name, options) {
|
|
1522
|
+
const skipCode = options.skipCode ?? 2;
|
|
1523
|
+
const timeoutMs = options.timeoutMs ?? GIT_NETWORK_TIMEOUT_MS;
|
|
1524
|
+
const script = resolveGitScript(name);
|
|
1525
|
+
const args = [script, ...options.args ?? []];
|
|
1526
|
+
try {
|
|
1527
|
+
const { stdout, stderr } = await execFileAsync("bash", args, {
|
|
1528
|
+
cwd: options.cwd,
|
|
1529
|
+
env: gitExecEnv(),
|
|
1530
|
+
encoding: "utf8",
|
|
1531
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1532
|
+
timeout: timeoutMs,
|
|
1533
|
+
killSignal: "SIGTERM"
|
|
1534
|
+
});
|
|
1535
|
+
if (stdout.trim()) process.stdout.write(stdout);
|
|
1536
|
+
if (stderr.trim()) process.stderr.write(stderr);
|
|
1537
|
+
return true;
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
const e = err;
|
|
1540
|
+
if (e.stdout?.trim()) process.stdout.write(e.stdout);
|
|
1541
|
+
if (e.stderr?.trim()) process.stderr.write(e.stderr);
|
|
1542
|
+
if (e.code === skipCode) {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
if (e.killed || e.signal === "SIGTERM" || e.code === "ETIMEDOUT") {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
`[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`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
const detail = (e.stderr ?? e.message ?? String(err)).trim();
|
|
1551
|
+
throw new Error(`[apm] ${name} \u5931\u8D25${detail ? `: ${detail}` : ""}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function commitWorkingTreeIfDirty(cwd, message) {
|
|
1555
|
+
await ensureGitRepo(cwd);
|
|
1556
|
+
return runGitScript("commit_if_dirty.sh", {
|
|
1557
|
+
cwd,
|
|
1558
|
+
args: message?.trim() ? [message.trim()] : [],
|
|
1559
|
+
timeoutMs: GIT_LOCAL_TIMEOUT_MS
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
async function ensureFeatureBranch(cwd, branch, baselineBranch) {
|
|
1563
|
+
await runGitScript("ensure_feature_branch.sh", {
|
|
1564
|
+
cwd,
|
|
1565
|
+
args: [branch, baselineBranch],
|
|
1566
|
+
timeoutMs: GIT_PUSH_TIMEOUT_MS
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
async function pushCurrentBranch(cwd) {
|
|
1570
|
+
return runGitScript("push_code.sh", {
|
|
1571
|
+
cwd,
|
|
1572
|
+
timeoutMs: GIT_PUSH_TIMEOUT_MS
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/utils/home-repository.ts
|
|
1577
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1578
|
+
import { join as join9, resolve as resolve3 } from "path";
|
|
1579
|
+
var HOME_REPOSITORY_JSON = "/home/repository.json";
|
|
1580
|
+
function isSimpleChildDirName(name) {
|
|
1581
|
+
if (!name || name === "." || name === "..") return false;
|
|
1582
|
+
if (name.includes("/") || name.includes("\\")) return false;
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
function loadHomeRepositoryEntries() {
|
|
1586
|
+
const path = toFsPath(HOME_REPOSITORY_JSON);
|
|
1587
|
+
if (!existsSync6(path)) {
|
|
1588
|
+
throw new Error(`[apm] \u672A\u627E\u5230\u591A\u4ED3\u6E05\u5355 ${HOME_REPOSITORY_JSON}`);
|
|
1589
|
+
}
|
|
1590
|
+
let parsed;
|
|
1591
|
+
try {
|
|
1592
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1595
|
+
throw new Error(`[apm] \u89E3\u6790 ${HOME_REPOSITORY_JSON} \u5931\u8D25: ${detail}`);
|
|
1596
|
+
}
|
|
1597
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1598
|
+
throw new Error(
|
|
1599
|
+
`[apm] ${HOME_REPOSITORY_JSON} \u987B\u4E3A\u5BF9\u8C61\uFF1Akey=\u4E0B\u7EA7\u76EE\u5F55\uFF0Cvalue=git \u5730\u5740`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const entries = [];
|
|
1603
|
+
for (const [rawKey, rawVal] of Object.entries(
|
|
1604
|
+
parsed
|
|
1605
|
+
)) {
|
|
1606
|
+
const pathKey = rawKey.trim();
|
|
1607
|
+
if (!isSimpleChildDirName(pathKey)) {
|
|
1608
|
+
throw new Error(
|
|
1609
|
+
`[apm] ${HOME_REPOSITORY_JSON} \u975E\u6CD5\u76EE\u5F55 key: ${JSON.stringify(rawKey)}\uFF08\u987B\u4E3A\u5F53\u524D\u4E0B\u7EA7\u76EE\u5F55\u540D\uFF09`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof rawVal !== "string" || !rawVal.trim()) {
|
|
1613
|
+
throw new Error(
|
|
1614
|
+
`[apm] ${HOME_REPOSITORY_JSON}[${pathKey}] \u987B\u4E3A\u975E\u7A7A git \u5730\u5740\u5B57\u7B26\u4E32`
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
entries.push({ path: pathKey, remoteUrl: rawVal.trim() });
|
|
1618
|
+
}
|
|
1619
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
1620
|
+
if (entries.length === 0) {
|
|
1621
|
+
throw new Error(`[apm] ${HOME_REPOSITORY_JSON} \u4E3A\u7A7A\uFF0C\u65E0\u4ED3\u5E93\u53EF\u786E\u8BA4`);
|
|
1622
|
+
}
|
|
1623
|
+
return entries;
|
|
1624
|
+
}
|
|
1625
|
+
function resolveHomeRepositoryAbsolutePaths(workdirInput, entries = loadHomeRepositoryEntries()) {
|
|
1626
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
1627
|
+
return entries.map((entry) => resolve3(join9(workdir, entry.path)));
|
|
1628
|
+
}
|
|
1629
|
+
function tryLoadHomeRepositoryEntries() {
|
|
1630
|
+
const path = toFsPath(HOME_REPOSITORY_JSON);
|
|
1631
|
+
if (!existsSync6(path)) return null;
|
|
1632
|
+
return loadHomeRepositoryEntries();
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/utils/workspace-repos.ts
|
|
1636
|
+
import {
|
|
1637
|
+
existsSync as existsSync7,
|
|
1638
|
+
mkdirSync as mkdirSync5,
|
|
1639
|
+
readFileSync as readFileSync6,
|
|
1640
|
+
readdirSync as readdirSync3,
|
|
1641
|
+
statSync as statSync3,
|
|
1642
|
+
writeFileSync as writeFileSync4
|
|
1643
|
+
} from "fs";
|
|
1644
|
+
import { basename, dirname as dirname4, join as join10, relative, resolve as resolve4 } from "path";
|
|
1645
|
+
|
|
1646
|
+
// src/utils/git-remote.ts
|
|
1647
|
+
import { execFile as execFile2 } from "child_process";
|
|
1648
|
+
import { promisify as promisify2 } from "util";
|
|
1649
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1650
|
+
|
|
1651
|
+
// src/utils/workspace-repos.ts
|
|
1652
|
+
var WORKSPACE_REPOS_MANIFEST = "workspace-repos.json";
|
|
1653
|
+
var WORKSPACE_REPOS_VERSION = 1;
|
|
1654
|
+
function manifestPath(workdir) {
|
|
1655
|
+
return join10(workspaceApmDir(workdir), WORKSPACE_REPOS_MANIFEST);
|
|
1656
|
+
}
|
|
1657
|
+
function absoluteRepoPath(workdir, entry) {
|
|
1658
|
+
return resolve4(workdir, entry.path);
|
|
1659
|
+
}
|
|
1660
|
+
function toRelativeRepoPath(workdir, repoAbs) {
|
|
1661
|
+
return relative(workdir, repoAbs).replace(/\\/g, "/");
|
|
1662
|
+
}
|
|
1663
|
+
function normalizeRepoEntry(raw) {
|
|
1664
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1665
|
+
const o = raw;
|
|
1666
|
+
if (typeof o.path !== "string" || !o.path.trim()) return null;
|
|
1667
|
+
const path = o.path.trim().replace(/\\/g, "/");
|
|
1668
|
+
if (path === "." || path.includes("/")) return null;
|
|
1669
|
+
const entry = { path };
|
|
1670
|
+
if (typeof o.remoteUrl === "string" && o.remoteUrl.trim()) {
|
|
1671
|
+
entry.remoteUrl = o.remoteUrl.trim();
|
|
1672
|
+
} else if (o.remoteUrl === null) {
|
|
1673
|
+
entry.remoteUrl = null;
|
|
1674
|
+
}
|
|
1675
|
+
return entry;
|
|
1676
|
+
}
|
|
1677
|
+
function readManifest(workdir) {
|
|
1678
|
+
const path = toFsPath(manifestPath(workdir));
|
|
1679
|
+
if (!existsSync7(path)) {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
const raw = JSON.parse(
|
|
1684
|
+
readFileSync6(path, "utf8")
|
|
1685
|
+
);
|
|
1686
|
+
if (raw?.version !== WORKSPACE_REPOS_VERSION) {
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
if (raw.kind !== "multi") {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
if (!Array.isArray(raw.repos) || raw.repos.length === 0) {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
if (typeof raw.workdir !== "string" || !raw.workdir.trim()) {
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
const repos = raw.repos.map((item) => normalizeRepoEntry(item)).filter((item) => item != null);
|
|
1699
|
+
if (repos.length === 0) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
version: WORKSPACE_REPOS_VERSION,
|
|
1704
|
+
kind: "multi",
|
|
1705
|
+
workdir: raw.workdir,
|
|
1706
|
+
repos,
|
|
1707
|
+
scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt.trim() ? raw.scannedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
1708
|
+
};
|
|
1709
|
+
} catch {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
function loadWorkspaceReposCache(workdirInput) {
|
|
1714
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
1715
|
+
const cached = readManifest(workdir);
|
|
1716
|
+
if (!cached) {
|
|
1717
|
+
throw new Error(
|
|
1718
|
+
`[apm] \u672A\u627E\u5230\u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 .apm/${WORKSPACE_REPOS_MANIFEST}\uFF0C\u8BF7\u5148\u6267\u884C\u4ED3\u5E93\u68C0\u6D4B`
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (normalizeWorkdirPath(cached.workdir) !== normalizeWorkdirPath(workdir)) {
|
|
1722
|
+
throw new Error(
|
|
1723
|
+
`[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`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
if (!Array.isArray(cached.repos) || cached.repos.length === 0) {
|
|
1727
|
+
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`);
|
|
1728
|
+
}
|
|
1729
|
+
console.log(
|
|
1730
|
+
`[apm] \u8BFB\u53D6\u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 repos=${cached.repos.map((r) => r.path).join(", ")}`
|
|
1731
|
+
);
|
|
1732
|
+
return {
|
|
1733
|
+
...cached,
|
|
1734
|
+
workdir
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function resolveWorkspaceRepoAbsolutePaths(manifest) {
|
|
1738
|
+
return manifest.repos.map(
|
|
1739
|
+
(entry) => absoluteRepoPath(manifest.workdir, entry)
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
function formatRepoLabel(workdir, repoAbs) {
|
|
1743
|
+
const rel = toRelativeRepoPath(resolveWorkdirPath(workdir), repoAbs);
|
|
1744
|
+
return rel || basename(repoAbs) || repoAbs;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/commands/branch.ts
|
|
1748
|
+
var TASK_BRANCH_PREFIX = "feat/task-";
|
|
1749
|
+
async function syncTaskRepos(workdir, message) {
|
|
1750
|
+
const workdirPath = resolveWorkdirPath(workdir);
|
|
1751
|
+
const repoRoots = resolveTaskBranchRepoRoots(workdirPath);
|
|
1752
|
+
let committed = 0;
|
|
1753
|
+
let commitSkipped = 0;
|
|
1754
|
+
let pushed = 0;
|
|
1755
|
+
let pushSkipped = 0;
|
|
1756
|
+
const failed = [];
|
|
1757
|
+
for (const gitRoot of repoRoots) {
|
|
1758
|
+
const label = formatRepoLabel(workdirPath, gitRoot);
|
|
1759
|
+
try {
|
|
1760
|
+
const didCommit = await commitWorkingTreeIfDirty(gitRoot, message);
|
|
1761
|
+
if (didCommit) {
|
|
1762
|
+
committed += 1;
|
|
1763
|
+
console.log(`[apm] \u5DF2\u63D0\u4EA4 @ ${label}`);
|
|
1764
|
+
} else {
|
|
1765
|
+
commitSkipped += 1;
|
|
1766
|
+
}
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1769
|
+
console.error(`[apm] \u4ED3\u5E93 ${label} \u63D0\u4EA4\u5931\u8D25\uFF08\u7EE7\u7EED\u5176\u5B83\u4ED3\uFF09: ${detail}`);
|
|
1770
|
+
failed.push(label);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
try {
|
|
1774
|
+
const didPush = await pushCurrentBranch(gitRoot);
|
|
1775
|
+
if (didPush) {
|
|
1776
|
+
pushed += 1;
|
|
1777
|
+
console.log(`[apm] \u5DF2 push @ ${label}`);
|
|
1778
|
+
} else {
|
|
1779
|
+
pushSkipped += 1;
|
|
1780
|
+
}
|
|
1781
|
+
} catch (err) {
|
|
1782
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1783
|
+
console.error(`[apm] \u4ED3\u5E93 ${label} push \u5931\u8D25\uFF08\u7EE7\u7EED\u5176\u5B83\u4ED3\uFF09: ${detail}`);
|
|
1784
|
+
failed.push(label);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return {
|
|
1788
|
+
committed,
|
|
1789
|
+
commitSkipped,
|
|
1790
|
+
pushed,
|
|
1791
|
+
pushSkipped,
|
|
1792
|
+
failed,
|
|
1793
|
+
repos: repoRoots
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function branchNameForTask(taskId) {
|
|
1797
|
+
const id = taskId.trim();
|
|
1798
|
+
if (!id) {
|
|
1799
|
+
throw new Error("[apm] \u4EFB\u52A1 ID \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1800
|
+
}
|
|
1801
|
+
if (/[\s/\\]/.test(id)) {
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
"[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"
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
return `${TASK_BRANCH_PREFIX}${id}`;
|
|
1807
|
+
}
|
|
1808
|
+
async function ensureTaskFeatureBranch(branch, projectId, options) {
|
|
1809
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1810
|
+
const gitRoot = await resolveGitRepoRoot(cwd);
|
|
1811
|
+
await ensureFeatureBranch(gitRoot, branch, projectId);
|
|
1812
|
+
return branch;
|
|
1813
|
+
}
|
|
1814
|
+
function resolveTaskBranchRepoRoots(workdirPath) {
|
|
1815
|
+
const entries = tryLoadHomeRepositoryEntries();
|
|
1816
|
+
if (entries) {
|
|
1817
|
+
const roots = resolveHomeRepositoryAbsolutePaths(workdirPath, entries);
|
|
1818
|
+
console.log(
|
|
1819
|
+
`[apm] \u8BFB\u53D6 ${HOME_REPOSITORY_JSON} repos=${entries.map((e) => `${e.path}\u2192${e.remoteUrl}`).join(", ")}`
|
|
1820
|
+
);
|
|
1821
|
+
return roots;
|
|
1822
|
+
}
|
|
1823
|
+
const manifest = loadWorkspaceReposCache(workdirPath);
|
|
1824
|
+
return resolveWorkspaceRepoAbsolutePaths(manifest);
|
|
1825
|
+
}
|
|
1826
|
+
async function runTaskBranch(taskId, projectId, options = {}) {
|
|
1827
|
+
const trimmedTaskId = taskId.trim();
|
|
1828
|
+
if (!trimmedTaskId) {
|
|
1829
|
+
throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1830
|
+
}
|
|
1831
|
+
const trimmedProjectId = projectId.trim();
|
|
1832
|
+
if (!trimmedProjectId) {
|
|
1833
|
+
throw new Error("[apm] projectId\uFF08\u9879\u76EE ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1834
|
+
}
|
|
1835
|
+
if (/[\s\\]/.test(trimmedProjectId) || trimmedProjectId.includes("..")) {
|
|
1836
|
+
throw new Error("[apm] \u9879\u76EE ID \u975E\u6CD5");
|
|
1837
|
+
}
|
|
1838
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1839
|
+
const workdirPath = resolveWorkdirPath(cwd);
|
|
1840
|
+
const repoRoots = resolveTaskBranchRepoRoots(workdirPath);
|
|
1841
|
+
if (repoRoots.length === 0) {
|
|
1842
|
+
throw new Error("[apm] \u5DE5\u4F5C\u533A\u4ED3\u5E93\u6E05\u5355\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u786E\u8BA4\u7279\u6027\u5206\u652F");
|
|
1843
|
+
}
|
|
1844
|
+
const branch = branchNameForTask(trimmedTaskId);
|
|
1845
|
+
const repoLabels = repoRoots.map(
|
|
1846
|
+
(gitRoot) => formatRepoLabel(workdirPath, gitRoot)
|
|
1847
|
+
);
|
|
1848
|
+
console.log(
|
|
1849
|
+
`[apm] \u591A\u4ED3\u786E\u8BA4\u7279\u6027\u5206\u652F ${branch} projectId=${trimmedProjectId} repos=${repoLabels.join(",")}`
|
|
1850
|
+
);
|
|
1851
|
+
for (const gitRoot of repoRoots) {
|
|
1852
|
+
const label = formatRepoLabel(workdirPath, gitRoot);
|
|
1853
|
+
console.log(
|
|
1854
|
+
`[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (projectId=${trimmedProjectId})`
|
|
1855
|
+
);
|
|
1856
|
+
try {
|
|
1857
|
+
await ensureTaskFeatureBranch(branch, trimmedProjectId, {
|
|
1858
|
+
...options,
|
|
1859
|
+
cwd: gitRoot
|
|
1860
|
+
});
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1863
|
+
throw new Error(
|
|
1864
|
+
`[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}`
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
branch,
|
|
1870
|
+
projectId: trimmedProjectId,
|
|
1871
|
+
kind: "multi",
|
|
1872
|
+
repos: repoRoots
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/commands/ensure-task-branch.ts
|
|
1877
|
+
async function runEnsureTaskBranchCli(options = {}) {
|
|
1878
|
+
const workdir = resolveWorkdirPath(options.cwd ?? process.cwd());
|
|
1879
|
+
const taskId = options.taskId?.trim() || process.env.APM_TASK_ID?.trim() || "";
|
|
1880
|
+
const projectId = options.projectId?.trim() || process.env.APM_PROJECT_ID?.trim() || "";
|
|
1881
|
+
if (!taskId) {
|
|
1882
|
+
throw new Error(
|
|
1883
|
+
"[apm] \u7F3A\u5C11 taskId\uFF1A\u8BF7\u4F20\u5165\u53C2\u6570\u6216\u8BBE\u7F6E APM_TASK_ID"
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
if (!projectId) {
|
|
1887
|
+
throw new Error(
|
|
1888
|
+
"[apm] \u7F3A\u5C11 projectId\uFF1A\u8BF7\u4F20\u5165\u53C2\u6570\u6216\u8BBE\u7F6E APM_PROJECT_ID"
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
const result = await runTaskBranch(taskId, projectId, { cwd: workdir });
|
|
1892
|
+
console.log(
|
|
1893
|
+
`[apm] \u7279\u6027\u5206\u652F\u5DF2\u5C31\u7EEA\uFF08\u5168\u90E8 ${result.repos.length} \u4E2A\u4ED3\uFF09 branch=${result.branch} projectId=${result.projectId}`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// src/utils/user-prompt-cache.ts
|
|
1898
|
+
import { createHash } from "crypto";
|
|
1899
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1900
|
+
import { join as join11 } from "path";
|
|
1901
|
+
import { tmpdir } from "os";
|
|
1902
|
+
var CACHE_ROOT = join11(tmpdir(), "apm-hooks");
|
|
1903
|
+
var PROMPT_FILE = "user-prompt.txt";
|
|
1904
|
+
var COMMIT_SUBJECT_MAX_LEN = 100;
|
|
1905
|
+
function workdirKey(workdir) {
|
|
1906
|
+
return createHash("sha256").update(resolveWorkdirPath(workdir)).digest("hex").slice(0, 16);
|
|
1907
|
+
}
|
|
1908
|
+
function userPromptCachePath(workdirInput) {
|
|
1909
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
1910
|
+
return join11(CACHE_ROOT, workdirKey(workdir), PROMPT_FILE);
|
|
1911
|
+
}
|
|
1912
|
+
function formatUserPromptAsCommitMessage(prompt) {
|
|
1913
|
+
const oneLine = prompt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
1914
|
+
if (!oneLine) return "";
|
|
1915
|
+
const cleaned = oneLine.replace(/\s+/g, " ").trim();
|
|
1916
|
+
if (cleaned.length <= COMMIT_SUBJECT_MAX_LEN) return cleaned;
|
|
1917
|
+
return `${cleaned.slice(0, COMMIT_SUBJECT_MAX_LEN - 1)}\u2026`;
|
|
1918
|
+
}
|
|
1919
|
+
function loadUserPromptCommitMessage(workdirInput) {
|
|
1920
|
+
const path = toFsPath(userPromptCachePath(workdirInput));
|
|
1921
|
+
if (!existsSync8(path)) return null;
|
|
1922
|
+
try {
|
|
1923
|
+
const raw = readFileSync7(path, "utf8");
|
|
1924
|
+
const msg = formatUserPromptAsCommitMessage(raw);
|
|
1925
|
+
return msg || null;
|
|
1926
|
+
} catch {
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// src/commands/commit-task-repos.ts
|
|
1932
|
+
function resolveCommitTaskMessage(workdir, explicit) {
|
|
1933
|
+
const fromOpt = explicit?.trim() ?? "";
|
|
1934
|
+
if (fromOpt) return fromOpt;
|
|
1935
|
+
const fromPrompt = loadUserPromptCommitMessage(workdir);
|
|
1936
|
+
if (fromPrompt) return fromPrompt;
|
|
1937
|
+
const taskId = process.env.APM_TASK_ID?.trim() || "";
|
|
1938
|
+
const projectId = process.env.APM_PROJECT_ID?.trim() || "";
|
|
1939
|
+
if (taskId) {
|
|
1940
|
+
let branch = "";
|
|
1941
|
+
try {
|
|
1942
|
+
branch = branchNameForTask(taskId);
|
|
1943
|
+
} catch {
|
|
1944
|
+
branch = `feat/task-${taskId}`;
|
|
1945
|
+
}
|
|
1946
|
+
if (projectId) {
|
|
1947
|
+
return `chore(apm): sync ${branch} (project ${projectId})`;
|
|
1948
|
+
}
|
|
1949
|
+
return `chore(apm): sync ${branch}`;
|
|
1950
|
+
}
|
|
1951
|
+
if (projectId) {
|
|
1952
|
+
return `chore(apm): sync workspace (project ${projectId})`;
|
|
1953
|
+
}
|
|
1954
|
+
return void 0;
|
|
1955
|
+
}
|
|
1956
|
+
async function runCommitTaskReposCli(options = {}) {
|
|
1957
|
+
const workdir = resolveWorkdirPath(options.cwd ?? process.cwd());
|
|
1958
|
+
const message = resolveCommitTaskMessage(workdir, options.message);
|
|
1959
|
+
if (message) {
|
|
1960
|
+
console.log(`[apm] commit message: ${message}`);
|
|
1961
|
+
}
|
|
1962
|
+
const result = await syncTaskRepos(workdir, message);
|
|
1963
|
+
console.log(
|
|
1964
|
+
`[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}`
|
|
1965
|
+
);
|
|
1966
|
+
if (result.failed.length > 0) {
|
|
1967
|
+
throw new Error(
|
|
1968
|
+
`[apm] \u4EE5\u4E0B\u4ED3\u5E93 commit/push \u5931\u8D25: ${result.failed.join(", ")}`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
2060
1973
|
// src/index.ts
|
|
2061
1974
|
function buildProgram() {
|
|
2062
1975
|
const program = new Command();
|
|
2063
1976
|
program.name("apm").description(
|
|
2064
1977
|
`\u6BD4\u90BB\u661F\u56FE\u547D\u4EE4\u884C\uFF08WebIDE \u5DE5\u4F5C\u533A\u4E0E\u7814\u53D1\u81EA\u52A8\u5316\uFF09\u3002
|
|
2065
|
-
\
|
|
1978
|
+
\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`
|
|
2066
1979
|
).version(readCliVersion(), "-V, --version", "\u663E\u793A\u7248\u672C\u53F7").helpOption("-h, --help", "\u663E\u793A\u5E2E\u52A9").showHelpAfterError(true);
|
|
2067
1980
|
program.command("login").description(
|
|
2068
1981
|
"\u9A8C\u8BC1 API Key \u5E76\u5199\u5165 ~/.config/apm/config.json\uFF08GET /api/v1/cli/me\uFF09"
|
|
2069
|
-
).option("--api-key <key>", "\u8FDC\u7A0B\u4E3B\u673A API Key\uFF08cm_ \u5F00\u5934\uFF09").option(
|
|
2070
|
-
|
|
2071
|
-
|
|
1982
|
+
).option("--api-key <key>", "\u8FDC\u7A0B\u4E3B\u673A API Key\uFF08cm_ \u5F00\u5934\uFF09").option(
|
|
1983
|
+
"--server <url>",
|
|
1984
|
+
"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"
|
|
1985
|
+
).option(
|
|
1986
|
+
"--socket-server <url>",
|
|
1987
|
+
"Agent WebSocket \u6839\u5730\u5740\uFF08\u53EF\u4E0E API \u4E0D\u540C\uFF09\uFF0C\u4F8B\u5982 wss://ws.example.com"
|
|
1988
|
+
).action(
|
|
1989
|
+
async (opts) => {
|
|
1990
|
+
await runLogin(opts);
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
2072
1993
|
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 () => {
|
|
2073
1994
|
await runInit();
|
|
2074
1995
|
});
|
|
@@ -2085,15 +2006,47 @@ function buildProgram() {
|
|
|
2085
2006
|
program.command("update-skills").description("\u4ECE\u5E73\u53F0\u540C\u6B65 .apm/ \u4E0B\u7684 WebIDE \u6307\u5357\u4E0E\u89C4\u5219").action(async () => {
|
|
2086
2007
|
await runUpdateSkills();
|
|
2087
2008
|
});
|
|
2088
|
-
program.command("
|
|
2089
|
-
"\
|
|
2090
|
-
).
|
|
2091
|
-
|
|
2009
|
+
program.command("ensure-task-branch").description(
|
|
2010
|
+
"\u786E\u4FDD\u5DE5\u4F5C\u533A\u5404\u4ED3\u4F4D\u4E8E feat/task-<\u4EFB\u52A1ID>\uFF08\u4E0D\u5B58\u5728\u5219\u57FA\u4E8E\u9879\u76EE ID \u540C\u540D\u5206\u652F\u4ECE\u8FDC\u7A0B\u521B\u5EFA\uFF09\uFF1B\u4F9B Cursor hooks \u8C03\u7528"
|
|
2011
|
+
).argument(
|
|
2012
|
+
"[taskId]",
|
|
2013
|
+
"\u4EFB\u52A1 ID\uFF08\u521B\u5EFA/\u5207\u6362\u5206\u652F\uFF09\uFF1B\u7701\u7565\u65F6\u8BFB APM_TASK_ID"
|
|
2014
|
+
).argument(
|
|
2015
|
+
"[projectId]",
|
|
2016
|
+
"\u9879\u76EE ID\uFF08\u4EC5\u521B\u5EFA\u65F6\u7528\uFF1B\u89C4\u8303\u4E0E\u9879\u76EE\u5206\u652F\u540C\u540D\uFF09\uFF1B\u7701\u7565\u65F6\u8BFB APM_PROJECT_ID"
|
|
2017
|
+
).option("--cwd <dir>", "\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").action(
|
|
2018
|
+
async (taskId, projectId, opts) => {
|
|
2019
|
+
await runEnsureTaskBranchCli({
|
|
2020
|
+
taskId,
|
|
2021
|
+
projectId,
|
|
2022
|
+
cwd: opts.cwd
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
program.command("commit-task-repos").description(
|
|
2027
|
+
"\u6267\u884C\u7ED3\u675F\u6536\u5C3E\uFF1A/home/repository.json \u5404\u4ED3\u6709\u53D8\u66F4\u624D commit\uFF0C\u518D push\uFF08\u65E0\u53D8\u66F4\u4E0D\u7A7A\u63D0\u4EA4\uFF09"
|
|
2028
|
+
).option("--cwd <dir>", "\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option(
|
|
2029
|
+
"-m, --message <msg>",
|
|
2030
|
+
"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"
|
|
2031
|
+
).action(async (opts) => {
|
|
2032
|
+
await runCommitTaskReposCli({
|
|
2033
|
+
cwd: opts.cwd,
|
|
2034
|
+
message: opts.message
|
|
2035
|
+
});
|
|
2092
2036
|
});
|
|
2093
2037
|
program.command("connect").description(
|
|
2094
2038
|
"\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"
|
|
2095
|
-
).option(
|
|
2096
|
-
|
|
2039
|
+
).option(
|
|
2040
|
+
"--server <url>",
|
|
2041
|
+
"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"
|
|
2042
|
+
).option(
|
|
2043
|
+
"--socket-server <url>",
|
|
2044
|
+
"Agent WebSocket \u6839\u5730\u5740\uFF08\u8986\u76D6 config / APM_SOCKET_SERVER\uFF09"
|
|
2045
|
+
).action(async (opts) => {
|
|
2046
|
+
await runConnect({
|
|
2047
|
+
server: opts.server,
|
|
2048
|
+
socketServer: opts.socketServer
|
|
2049
|
+
});
|
|
2097
2050
|
});
|
|
2098
2051
|
return program;
|
|
2099
2052
|
}
|