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