ai-project-manage-cli 8.1.21 → 8.1.23
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 +0 -4
- package/dist/index.js +542 -656
- 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 +57 -0
- package/dist/scripts/push_code.sh +21 -0
- package/dist/worker-script.js +2592 -3120
- package/package.json +2 -2
- 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
|
@@ -289,22 +289,6 @@ var requestConfig = {
|
|
|
289
289
|
method: "POST",
|
|
290
290
|
path: "/cli/webide/pull-requests/merge"
|
|
291
291
|
}),
|
|
292
|
-
getProjectDocumentManifest: defineEndpoint({
|
|
293
|
-
method: "GET",
|
|
294
|
-
path: "/cli/project-documents/manifest"
|
|
295
|
-
}),
|
|
296
|
-
listProjectDocuments: defineEndpoint({
|
|
297
|
-
method: "GET",
|
|
298
|
-
path: "/cli/project-documents"
|
|
299
|
-
}),
|
|
300
|
-
upsertProjectDocument: defineEndpoint({
|
|
301
|
-
method: "PUT",
|
|
302
|
-
path: "/cli/project-documents/upsert"
|
|
303
|
-
}),
|
|
304
|
-
removeProjectDocument: defineEndpoint({
|
|
305
|
-
method: "DELETE",
|
|
306
|
-
path: "/cli/project-documents"
|
|
307
|
-
}),
|
|
308
292
|
getApmLogStorage: defineEndpoint({
|
|
309
293
|
method: "GET",
|
|
310
294
|
path: "/cli/apm-log-storage"
|
|
@@ -423,9 +407,6 @@ async function ensureLoggedConfig() {
|
|
|
423
407
|
}
|
|
424
408
|
return cfg;
|
|
425
409
|
}
|
|
426
|
-
async function ensureDirExists(dir) {
|
|
427
|
-
mkdirSync2(dir, { recursive: true });
|
|
428
|
-
}
|
|
429
410
|
async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
|
|
430
411
|
const dir = workspaceApmDir(cwd);
|
|
431
412
|
const fsDir = toFsPath(dir);
|
|
@@ -838,360 +819,11 @@ async function runUpdateSkills() {
|
|
|
838
819
|
await syncWorkspaceSkills(resolveWorkdirPath());
|
|
839
820
|
}
|
|
840
821
|
|
|
841
|
-
// src/utils/project-documents.ts
|
|
842
|
-
import {
|
|
843
|
-
existsSync as existsSync5,
|
|
844
|
-
readdirSync as readdirSync3,
|
|
845
|
-
readFileSync as readFileSync4,
|
|
846
|
-
rmSync,
|
|
847
|
-
writeFileSync as writeFileSync3
|
|
848
|
-
} from "fs";
|
|
849
|
-
import { createHash } from "crypto";
|
|
850
|
-
import { dirname as dirname2, join as join6, relative, sep } from "path";
|
|
851
|
-
|
|
852
|
-
// src/utils/git.ts
|
|
853
|
-
import { execFile as execFile2 } from "child_process";
|
|
854
|
-
import { promisify as promisify2 } from "util";
|
|
855
|
-
|
|
856
|
-
// src/utils/git-remote.ts
|
|
857
|
-
import { execFile } from "child_process";
|
|
858
|
-
import { promisify } from "util";
|
|
859
|
-
var execFileAsync = promisify(execFile);
|
|
860
|
-
|
|
861
|
-
// src/utils/git.ts
|
|
862
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
863
|
-
function gitExecEnv() {
|
|
864
|
-
const existing = process.env.GIT_SSH_COMMAND?.trim();
|
|
865
|
-
const acceptNew = "-o StrictHostKeyChecking=accept-new";
|
|
866
|
-
let gitSshCommand;
|
|
867
|
-
if (!existing) {
|
|
868
|
-
gitSshCommand = `ssh ${acceptNew}`;
|
|
869
|
-
} else if (/\bStrictHostKeyChecking=/.test(existing)) {
|
|
870
|
-
gitSshCommand = existing;
|
|
871
|
-
} else {
|
|
872
|
-
gitSshCommand = `${existing} ${acceptNew}`;
|
|
873
|
-
}
|
|
874
|
-
return { ...process.env, GIT_SSH_COMMAND: gitSshCommand };
|
|
875
|
-
}
|
|
876
|
-
async function execGit(cwd, args, quiet = false) {
|
|
877
|
-
try {
|
|
878
|
-
const { stdout, stderr } = await execFileAsync2("git", args, {
|
|
879
|
-
cwd,
|
|
880
|
-
env: gitExecEnv(),
|
|
881
|
-
encoding: "utf8",
|
|
882
|
-
maxBuffer: 10 * 1024 * 1024
|
|
883
|
-
});
|
|
884
|
-
if (!quiet && stderr.trim()) {
|
|
885
|
-
process.stderr.write(stderr);
|
|
886
|
-
}
|
|
887
|
-
return stdout;
|
|
888
|
-
} catch (err) {
|
|
889
|
-
const e = err;
|
|
890
|
-
const detail = (e.stderr ?? e.message ?? String(err)).trim();
|
|
891
|
-
throw new Error(
|
|
892
|
-
`[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
async function getCurrentBranch(cwd) {
|
|
897
|
-
return (await execGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
|
|
898
|
-
}
|
|
899
|
-
async function resolveGitRepoRoot(cwd) {
|
|
900
|
-
return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
|
|
901
|
-
}
|
|
902
|
-
var FEATURE_BRANCH_RE = /^feat\/task-(.+)$/;
|
|
903
|
-
function taskIdFromFeatureBranch(branch) {
|
|
904
|
-
const name = branch.trim().replace(/^origin\//, "");
|
|
905
|
-
const match = name.match(FEATURE_BRANCH_RE);
|
|
906
|
-
const id = match?.[1]?.trim() ?? "";
|
|
907
|
-
return id || null;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// src/utils/project-documents.ts
|
|
911
|
-
var MANIFEST_FILE = "manifest.json";
|
|
912
|
-
function normalizeProjectIdForPath(projectId) {
|
|
913
|
-
const id = projectId.trim();
|
|
914
|
-
if (!id) {
|
|
915
|
-
throw new Error("projectId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
916
|
-
}
|
|
917
|
-
if (id.includes("..") || id.includes("/") || id.includes("\\") || id.includes("\0")) {
|
|
918
|
-
throw new Error(`\u975E\u6CD5 projectId: ${projectId}`);
|
|
919
|
-
}
|
|
920
|
-
return id;
|
|
921
|
-
}
|
|
922
|
-
function projectDocumentsDir(apmRoot, projectId) {
|
|
923
|
-
const id = normalizeProjectIdForPath(projectId);
|
|
924
|
-
return join6(apmRoot ?? workspaceApmDir(), "project", id);
|
|
925
|
-
}
|
|
926
|
-
function projectDocumentLocalPath(apmRoot, projectId, documentPath) {
|
|
927
|
-
const normalized = normalizeLocalDocumentPath(documentPath);
|
|
928
|
-
return join6(
|
|
929
|
-
projectDocumentsDir(apmRoot, projectId),
|
|
930
|
-
...normalized.split("/")
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
function normalizeLocalDocumentPath(path) {
|
|
934
|
-
const trimmed = path.trim().replace(/\\/g, "/");
|
|
935
|
-
if (!trimmed || trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) {
|
|
936
|
-
throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path}`);
|
|
937
|
-
}
|
|
938
|
-
const segments = trimmed.split("/").filter(Boolean);
|
|
939
|
-
if (segments.some((segment) => segment === ".." || segment === ".")) {
|
|
940
|
-
throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path}`);
|
|
941
|
-
}
|
|
942
|
-
return segments.join("/");
|
|
943
|
-
}
|
|
944
|
-
function hashLocalFileContent(content) {
|
|
945
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
946
|
-
}
|
|
947
|
-
function readLocalManifest(apmRoot, projectId) {
|
|
948
|
-
const manifestPath = join6(
|
|
949
|
-
projectDocumentsDir(apmRoot, projectId),
|
|
950
|
-
MANIFEST_FILE
|
|
951
|
-
);
|
|
952
|
-
if (!existsSync5(manifestPath)) {
|
|
953
|
-
return null;
|
|
954
|
-
}
|
|
955
|
-
try {
|
|
956
|
-
return JSON.parse(
|
|
957
|
-
readFileSync4(manifestPath, "utf8")
|
|
958
|
-
);
|
|
959
|
-
} catch {
|
|
960
|
-
return null;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
function listLocalDocumentPaths(apmRoot, projectId) {
|
|
964
|
-
const root = projectDocumentsDir(apmRoot, projectId);
|
|
965
|
-
if (!existsSync5(root)) {
|
|
966
|
-
return [];
|
|
967
|
-
}
|
|
968
|
-
const paths = [];
|
|
969
|
-
const walk = (dir) => {
|
|
970
|
-
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
971
|
-
const abs = join6(dir, entry.name);
|
|
972
|
-
if (entry.isDirectory()) {
|
|
973
|
-
walk(abs);
|
|
974
|
-
continue;
|
|
975
|
-
}
|
|
976
|
-
if (entry.isFile() && entry.name === MANIFEST_FILE) {
|
|
977
|
-
continue;
|
|
978
|
-
}
|
|
979
|
-
const rel = relative(root, abs).split(sep).join("/");
|
|
980
|
-
paths.push(rel);
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
walk(root);
|
|
984
|
-
return paths.sort();
|
|
985
|
-
}
|
|
986
|
-
function diffManifestPaths(remote, local) {
|
|
987
|
-
const remoteMap = new Map(
|
|
988
|
-
(remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
|
|
989
|
-
);
|
|
990
|
-
const localMap = new Map(
|
|
991
|
-
(local?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
|
|
992
|
-
);
|
|
993
|
-
const download = [];
|
|
994
|
-
for (const [path, hash] of remoteMap) {
|
|
995
|
-
if (localMap.get(path) !== hash) {
|
|
996
|
-
download.push(path);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
const deleteLocal = [];
|
|
1000
|
-
for (const path of localMap.keys()) {
|
|
1001
|
-
if (!remoteMap.has(path)) {
|
|
1002
|
-
deleteLocal.push(path);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return { download, deleteLocal };
|
|
1006
|
-
}
|
|
1007
|
-
async function resolveProjectIdForSync(api, workdirPath, options) {
|
|
1008
|
-
const explicit = options?.projectId?.trim();
|
|
1009
|
-
if (explicit) {
|
|
1010
|
-
return { projectId: explicit, diagnostic: null };
|
|
1011
|
-
}
|
|
1012
|
-
try {
|
|
1013
|
-
const gitRoot = await resolveGitRepoRoot(workdirPath);
|
|
1014
|
-
const current = await getCurrentBranch(gitRoot);
|
|
1015
|
-
const taskId = taskIdFromFeatureBranch(current);
|
|
1016
|
-
if (!taskId) {
|
|
1017
|
-
return {
|
|
1018
|
-
projectId: null,
|
|
1019
|
-
diagnostic: `\u5F53\u524D\u5206\u652F ${current} \u4E0D\u662F feat/task-*\uFF0C\u65E0\u6CD5\u89E3\u6790\u6240\u5C5E\u9879\u76EE\u3002
|
|
1020
|
-
\u8BF7\u5728\u7279\u6027\u5206\u652F\u4E0A\u6267\u884C\uFF0C\u6216\u663E\u5F0F\u6307\u5B9A projectId\u3002`
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
const baseline = await api.cli.projectBaseBranch({ taskId });
|
|
1024
|
-
const projectId = baseline.projectId?.trim() || null;
|
|
1025
|
-
if (!projectId) {
|
|
1026
|
-
return {
|
|
1027
|
-
projectId: null,
|
|
1028
|
-
diagnostic: `\u4EFB\u52A1 ${taskId} \u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
return { projectId, diagnostic: null };
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1034
|
-
return {
|
|
1035
|
-
projectId: null,
|
|
1036
|
-
diagnostic: detail.replace(/^\[apm\]\s*/, "")
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
async function syncProjectDocumentsPull(workdirPath, apmDir, options) {
|
|
1041
|
-
const empty = {
|
|
1042
|
-
synced: false,
|
|
1043
|
-
projectId: null,
|
|
1044
|
-
downloaded: 0,
|
|
1045
|
-
deleted: 0
|
|
1046
|
-
};
|
|
1047
|
-
const cfg = await tryReadApmConfig();
|
|
1048
|
-
if (!cfg || !resolveApiKey(cfg)) {
|
|
1049
|
-
console.log(
|
|
1050
|
-
"[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"
|
|
1051
|
-
);
|
|
1052
|
-
return empty;
|
|
1053
|
-
}
|
|
1054
|
-
const api = createApmApiClient(cfg);
|
|
1055
|
-
const { projectId, diagnostic } = await resolveProjectIdForSync(
|
|
1056
|
-
api,
|
|
1057
|
-
workdirPath,
|
|
1058
|
-
options
|
|
1059
|
-
);
|
|
1060
|
-
if (!projectId) {
|
|
1061
|
-
console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
|
|
1062
|
-
${diagnostic ?? ""}`);
|
|
1063
|
-
return empty;
|
|
1064
|
-
}
|
|
1065
|
-
const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
|
|
1066
|
-
const docsDir = projectDocumentsDir(targetApmDir, projectId);
|
|
1067
|
-
await ensureDirExists(docsDir);
|
|
1068
|
-
const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest(
|
|
1069
|
-
{ projectId }
|
|
1070
|
-
);
|
|
1071
|
-
if (!remoteManifest) {
|
|
1072
|
-
console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
|
|
1073
|
-
return { ...empty, projectId };
|
|
1074
|
-
}
|
|
1075
|
-
const localManifest = readLocalManifest(targetApmDir, projectId);
|
|
1076
|
-
const { download, deleteLocal } = diffManifestPaths(
|
|
1077
|
-
remoteManifest,
|
|
1078
|
-
localManifest
|
|
1079
|
-
);
|
|
1080
|
-
let downloaded = 0;
|
|
1081
|
-
if (download.length > 0) {
|
|
1082
|
-
const { list } = await api.cli.listProjectDocuments({
|
|
1083
|
-
projectId,
|
|
1084
|
-
paths: download.join(",")
|
|
1085
|
-
});
|
|
1086
|
-
for (const doc of list) {
|
|
1087
|
-
const absPath = toFsPath(
|
|
1088
|
-
projectDocumentLocalPath(targetApmDir, projectId, doc.path)
|
|
1089
|
-
);
|
|
1090
|
-
await ensureDirExists(dirname2(absPath));
|
|
1091
|
-
writeFileSync3(absPath, doc.content, "utf8");
|
|
1092
|
-
downloaded += 1;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
let deleted = 0;
|
|
1096
|
-
for (const path of deleteLocal) {
|
|
1097
|
-
const absPath = toFsPath(
|
|
1098
|
-
projectDocumentLocalPath(targetApmDir, projectId, path)
|
|
1099
|
-
);
|
|
1100
|
-
if (existsSync5(absPath)) {
|
|
1101
|
-
rmSync(absPath, { force: true });
|
|
1102
|
-
deleted += 1;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
writeFileSync3(
|
|
1106
|
-
toFsPath(join6(docsDir, MANIFEST_FILE)),
|
|
1107
|
-
`${JSON.stringify(remoteManifest, null, 2)}
|
|
1108
|
-
`,
|
|
1109
|
-
"utf8"
|
|
1110
|
-
);
|
|
1111
|
-
console.log(
|
|
1112
|
-
`[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: projectId=${projectId} \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted} \u2192 .apm/project/${projectId}/`
|
|
1113
|
-
);
|
|
1114
|
-
return {
|
|
1115
|
-
synced: true,
|
|
1116
|
-
projectId,
|
|
1117
|
-
downloaded,
|
|
1118
|
-
deleted
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot, options) {
|
|
1122
|
-
const api = createApmApiClient(cfg);
|
|
1123
|
-
const { projectId } = await resolveProjectIdForSync(
|
|
1124
|
-
api,
|
|
1125
|
-
workdirPath,
|
|
1126
|
-
options
|
|
1127
|
-
);
|
|
1128
|
-
if (!projectId) {
|
|
1129
|
-
return 0;
|
|
1130
|
-
}
|
|
1131
|
-
const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
|
|
1132
|
-
const localPaths = listLocalDocumentPaths(targetApmDir, projectId);
|
|
1133
|
-
if (localPaths.length === 0) {
|
|
1134
|
-
console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
|
|
1135
|
-
return 0;
|
|
1136
|
-
}
|
|
1137
|
-
const remoteManifest = (await api.cli.getProjectDocumentManifest({ projectId })).manifest ?? null;
|
|
1138
|
-
const remoteHashByPath = new Map(
|
|
1139
|
-
(remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
|
|
1140
|
-
);
|
|
1141
|
-
const remoteDescriptionByPath = new Map(
|
|
1142
|
-
(remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.description])
|
|
1143
|
-
);
|
|
1144
|
-
let synced = 0;
|
|
1145
|
-
for (const path of localPaths) {
|
|
1146
|
-
const absPath = toFsPath(
|
|
1147
|
-
projectDocumentLocalPath(targetApmDir, projectId, path)
|
|
1148
|
-
);
|
|
1149
|
-
const content = readFileSync4(absPath, "utf8");
|
|
1150
|
-
const contentHash = hashLocalFileContent(content);
|
|
1151
|
-
if (remoteHashByPath.get(path) === contentHash) {
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
await api.cli.upsertProjectDocument({
|
|
1155
|
-
projectId,
|
|
1156
|
-
path,
|
|
1157
|
-
content,
|
|
1158
|
-
description: remoteDescriptionByPath.get(path) ?? void 0
|
|
1159
|
-
});
|
|
1160
|
-
synced += 1;
|
|
1161
|
-
console.log(`[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: ${path}`);
|
|
1162
|
-
}
|
|
1163
|
-
if (synced === 0) {
|
|
1164
|
-
console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
|
|
1165
|
-
}
|
|
1166
|
-
return synced;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// src/commands/sync-project-documents.ts
|
|
1170
|
-
async function runSyncProjectDocuments(options) {
|
|
1171
|
-
const workdir = resolveWorkdirPath();
|
|
1172
|
-
const apmRoot = workspaceApmDir(workdir);
|
|
1173
|
-
const doPush = Boolean(options.push);
|
|
1174
|
-
const doPull = Boolean(options.pull) || !doPush;
|
|
1175
|
-
if (doPull) {
|
|
1176
|
-
await syncProjectDocumentsPull(workdir, apmRoot);
|
|
1177
|
-
}
|
|
1178
|
-
if (doPush) {
|
|
1179
|
-
const cfg = await tryReadApmConfig();
|
|
1180
|
-
if (!cfg) {
|
|
1181
|
-
throw new Error("[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8BF7\u5148\u6267\u884C apm login");
|
|
1182
|
-
}
|
|
1183
|
-
await syncProjectDocumentsPush(cfg, workdir, apmRoot);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
822
|
// src/commands/connect.ts
|
|
1188
823
|
import WebSocket from "ws";
|
|
1189
|
-
import { setTimeout as
|
|
824
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
1190
825
|
|
|
1191
826
|
// src/ws/protocol.ts
|
|
1192
|
-
function isWebIdeStartProjectTarget(value) {
|
|
1193
|
-
return value === "frontend" || value === "backend" || value === "mobile";
|
|
1194
|
-
}
|
|
1195
827
|
function nonEmptyString(v) {
|
|
1196
828
|
return typeof v === "string" && v.trim().length > 0;
|
|
1197
829
|
}
|
|
@@ -1202,18 +834,6 @@ function parseAgentWsMessage(raw) {
|
|
|
1202
834
|
return null;
|
|
1203
835
|
}
|
|
1204
836
|
}
|
|
1205
|
-
function serializeAgentWsMessage(msg) {
|
|
1206
|
-
return JSON.stringify(msg);
|
|
1207
|
-
}
|
|
1208
|
-
function validateHeartbeat(o) {
|
|
1209
|
-
if (o.type !== "heartbeat") {
|
|
1210
|
-
return { ok: false, reason: "\u671F\u671B heartbeat" };
|
|
1211
|
-
}
|
|
1212
|
-
if (!nonEmptyString(o.userId)) {
|
|
1213
|
-
return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
|
|
1214
|
-
}
|
|
1215
|
-
return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
|
|
1216
|
-
}
|
|
1217
837
|
function validateWebIdeMessagePush(o) {
|
|
1218
838
|
if (o.type !== "webide-message") {
|
|
1219
839
|
return { ok: false, reason: "\u671F\u671B webide-message" };
|
|
@@ -1292,89 +912,21 @@ function validateSteer(o) {
|
|
|
1292
912
|
}
|
|
1293
913
|
};
|
|
1294
914
|
}
|
|
1295
|
-
function
|
|
1296
|
-
if (o.type !== "webide-stop-terminals") {
|
|
1297
|
-
return { ok: false, reason: "\u671F\u671B webide-stop-terminals" };
|
|
1298
|
-
}
|
|
1299
|
-
if (!nonEmptyString(o.taskId)) {
|
|
1300
|
-
return { ok: false, reason: "webide-stop-terminals \u7F3A\u5C11 taskId" };
|
|
1301
|
-
}
|
|
1302
|
-
return {
|
|
1303
|
-
ok: true,
|
|
1304
|
-
data: {
|
|
1305
|
-
type: "webide-stop-terminals",
|
|
1306
|
-
taskId: o.taskId.trim()
|
|
1307
|
-
}
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
function validateWebIdeStartProjectPush(o) {
|
|
1311
|
-
if (o.type !== "webide-start-project") {
|
|
1312
|
-
return { ok: false, reason: "\u671F\u671B webide-start-project" };
|
|
1313
|
-
}
|
|
1314
|
-
if (!nonEmptyString(o.taskId)) {
|
|
1315
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 taskId" };
|
|
1316
|
-
}
|
|
1317
|
-
if (!nonEmptyString(o.messageId)) {
|
|
1318
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 messageId" };
|
|
1319
|
-
}
|
|
1320
|
-
if (!isWebIdeStartProjectTarget(o.target)) {
|
|
1321
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11\u6709\u6548 target" };
|
|
1322
|
-
}
|
|
1323
|
-
if (!nonEmptyString(o.content)) {
|
|
1324
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 content" };
|
|
1325
|
-
}
|
|
1326
|
-
if (!nonEmptyString(o.model)) {
|
|
1327
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 model" };
|
|
1328
|
-
}
|
|
1329
|
-
if (!nonEmptyString(o.apiKey)) {
|
|
1330
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 apiKey" };
|
|
1331
|
-
}
|
|
1332
|
-
if (!nonEmptyString(o.workdir)) {
|
|
1333
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 workdir" };
|
|
1334
|
-
}
|
|
1335
|
-
if (!nonEmptyString(o.user)) {
|
|
1336
|
-
return { ok: false, reason: "webide-start-project \u7F3A\u5C11 user" };
|
|
1337
|
-
}
|
|
1338
|
-
return {
|
|
1339
|
-
ok: true,
|
|
1340
|
-
data: {
|
|
1341
|
-
type: "webide-start-project",
|
|
1342
|
-
taskId: o.taskId.trim(),
|
|
1343
|
-
messageId: o.messageId.trim(),
|
|
1344
|
-
target: o.target,
|
|
1345
|
-
restart: o.restart === true,
|
|
1346
|
-
content: o.content,
|
|
1347
|
-
model: o.model.trim(),
|
|
1348
|
-
apiKey: o.apiKey.trim(),
|
|
1349
|
-
workdir: o.workdir.trim(),
|
|
1350
|
-
user: o.user.trim()
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
function validateAgentWsMessage(value, kind) {
|
|
915
|
+
function validateAgentWsMessage(value, _kind = "outbound") {
|
|
1355
916
|
if (typeof value !== "object" || value === null) {
|
|
1356
917
|
return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
|
|
1357
918
|
}
|
|
1358
919
|
const o = value;
|
|
1359
920
|
const type = o.type;
|
|
1360
|
-
if (type !== "
|
|
921
|
+
if (type !== "webide-message" && type !== "cancel" && type !== "steer") {
|
|
1361
922
|
return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
|
|
1362
923
|
}
|
|
1363
|
-
if (kind === "heartbeat" || type === "heartbeat") {
|
|
1364
|
-
return validateHeartbeat(o);
|
|
1365
|
-
}
|
|
1366
924
|
if (type === "cancel") {
|
|
1367
925
|
return validateCancel(o);
|
|
1368
926
|
}
|
|
1369
927
|
if (type === "steer") {
|
|
1370
928
|
return validateSteer(o);
|
|
1371
929
|
}
|
|
1372
|
-
if (type === "webide-start-project") {
|
|
1373
|
-
return validateWebIdeStartProjectPush(o);
|
|
1374
|
-
}
|
|
1375
|
-
if (type === "webide-stop-terminals") {
|
|
1376
|
-
return validateWebIdeStopTerminals(o);
|
|
1377
|
-
}
|
|
1378
930
|
return validateWebIdeMessagePush(o);
|
|
1379
931
|
}
|
|
1380
932
|
|
|
@@ -1409,7 +961,7 @@ function handleSteerAction(payload, ctx) {
|
|
|
1409
961
|
}
|
|
1410
962
|
|
|
1411
963
|
// src/commands/connect/core/worker/worker.ts
|
|
1412
|
-
import { dirname as
|
|
964
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
1413
965
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1414
966
|
import { Worker } from "node:worker_threads";
|
|
1415
967
|
|
|
@@ -1435,8 +987,8 @@ function resolveWebIdeWorkerHeapMb(env = process.env, totalMemoryBytes = totalme
|
|
|
1435
987
|
}
|
|
1436
988
|
|
|
1437
989
|
// src/commands/connect/core/worker/worker.ts
|
|
1438
|
-
var workerFile =
|
|
1439
|
-
|
|
990
|
+
var workerFile = join6(
|
|
991
|
+
dirname2(fileURLToPath2(import.meta.url)),
|
|
1440
992
|
"worker-script.js"
|
|
1441
993
|
);
|
|
1442
994
|
var ActionWorker = class {
|
|
@@ -1455,8 +1007,8 @@ var ActionWorker = class {
|
|
|
1455
1007
|
}
|
|
1456
1008
|
this.msg = msg;
|
|
1457
1009
|
this.apmClient = createApmApiClient(cfg);
|
|
1458
|
-
this.promise = new Promise((
|
|
1459
|
-
this.resolve =
|
|
1010
|
+
this.promise = new Promise((resolve5, reject) => {
|
|
1011
|
+
this.resolve = resolve5;
|
|
1460
1012
|
this.reject = reject;
|
|
1461
1013
|
});
|
|
1462
1014
|
this.worker = new Worker(workerFile, {
|
|
@@ -1584,148 +1136,9 @@ function handleWebIdeMessageAction(payload, ctx) {
|
|
|
1584
1136
|
trackActionTask(ctx, task);
|
|
1585
1137
|
}
|
|
1586
1138
|
|
|
1587
|
-
// src/commands/connect/actions/start-project.ts
|
|
1588
|
-
function handleStartProjectAction(payload, ctx) {
|
|
1589
|
-
const perMessageController = new AbortController();
|
|
1590
|
-
ctx.activeRuns.set(payload.messageId, perMessageController);
|
|
1591
|
-
if (ctx.pendingCancels.has(payload.messageId)) {
|
|
1592
|
-
ctx.activeRuns.delete(payload.messageId);
|
|
1593
|
-
ctx.pendingCancels.delete(payload.messageId);
|
|
1594
|
-
const cancelTask = (async () => {
|
|
1595
|
-
try {
|
|
1596
|
-
const api = createApmApiClient(ctx.cfg);
|
|
1597
|
-
await api.cli.webideUpdateMessageStatus({
|
|
1598
|
-
id: payload.messageId,
|
|
1599
|
-
status: "CANCELLED"
|
|
1600
|
-
});
|
|
1601
|
-
} catch (err) {
|
|
1602
|
-
console.error(
|
|
1603
|
-
`[apm] webide-start-project \u9884\u53D6\u6D88\u5199\u7EC8\u6001\u5931\u8D25 messageId=${payload.messageId}:`,
|
|
1604
|
-
err instanceof Error ? err.message : err
|
|
1605
|
-
);
|
|
1606
|
-
}
|
|
1607
|
-
})();
|
|
1608
|
-
trackActionTask(ctx, cancelTask);
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
const task = (async () => {
|
|
1612
|
-
const handle = new ActionWorker(payload);
|
|
1613
|
-
registerRunSteer(ctx, payload.messageId, handle);
|
|
1614
|
-
const onAbort = () => handle.cancel(payload.messageId);
|
|
1615
|
-
perMessageController.signal.addEventListener("abort", onAbort, {
|
|
1616
|
-
once: true
|
|
1617
|
-
});
|
|
1618
|
-
try {
|
|
1619
|
-
await handle.done;
|
|
1620
|
-
} finally {
|
|
1621
|
-
perMessageController.signal.removeEventListener("abort", onAbort);
|
|
1622
|
-
unregisterRunSteer(ctx, payload.messageId);
|
|
1623
|
-
ctx.activeRuns.delete(payload.messageId);
|
|
1624
|
-
}
|
|
1625
|
-
})();
|
|
1626
|
-
trackActionTask(ctx, task);
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// src/commands/connect/mcps/pty-session-mcp.ts
|
|
1630
|
-
var DEFAULT_PTY_SESSION_PORT = "7788";
|
|
1631
|
-
function resolvePtySessionBaseUrl(env = process.env) {
|
|
1632
|
-
const explicit = env.LOCAL_TERMINAL_URL?.trim();
|
|
1633
|
-
if (explicit) return explicit.replace(/\/+$/, "");
|
|
1634
|
-
const port = env.LOCAL_TERMINAL_PORT?.trim() || DEFAULT_PTY_SESSION_PORT;
|
|
1635
|
-
return `http://127.0.0.1:${port}`;
|
|
1636
|
-
}
|
|
1637
|
-
function asSessionList(data) {
|
|
1638
|
-
if (Array.isArray(data)) return data;
|
|
1639
|
-
if (data && typeof data === "object") {
|
|
1640
|
-
const sessions = data.sessions;
|
|
1641
|
-
if (Array.isArray(sessions)) return sessions;
|
|
1642
|
-
}
|
|
1643
|
-
return [];
|
|
1644
|
-
}
|
|
1645
|
-
async function requestPtyJson(url, token, init) {
|
|
1646
|
-
const res = await fetch(url, {
|
|
1647
|
-
...init,
|
|
1648
|
-
headers: {
|
|
1649
|
-
Authorization: `Bearer ${token}`,
|
|
1650
|
-
...init?.headers ?? {}
|
|
1651
|
-
}
|
|
1652
|
-
});
|
|
1653
|
-
if (!res.ok) {
|
|
1654
|
-
throw new Error(`pty-session HTTP ${res.status}`);
|
|
1655
|
-
}
|
|
1656
|
-
try {
|
|
1657
|
-
return await res.json();
|
|
1658
|
-
} catch {
|
|
1659
|
-
return null;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
async function killAllPtySessions(cfg) {
|
|
1663
|
-
const token = resolveApiKey(cfg);
|
|
1664
|
-
if (!token) return 0;
|
|
1665
|
-
const baseUrl = resolvePtySessionBaseUrl();
|
|
1666
|
-
let body;
|
|
1667
|
-
try {
|
|
1668
|
-
body = await requestPtyJson(`${baseUrl}/sessions`, token);
|
|
1669
|
-
} catch (err) {
|
|
1670
|
-
console.warn(
|
|
1671
|
-
"[apm] \u5217\u51FA PTY \u4F1A\u8BDD\u5931\u8D25:",
|
|
1672
|
-
err instanceof Error ? err.message : err
|
|
1673
|
-
);
|
|
1674
|
-
return 0;
|
|
1675
|
-
}
|
|
1676
|
-
const payload = body;
|
|
1677
|
-
if (payload && typeof payload === "object" && payload.ok === false) {
|
|
1678
|
-
return 0;
|
|
1679
|
-
}
|
|
1680
|
-
const data = payload && typeof payload === "object" && "data" in payload ? payload.data : body;
|
|
1681
|
-
const sessions = asSessionList(data);
|
|
1682
|
-
let killed = 0;
|
|
1683
|
-
for (const session of sessions) {
|
|
1684
|
-
const id = session.id?.trim();
|
|
1685
|
-
if (!id) continue;
|
|
1686
|
-
if (session.running === false) continue;
|
|
1687
|
-
try {
|
|
1688
|
-
await requestPtyJson(
|
|
1689
|
-
`${baseUrl}/sessions/${encodeURIComponent(id)}`,
|
|
1690
|
-
token,
|
|
1691
|
-
{
|
|
1692
|
-
method: "DELETE"
|
|
1693
|
-
}
|
|
1694
|
-
);
|
|
1695
|
-
killed += 1;
|
|
1696
|
-
} catch (err) {
|
|
1697
|
-
console.warn(
|
|
1698
|
-
`[apm] \u7ED3\u675F PTY \u4F1A\u8BDD\u5931\u8D25 id=${id}:`,
|
|
1699
|
-
err instanceof Error ? err.message : err
|
|
1700
|
-
);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
return killed;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// src/commands/connect/actions/stop-terminals.ts
|
|
1707
|
-
function handleStopTerminalsAction(payload, ctx) {
|
|
1708
|
-
const task = (async () => {
|
|
1709
|
-
try {
|
|
1710
|
-
const killed = await killAllPtySessions(ctx.cfg);
|
|
1711
|
-
console.log(
|
|
1712
|
-
`[apm] webide-stop-terminals taskId=${payload.taskId} killed=${killed}`
|
|
1713
|
-
);
|
|
1714
|
-
} catch (err) {
|
|
1715
|
-
console.warn(
|
|
1716
|
-
`[apm] webide-stop-terminals \u5931\u8D25 taskId=${payload.taskId}:`,
|
|
1717
|
-
err instanceof Error ? err.message : err
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
})();
|
|
1721
|
-
trackActionTask(ctx, task);
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
1139
|
// src/commands/connect/actions/index.ts
|
|
1725
1140
|
function dispatchConnectAction(payload, ctx) {
|
|
1726
1141
|
switch (payload.type) {
|
|
1727
|
-
case "heartbeat":
|
|
1728
|
-
return;
|
|
1729
1142
|
case "cancel":
|
|
1730
1143
|
handleCancelAction(payload, ctx);
|
|
1731
1144
|
return;
|
|
@@ -1735,25 +1148,22 @@ function dispatchConnectAction(payload, ctx) {
|
|
|
1735
1148
|
case "webide-message":
|
|
1736
1149
|
handleWebIdeMessageAction(payload, ctx);
|
|
1737
1150
|
return;
|
|
1738
|
-
case "webide-start-project":
|
|
1739
|
-
handleStartProjectAction(payload, ctx);
|
|
1740
|
-
return;
|
|
1741
|
-
case "webide-stop-terminals":
|
|
1742
|
-
handleStopTerminalsAction(payload, ctx);
|
|
1743
|
-
return;
|
|
1744
1151
|
}
|
|
1745
1152
|
}
|
|
1746
1153
|
|
|
1747
1154
|
// src/commands/connect-lock.ts
|
|
1748
1155
|
import {
|
|
1749
|
-
existsSync as
|
|
1156
|
+
existsSync as existsSync5,
|
|
1750
1157
|
mkdirSync as mkdirSync4,
|
|
1751
|
-
readFileSync as
|
|
1158
|
+
readFileSync as readFileSync4,
|
|
1752
1159
|
unlinkSync as unlinkSync2,
|
|
1753
|
-
writeFileSync as
|
|
1160
|
+
writeFileSync as writeFileSync3
|
|
1754
1161
|
} from "fs";
|
|
1755
|
-
import {
|
|
1756
|
-
|
|
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;
|
|
1757
1167
|
function isProcessAlive(pid) {
|
|
1758
1168
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1759
1169
|
try {
|
|
@@ -1764,9 +1174,9 @@ function isProcessAlive(pid) {
|
|
|
1764
1174
|
}
|
|
1765
1175
|
}
|
|
1766
1176
|
function readConnectLock() {
|
|
1767
|
-
if (!
|
|
1177
|
+
if (!existsSync5(CONNECT_LOCK_PATH)) return null;
|
|
1768
1178
|
try {
|
|
1769
|
-
const raw =
|
|
1179
|
+
const raw = readFileSync4(CONNECT_LOCK_PATH, "utf8");
|
|
1770
1180
|
const parsed = JSON.parse(raw);
|
|
1771
1181
|
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string") {
|
|
1772
1182
|
return null;
|
|
@@ -1788,27 +1198,44 @@ function pruneStaleConnectLock() {
|
|
|
1788
1198
|
} catch {
|
|
1789
1199
|
}
|
|
1790
1200
|
}
|
|
1791
|
-
function
|
|
1792
|
-
pruneStaleConnectLock();
|
|
1793
|
-
const existing = readConnectLock();
|
|
1794
|
-
if (existing && isProcessAlive(existing.pid)) {
|
|
1795
|
-
if (existing.pid === process.pid) {
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
console.error(`[apm] \u5DF2\u6709 apm connect \u5728\u8FD0\u884C (pid=${existing.pid})`);
|
|
1799
|
-
process.exit(1);
|
|
1800
|
-
}
|
|
1201
|
+
function writeConnectLock() {
|
|
1801
1202
|
mkdirSync4(APM_CONFIG_DIR, { recursive: true });
|
|
1802
1203
|
const lock = {
|
|
1803
1204
|
pid: process.pid,
|
|
1804
1205
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1805
1206
|
};
|
|
1806
|
-
|
|
1207
|
+
writeFileSync3(
|
|
1807
1208
|
CONNECT_LOCK_PATH,
|
|
1808
1209
|
JSON.stringify(lock, null, 2) + "\n",
|
|
1809
1210
|
"utf8"
|
|
1810
1211
|
);
|
|
1811
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
|
+
}
|
|
1812
1239
|
function releaseConnectLock() {
|
|
1813
1240
|
const lock = readConnectLock();
|
|
1814
1241
|
if (lock?.pid !== process.pid) return;
|
|
@@ -1819,36 +1246,20 @@ function releaseConnectLock() {
|
|
|
1819
1246
|
}
|
|
1820
1247
|
|
|
1821
1248
|
// src/commands/connect.ts
|
|
1822
|
-
var HEARTBEAT_MS = 3e4;
|
|
1823
1249
|
var RECONNECT_MIN_MS = 1e3;
|
|
1824
1250
|
var RECONNECT_MAX_MS = 6e4;
|
|
1825
1251
|
var SHUTDOWN_DRAIN_MS = 3e3;
|
|
1826
1252
|
function interruptibleSleep(ms, signal) {
|
|
1827
1253
|
if (signal.aborted) return Promise.resolve();
|
|
1828
|
-
return new Promise((
|
|
1829
|
-
const timer = setTimeout(
|
|
1254
|
+
return new Promise((resolve5) => {
|
|
1255
|
+
const timer = setTimeout(resolve5, ms);
|
|
1830
1256
|
const onAbort = () => {
|
|
1831
1257
|
clearTimeout(timer);
|
|
1832
|
-
|
|
1258
|
+
resolve5();
|
|
1833
1259
|
};
|
|
1834
1260
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1835
1261
|
});
|
|
1836
1262
|
}
|
|
1837
|
-
function startHeartbeat(ws, clientMachineId) {
|
|
1838
|
-
const send = () => {
|
|
1839
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1840
|
-
ws.send(
|
|
1841
|
-
serializeAgentWsMessage({
|
|
1842
|
-
type: "heartbeat",
|
|
1843
|
-
userId: clientMachineId
|
|
1844
|
-
})
|
|
1845
|
-
);
|
|
1846
|
-
}
|
|
1847
|
-
};
|
|
1848
|
-
send();
|
|
1849
|
-
const timer = setInterval(send, HEARTBEAT_MS);
|
|
1850
|
-
return () => clearInterval(timer);
|
|
1851
|
-
}
|
|
1852
1263
|
function attachWsHandlers(ws, ctx, onOpen) {
|
|
1853
1264
|
const { isShuttingDown } = ctx;
|
|
1854
1265
|
ws.on("open", () => {
|
|
@@ -1863,9 +1274,6 @@ function attachWsHandlers(ws, ctx, onOpen) {
|
|
|
1863
1274
|
console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
|
|
1864
1275
|
return;
|
|
1865
1276
|
}
|
|
1866
|
-
if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
1277
|
const validated = validateAgentWsMessage(parsed, "outbound");
|
|
1870
1278
|
if (!validated.ok) {
|
|
1871
1279
|
console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
|
|
@@ -1875,15 +1283,13 @@ function attachWsHandlers(ws, ctx, onOpen) {
|
|
|
1875
1283
|
});
|
|
1876
1284
|
}
|
|
1877
1285
|
function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
1878
|
-
return new Promise((
|
|
1286
|
+
return new Promise((resolve5, reject) => {
|
|
1879
1287
|
const ws = new WebSocket(url);
|
|
1880
|
-
let stopHeartbeat;
|
|
1881
1288
|
let settled = false;
|
|
1882
1289
|
const finish = (fn) => {
|
|
1883
1290
|
if (settled) return;
|
|
1884
1291
|
settled = true;
|
|
1885
1292
|
connectionAbort.removeEventListener("abort", onConnectionAbort);
|
|
1886
|
-
stopHeartbeat?.();
|
|
1887
1293
|
fn();
|
|
1888
1294
|
};
|
|
1889
1295
|
const onConnectionAbort = () => {
|
|
@@ -1892,10 +1298,7 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
|
1892
1298
|
}
|
|
1893
1299
|
};
|
|
1894
1300
|
connectionAbort.addEventListener("abort", onConnectionAbort);
|
|
1895
|
-
attachWsHandlers(ws, ctx,
|
|
1896
|
-
stopHeartbeat = startHeartbeat(ws, ctx.clientMachineId);
|
|
1897
|
-
onConnected();
|
|
1898
|
-
});
|
|
1301
|
+
attachWsHandlers(ws, ctx, onConnected);
|
|
1899
1302
|
ws.on("close", (code, reason) => {
|
|
1900
1303
|
console.log(
|
|
1901
1304
|
`[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
|
|
@@ -1904,7 +1307,7 @@ function connectOnce(url, ctx, connectionAbort, onConnected) {
|
|
|
1904
1307
|
finish(() => reject(new Error("shutdown")));
|
|
1905
1308
|
return;
|
|
1906
1309
|
}
|
|
1907
|
-
finish(
|
|
1310
|
+
finish(resolve5);
|
|
1908
1311
|
});
|
|
1909
1312
|
ws.on("error", (err) => {
|
|
1910
1313
|
console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
|
|
@@ -1937,7 +1340,7 @@ async function runConnect(options) {
|
|
|
1937
1340
|
console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
|
|
1938
1341
|
process.exit(1);
|
|
1939
1342
|
}
|
|
1940
|
-
acquireConnectLock();
|
|
1343
|
+
await acquireConnectLock();
|
|
1941
1344
|
process.on("exit", releaseConnectLock);
|
|
1942
1345
|
let shuttingDown = false;
|
|
1943
1346
|
let shutdownPromise = null;
|
|
@@ -1964,11 +1367,11 @@ async function runConnect(options) {
|
|
|
1964
1367
|
lifecycleAbort.abort();
|
|
1965
1368
|
currentConnectionAbort?.abort();
|
|
1966
1369
|
shutdownAbort.abort();
|
|
1370
|
+
releaseConnectLock();
|
|
1967
1371
|
try {
|
|
1968
|
-
await Promise.race([Promise.all(activeTasks),
|
|
1372
|
+
await Promise.race([Promise.all(activeTasks), delay2(drainMs)]);
|
|
1969
1373
|
} catch {
|
|
1970
1374
|
}
|
|
1971
|
-
releaseConnectLock();
|
|
1972
1375
|
process.exit(code);
|
|
1973
1376
|
};
|
|
1974
1377
|
const shutdown = (code = 0) => {
|
|
@@ -2027,6 +1430,461 @@ async function runConnect(options) {
|
|
|
2027
1430
|
}
|
|
2028
1431
|
}
|
|
2029
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 DEFAULT_REPOS_MANIFEST = "/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 resolveReposManifestPath(explicit) {
|
|
1586
|
+
const fromOpt = explicit?.trim() || "";
|
|
1587
|
+
if (fromOpt) return fromOpt;
|
|
1588
|
+
const fromEnv = process.env.APM_REPOS_MANIFEST?.trim() || "";
|
|
1589
|
+
if (fromEnv) return fromEnv;
|
|
1590
|
+
return DEFAULT_REPOS_MANIFEST;
|
|
1591
|
+
}
|
|
1592
|
+
function loadHomeRepositoryEntries(manifestPath) {
|
|
1593
|
+
const label = manifestPath.trim() || DEFAULT_REPOS_MANIFEST;
|
|
1594
|
+
const path = toFsPath(label);
|
|
1595
|
+
if (!existsSync6(path)) {
|
|
1596
|
+
throw new Error(`[apm] \u672A\u627E\u5230\u591A\u4ED3\u6E05\u5355 ${label}`);
|
|
1597
|
+
}
|
|
1598
|
+
let parsed;
|
|
1599
|
+
try {
|
|
1600
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1603
|
+
throw new Error(`[apm] \u89E3\u6790 ${label} \u5931\u8D25: ${detail}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
`[apm] ${label} \u987B\u4E3A\u5BF9\u8C61\uFF1Akey=\u4E0B\u7EA7\u76EE\u5F55\uFF0Cvalue=git \u5730\u5740`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const entries = [];
|
|
1611
|
+
for (const [rawKey, rawVal] of Object.entries(
|
|
1612
|
+
parsed
|
|
1613
|
+
)) {
|
|
1614
|
+
const pathKey = rawKey.trim();
|
|
1615
|
+
if (!isSimpleChildDirName(pathKey)) {
|
|
1616
|
+
throw new Error(
|
|
1617
|
+
`[apm] ${label} \u975E\u6CD5\u76EE\u5F55 key: ${JSON.stringify(rawKey)}\uFF08\u987B\u4E3A\u5F53\u524D\u4E0B\u7EA7\u76EE\u5F55\u540D\uFF09`
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
if (typeof rawVal !== "string" || !rawVal.trim()) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`[apm] ${label}[${pathKey}] \u987B\u4E3A\u975E\u7A7A git \u5730\u5740\u5B57\u7B26\u4E32`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
entries.push({ path: pathKey, remoteUrl: rawVal.trim() });
|
|
1626
|
+
}
|
|
1627
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
1628
|
+
if (entries.length === 0) {
|
|
1629
|
+
throw new Error(`[apm] ${label} \u4E3A\u7A7A\uFF0C\u65E0\u4ED3\u5E93\u53EF\u786E\u8BA4`);
|
|
1630
|
+
}
|
|
1631
|
+
return entries;
|
|
1632
|
+
}
|
|
1633
|
+
function resolveHomeRepositoryAbsolutePaths(workdirInput, entries) {
|
|
1634
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
1635
|
+
return entries.map((entry) => resolve3(join9(workdir, entry.path)));
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/utils/workspace-repos.ts
|
|
1639
|
+
import { basename, dirname as dirname4, join as join10, relative, resolve as resolve4 } from "path";
|
|
1640
|
+
|
|
1641
|
+
// src/utils/git-remote.ts
|
|
1642
|
+
import { execFile as execFile2 } from "child_process";
|
|
1643
|
+
import { promisify as promisify2 } from "util";
|
|
1644
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1645
|
+
|
|
1646
|
+
// src/utils/workspace-repos.ts
|
|
1647
|
+
function toRelativeRepoPath(workdir, repoAbs) {
|
|
1648
|
+
return relative(workdir, repoAbs).replace(/\\/g, "/");
|
|
1649
|
+
}
|
|
1650
|
+
function formatRepoLabel(workdir, repoAbs) {
|
|
1651
|
+
const rel = toRelativeRepoPath(resolveWorkdirPath(workdir), repoAbs);
|
|
1652
|
+
return rel || basename(repoAbs) || repoAbs;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/commands/branch.ts
|
|
1656
|
+
var TASK_BRANCH_PREFIX = "feat/task-";
|
|
1657
|
+
async function syncTaskRepos(workdir, options = {}) {
|
|
1658
|
+
const opts = typeof options === "string" ? { message: options } : options;
|
|
1659
|
+
const workdirPath = resolveWorkdirPath(workdir);
|
|
1660
|
+
const repoRoots = resolveTaskBranchRepoRoots(
|
|
1661
|
+
workdirPath,
|
|
1662
|
+
opts.reposManifest
|
|
1663
|
+
);
|
|
1664
|
+
let committed = 0;
|
|
1665
|
+
let commitSkipped = 0;
|
|
1666
|
+
let pushed = 0;
|
|
1667
|
+
let pushSkipped = 0;
|
|
1668
|
+
const failed = [];
|
|
1669
|
+
const message = opts.message;
|
|
1670
|
+
for (const gitRoot of repoRoots) {
|
|
1671
|
+
const label = formatRepoLabel(workdirPath, gitRoot);
|
|
1672
|
+
try {
|
|
1673
|
+
const didCommit = await commitWorkingTreeIfDirty(gitRoot, message);
|
|
1674
|
+
if (didCommit) {
|
|
1675
|
+
committed += 1;
|
|
1676
|
+
console.log(`[apm] \u5DF2\u63D0\u4EA4 @ ${label}`);
|
|
1677
|
+
} else {
|
|
1678
|
+
commitSkipped += 1;
|
|
1679
|
+
}
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1682
|
+
console.error(`[apm] \u4ED3\u5E93 ${label} \u63D0\u4EA4\u5931\u8D25\uFF08\u7EE7\u7EED\u5176\u5B83\u4ED3\uFF09: ${detail}`);
|
|
1683
|
+
failed.push(label);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
try {
|
|
1687
|
+
const didPush = await pushCurrentBranch(gitRoot);
|
|
1688
|
+
if (didPush) {
|
|
1689
|
+
pushed += 1;
|
|
1690
|
+
console.log(`[apm] \u5DF2 push @ ${label}`);
|
|
1691
|
+
} else {
|
|
1692
|
+
pushSkipped += 1;
|
|
1693
|
+
}
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1696
|
+
console.error(`[apm] \u4ED3\u5E93 ${label} push \u5931\u8D25\uFF08\u7EE7\u7EED\u5176\u5B83\u4ED3\uFF09: ${detail}`);
|
|
1697
|
+
failed.push(label);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1701
|
+
committed,
|
|
1702
|
+
commitSkipped,
|
|
1703
|
+
pushed,
|
|
1704
|
+
pushSkipped,
|
|
1705
|
+
failed,
|
|
1706
|
+
repos: repoRoots
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function branchNameForTask(taskId) {
|
|
1710
|
+
const id = taskId.trim();
|
|
1711
|
+
if (!id) {
|
|
1712
|
+
throw new Error("[apm] \u4EFB\u52A1 ID \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1713
|
+
}
|
|
1714
|
+
if (/[\s/\\]/.test(id)) {
|
|
1715
|
+
throw new Error(
|
|
1716
|
+
"[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"
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
return `${TASK_BRANCH_PREFIX}${id}`;
|
|
1720
|
+
}
|
|
1721
|
+
async function ensureTaskFeatureBranch(branch, projectId, options) {
|
|
1722
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1723
|
+
const gitRoot = await resolveGitRepoRoot(cwd);
|
|
1724
|
+
await ensureFeatureBranch(gitRoot, branch, projectId);
|
|
1725
|
+
return branch;
|
|
1726
|
+
}
|
|
1727
|
+
function resolveTaskBranchRepoRoots(workdirPath, reposManifest) {
|
|
1728
|
+
const manifestPath = resolveReposManifestPath(reposManifest);
|
|
1729
|
+
const entries = loadHomeRepositoryEntries(manifestPath);
|
|
1730
|
+
const roots = resolveHomeRepositoryAbsolutePaths(workdirPath, entries);
|
|
1731
|
+
console.log(
|
|
1732
|
+
`[apm] \u8BFB\u53D6 ${manifestPath} repos=${entries.map((e) => `${e.path}\u2192${e.remoteUrl}`).join(", ")}`
|
|
1733
|
+
);
|
|
1734
|
+
return roots;
|
|
1735
|
+
}
|
|
1736
|
+
async function runTaskBranch(taskId, projectId, options = {}) {
|
|
1737
|
+
const trimmedTaskId = taskId.trim();
|
|
1738
|
+
if (!trimmedTaskId) {
|
|
1739
|
+
throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1740
|
+
}
|
|
1741
|
+
const trimmedProjectId = projectId.trim();
|
|
1742
|
+
if (!trimmedProjectId) {
|
|
1743
|
+
throw new Error("[apm] projectId\uFF08\u9879\u76EE ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1744
|
+
}
|
|
1745
|
+
if (/[\s\\]/.test(trimmedProjectId) || trimmedProjectId.includes("..")) {
|
|
1746
|
+
throw new Error("[apm] \u9879\u76EE ID \u975E\u6CD5");
|
|
1747
|
+
}
|
|
1748
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1749
|
+
const workdirPath = resolveWorkdirPath(cwd);
|
|
1750
|
+
const repoRoots = resolveTaskBranchRepoRoots(
|
|
1751
|
+
workdirPath,
|
|
1752
|
+
options.reposManifest
|
|
1753
|
+
);
|
|
1754
|
+
if (repoRoots.length === 0) {
|
|
1755
|
+
throw new Error("[apm] \u5DE5\u4F5C\u533A\u4ED3\u5E93\u6E05\u5355\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u786E\u8BA4\u7279\u6027\u5206\u652F");
|
|
1756
|
+
}
|
|
1757
|
+
const branch = branchNameForTask(trimmedTaskId);
|
|
1758
|
+
const repoLabels = repoRoots.map(
|
|
1759
|
+
(gitRoot) => formatRepoLabel(workdirPath, gitRoot)
|
|
1760
|
+
);
|
|
1761
|
+
console.log(
|
|
1762
|
+
`[apm] \u591A\u4ED3\u786E\u8BA4\u7279\u6027\u5206\u652F ${branch} projectId=${trimmedProjectId} repos=${repoLabels.join(",")}`
|
|
1763
|
+
);
|
|
1764
|
+
for (const gitRoot of repoRoots) {
|
|
1765
|
+
const label = formatRepoLabel(workdirPath, gitRoot);
|
|
1766
|
+
console.log(
|
|
1767
|
+
`[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (projectId=${trimmedProjectId})`
|
|
1768
|
+
);
|
|
1769
|
+
try {
|
|
1770
|
+
await ensureTaskFeatureBranch(branch, trimmedProjectId, {
|
|
1771
|
+
...options,
|
|
1772
|
+
cwd: gitRoot
|
|
1773
|
+
});
|
|
1774
|
+
} catch (err) {
|
|
1775
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`[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}`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return {
|
|
1782
|
+
branch,
|
|
1783
|
+
projectId: trimmedProjectId,
|
|
1784
|
+
kind: "multi",
|
|
1785
|
+
repos: repoRoots
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// src/commands/ensure-task-branch.ts
|
|
1790
|
+
async function runEnsureTaskBranchCli(options = {}) {
|
|
1791
|
+
const workdir = resolveWorkdirPath(options.cwd ?? process.cwd());
|
|
1792
|
+
const taskId = options.taskId?.trim() || "";
|
|
1793
|
+
const projectId = options.projectId?.trim() || "";
|
|
1794
|
+
if (!taskId) {
|
|
1795
|
+
throw new Error("[apm] \u7F3A\u5C11 taskId\uFF1A\u8BF7\u4F20\u5165\u53C2\u6570");
|
|
1796
|
+
}
|
|
1797
|
+
if (!projectId) {
|
|
1798
|
+
throw new Error("[apm] \u7F3A\u5C11 projectId\uFF1A\u8BF7\u4F20\u5165\u53C2\u6570");
|
|
1799
|
+
}
|
|
1800
|
+
const result = await runTaskBranch(taskId, projectId, {
|
|
1801
|
+
cwd: workdir,
|
|
1802
|
+
reposManifest: options.reposManifest
|
|
1803
|
+
});
|
|
1804
|
+
console.log(
|
|
1805
|
+
`[apm] \u7279\u6027\u5206\u652F\u5DF2\u5C31\u7EEA\uFF08\u5168\u90E8 ${result.repos.length} \u4E2A\u4ED3\uFF09 branch=${result.branch} projectId=${result.projectId}`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/utils/user-prompt-cache.ts
|
|
1810
|
+
import { createHash } from "crypto";
|
|
1811
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1812
|
+
import { join as join11 } from "path";
|
|
1813
|
+
import { tmpdir } from "os";
|
|
1814
|
+
var CACHE_ROOT = join11(tmpdir(), "apm-hooks");
|
|
1815
|
+
var PROMPT_FILE = "user-prompt.txt";
|
|
1816
|
+
var COMMIT_SUBJECT_MAX_LEN = 100;
|
|
1817
|
+
function workdirKey(workdir) {
|
|
1818
|
+
return createHash("sha256").update(resolveWorkdirPath(workdir)).digest("hex").slice(0, 16);
|
|
1819
|
+
}
|
|
1820
|
+
function userPromptCachePath(workdirInput) {
|
|
1821
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
1822
|
+
return join11(CACHE_ROOT, workdirKey(workdir), PROMPT_FILE);
|
|
1823
|
+
}
|
|
1824
|
+
function formatUserPromptAsCommitMessage(prompt) {
|
|
1825
|
+
const oneLine = prompt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
1826
|
+
if (!oneLine) return "";
|
|
1827
|
+
const cleaned = oneLine.replace(/\s+/g, " ").trim();
|
|
1828
|
+
if (cleaned.length <= COMMIT_SUBJECT_MAX_LEN) return cleaned;
|
|
1829
|
+
return `${cleaned.slice(0, COMMIT_SUBJECT_MAX_LEN - 1)}\u2026`;
|
|
1830
|
+
}
|
|
1831
|
+
function loadUserPromptCommitMessage(workdirInput) {
|
|
1832
|
+
const path = toFsPath(userPromptCachePath(workdirInput));
|
|
1833
|
+
if (!existsSync7(path)) return null;
|
|
1834
|
+
try {
|
|
1835
|
+
const raw = readFileSync6(path, "utf8");
|
|
1836
|
+
const msg = formatUserPromptAsCommitMessage(raw);
|
|
1837
|
+
return msg || null;
|
|
1838
|
+
} catch {
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/commands/commit-task-repos.ts
|
|
1844
|
+
function resolveCommitTaskMessage(workdir, explicit) {
|
|
1845
|
+
const fromOpt = explicit?.trim() ?? "";
|
|
1846
|
+
if (fromOpt) return fromOpt;
|
|
1847
|
+
const fromPrompt = loadUserPromptCommitMessage(workdir);
|
|
1848
|
+
if (fromPrompt) return fromPrompt;
|
|
1849
|
+
const taskId = process.env.APM_TASK_ID?.trim() || "";
|
|
1850
|
+
const projectId = process.env.APM_PROJECT_ID?.trim() || "";
|
|
1851
|
+
if (taskId) {
|
|
1852
|
+
let branch = "";
|
|
1853
|
+
try {
|
|
1854
|
+
branch = branchNameForTask(taskId);
|
|
1855
|
+
} catch {
|
|
1856
|
+
branch = `feat/task-${taskId}`;
|
|
1857
|
+
}
|
|
1858
|
+
if (projectId) {
|
|
1859
|
+
return `chore(apm): sync ${branch} (project ${projectId})`;
|
|
1860
|
+
}
|
|
1861
|
+
return `chore(apm): sync ${branch}`;
|
|
1862
|
+
}
|
|
1863
|
+
if (projectId) {
|
|
1864
|
+
return `chore(apm): sync workspace (project ${projectId})`;
|
|
1865
|
+
}
|
|
1866
|
+
return void 0;
|
|
1867
|
+
}
|
|
1868
|
+
async function runCommitTaskReposCli(options = {}) {
|
|
1869
|
+
const workdir = resolveWorkdirPath(options.cwd ?? process.cwd());
|
|
1870
|
+
const message = resolveCommitTaskMessage(workdir, options.message);
|
|
1871
|
+
if (message) {
|
|
1872
|
+
console.log(`[apm] commit message: ${message}`);
|
|
1873
|
+
}
|
|
1874
|
+
const result = await syncTaskRepos(workdir, {
|
|
1875
|
+
message,
|
|
1876
|
+
reposManifest: options.reposManifest
|
|
1877
|
+
});
|
|
1878
|
+
console.log(
|
|
1879
|
+
`[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}`
|
|
1880
|
+
);
|
|
1881
|
+
if (result.failed.length > 0) {
|
|
1882
|
+
throw new Error(
|
|
1883
|
+
`[apm] \u4EE5\u4E0B\u4ED3\u5E93 commit/push \u5931\u8D25: ${result.failed.join(", ")}`
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
2030
1888
|
// src/index.ts
|
|
2031
1889
|
function buildProgram() {
|
|
2032
1890
|
const program = new Command();
|
|
@@ -2063,10 +1921,38 @@ function buildProgram() {
|
|
|
2063
1921
|
program.command("update-skills").description("\u4ECE\u5E73\u53F0\u540C\u6B65 .apm/ \u4E0B\u7684 WebIDE \u6307\u5357\u4E0E\u89C4\u5219").action(async () => {
|
|
2064
1922
|
await runUpdateSkills();
|
|
2065
1923
|
});
|
|
2066
|
-
program.command("
|
|
2067
|
-
"\
|
|
2068
|
-
).
|
|
2069
|
-
|
|
1924
|
+
program.command("ensure-task-branch").description(
|
|
1925
|
+
"\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"
|
|
1926
|
+
).argument("<taskId>", "\u4EFB\u52A1 ID\uFF08\u521B\u5EFA/\u5207\u6362\u5206\u652F\uFF09").argument(
|
|
1927
|
+
"<projectId>",
|
|
1928
|
+
"\u9879\u76EE ID\uFF08\u4EC5\u521B\u5EFA\u65F6\u7528\uFF1B\u89C4\u8303\u4E0E\u9879\u76EE\u5206\u652F\u540C\u540D\uFF09"
|
|
1929
|
+
).option("--cwd <dir>", "\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option(
|
|
1930
|
+
"--repos <file>",
|
|
1931
|
+
"\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"
|
|
1932
|
+
).action(
|
|
1933
|
+
async (taskId, projectId, opts) => {
|
|
1934
|
+
await runEnsureTaskBranchCli({
|
|
1935
|
+
taskId,
|
|
1936
|
+
projectId,
|
|
1937
|
+
cwd: opts.cwd,
|
|
1938
|
+
reposManifest: opts.repos
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
);
|
|
1942
|
+
program.command("commit-task-repos").description(
|
|
1943
|
+
"\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"
|
|
1944
|
+
).option("--cwd <dir>", "\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option(
|
|
1945
|
+
"--repos <file>",
|
|
1946
|
+
"\u591A\u4ED3\u6E05\u5355 JSON\uFF1B\u7701\u7565\u65F6\u8BFB APM_REPOS_MANIFEST\uFF0C\u518D\u9ED8\u8BA4 /home/repository.json"
|
|
1947
|
+
).option(
|
|
1948
|
+
"-m, --message <msg>",
|
|
1949
|
+
"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"
|
|
1950
|
+
).action(async (opts) => {
|
|
1951
|
+
await runCommitTaskReposCli({
|
|
1952
|
+
cwd: opts.cwd,
|
|
1953
|
+
reposManifest: opts.repos,
|
|
1954
|
+
message: opts.message
|
|
1955
|
+
});
|
|
2070
1956
|
});
|
|
2071
1957
|
program.command("connect").description(
|
|
2072
1958
|
"\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"
|