ai-project-manage-cli 7.1.5 → 7.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +130 -11
- package/dist/webide-message-worker.js +1067 -442
- package/package.json +2 -2
|
@@ -96,6 +96,10 @@ var requestConfig = {
|
|
|
96
96
|
method: "PUT",
|
|
97
97
|
path: "/cli/webide/plan"
|
|
98
98
|
}),
|
|
99
|
+
webideReplaceTestCases: defineEndpoint({
|
|
100
|
+
method: "PUT",
|
|
101
|
+
path: "/cli/webide/test-cases"
|
|
102
|
+
}),
|
|
99
103
|
branchBaseline: defineEndpoint({
|
|
100
104
|
method: "GET",
|
|
101
105
|
path: "/cli/tasks/branch-baseline"
|
|
@@ -128,6 +132,10 @@ var requestConfig = {
|
|
|
128
132
|
method: "POST",
|
|
129
133
|
path: "/cli/pull-requests"
|
|
130
134
|
}),
|
|
135
|
+
createWebIdeDraftPullRequest: defineEndpoint({
|
|
136
|
+
method: "POST",
|
|
137
|
+
path: "/cli/webide/draft-pull-requests"
|
|
138
|
+
}),
|
|
131
139
|
getRepositoryProjectDocumentManifest: defineEndpoint({
|
|
132
140
|
method: "GET",
|
|
133
141
|
path: "/cli/repository-project-documents/manifest"
|
|
@@ -196,76 +204,735 @@ async function tryReadApmConfig() {
|
|
|
196
204
|
if (typeof rawCfg.baseUrl !== "string") {
|
|
197
205
|
return null;
|
|
198
206
|
}
|
|
199
|
-
const apiKey = resolveApiKey(rawCfg);
|
|
200
|
-
if (!apiKey) return null;
|
|
201
|
-
const cfg = v;
|
|
202
|
-
const clientMachineId = resolveClientMachineId(cfg);
|
|
203
|
-
return {
|
|
204
|
-
baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
|
|
205
|
-
apiKey,
|
|
206
|
-
...clientMachineId ? { clientMachineId } : {}
|
|
207
|
-
};
|
|
208
|
-
} catch {
|
|
209
|
-
return null;
|
|
207
|
+
const apiKey = resolveApiKey(rawCfg);
|
|
208
|
+
if (!apiKey) return null;
|
|
209
|
+
const cfg = v;
|
|
210
|
+
const clientMachineId = resolveClientMachineId(cfg);
|
|
211
|
+
return {
|
|
212
|
+
baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
|
|
213
|
+
apiKey,
|
|
214
|
+
...clientMachineId ? { clientMachineId } : {}
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/api/client.ts
|
|
222
|
+
function createApmApiClient(cfg) {
|
|
223
|
+
const baseURL = `${cfg.baseUrl.trim().replace(/\/+$/, "")}/api/v1`;
|
|
224
|
+
return createApiClient(requestConfig, {
|
|
225
|
+
baseURL,
|
|
226
|
+
getToken: () => resolveApiKey(cfg) || void 0,
|
|
227
|
+
successCodes: [0],
|
|
228
|
+
unauthorizedCodes: [401]
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/workdir-path.ts
|
|
233
|
+
import { realpathSync } from "fs";
|
|
234
|
+
import { platform } from "os";
|
|
235
|
+
import { resolve } from "path";
|
|
236
|
+
function toFsPath(inputPath) {
|
|
237
|
+
const absolute = resolve(inputPath);
|
|
238
|
+
if (platform() !== "win32") return absolute;
|
|
239
|
+
if (absolute.startsWith("\\\\?\\")) return absolute;
|
|
240
|
+
const normalized = absolute.replace(/\//g, "\\");
|
|
241
|
+
if (normalized.startsWith("\\\\")) {
|
|
242
|
+
return `\\\\?\\UNC\\${normalized.slice(2)}`;
|
|
243
|
+
}
|
|
244
|
+
return `\\\\?\\${normalized}`;
|
|
245
|
+
}
|
|
246
|
+
function normalizeWorkdirPath(path) {
|
|
247
|
+
let normalized = path.trim().replace(/\\/g, "/").normalize("NFC");
|
|
248
|
+
if (normalized.startsWith("//?/")) {
|
|
249
|
+
normalized = normalized.slice(4);
|
|
250
|
+
}
|
|
251
|
+
const windowsDrive = /^([A-Za-z]:)\/*(.*)$/.exec(normalized);
|
|
252
|
+
if (windowsDrive) {
|
|
253
|
+
const drive = windowsDrive[1].toLowerCase();
|
|
254
|
+
const rest = windowsDrive[2].replace(/\/+/g, "/").replace(/\/$/, "");
|
|
255
|
+
return rest ? `${drive}/${rest}` : drive;
|
|
256
|
+
}
|
|
257
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
258
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
259
|
+
normalized = normalized.slice(0, -1);
|
|
260
|
+
}
|
|
261
|
+
return normalized;
|
|
262
|
+
}
|
|
263
|
+
function resolveWorkdirPath(cwd = process.cwd()) {
|
|
264
|
+
const absolute = resolve(cwd);
|
|
265
|
+
try {
|
|
266
|
+
return normalizeWorkdirPath(realpathSync.native(absolute));
|
|
267
|
+
} catch {
|
|
268
|
+
return normalizeWorkdirPath(absolute);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function requireRemoteWorkdir(workdir) {
|
|
272
|
+
const trimmed = typeof workdir === "string" ? workdir.trim() : "";
|
|
273
|
+
if (!trimmed) {
|
|
274
|
+
throw new Error("[apm] \u8FDC\u7A0B\u6D88\u606F\u7F3A\u5C11\u5DE5\u4F5C\u76EE\u5F55 workdir");
|
|
275
|
+
}
|
|
276
|
+
return resolveWorkdirPath(trimmed);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/git-remote.ts
|
|
280
|
+
import { execFile } from "child_process";
|
|
281
|
+
import { promisify } from "util";
|
|
282
|
+
var execFileAsync = promisify(execFile);
|
|
283
|
+
async function tryReadGitOriginUrl(cwd) {
|
|
284
|
+
try {
|
|
285
|
+
const { stdout } = await execFileAsync(
|
|
286
|
+
"git",
|
|
287
|
+
["config", "--get", "remote.origin.url"],
|
|
288
|
+
{ cwd, encoding: "utf8", maxBuffer: 1024 * 1024 }
|
|
289
|
+
);
|
|
290
|
+
const url = stdout.trim();
|
|
291
|
+
return url || null;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/git-utils.ts
|
|
298
|
+
import { execFile as execFile2 } from "child_process";
|
|
299
|
+
import { promisify as promisify2 } from "util";
|
|
300
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
301
|
+
async function execGit(cwd, args, quiet = false) {
|
|
302
|
+
try {
|
|
303
|
+
const { stdout, stderr } = await execFileAsync2("git", args, {
|
|
304
|
+
cwd,
|
|
305
|
+
encoding: "utf8",
|
|
306
|
+
maxBuffer: 10 * 1024 * 1024
|
|
307
|
+
});
|
|
308
|
+
if (!quiet && stderr.trim()) {
|
|
309
|
+
process.stderr.write(stderr);
|
|
310
|
+
}
|
|
311
|
+
return stdout;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const e = err;
|
|
314
|
+
const detail = (e.stderr ?? e.message ?? String(err)).trim();
|
|
315
|
+
throw new Error(
|
|
316
|
+
`[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function isGitRepo(cwd) {
|
|
321
|
+
try {
|
|
322
|
+
await execGit(cwd, ["rev-parse", "--git-dir"], true);
|
|
323
|
+
return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function ensureGitRepo(cwd) {
|
|
329
|
+
await execGit(cwd, ["rev-parse", "--git-dir"], true);
|
|
330
|
+
}
|
|
331
|
+
async function getCurrentBranch(cwd) {
|
|
332
|
+
return (await execGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
|
|
333
|
+
}
|
|
334
|
+
async function isWorkingTreeDirty(cwd) {
|
|
335
|
+
const out = await execGit(cwd, ["status", "--porcelain"], true);
|
|
336
|
+
return out.trim().length > 0;
|
|
337
|
+
}
|
|
338
|
+
async function remoteBranchExists(cwd, branch) {
|
|
339
|
+
const out = await execGit(
|
|
340
|
+
cwd,
|
|
341
|
+
["ls-remote", "--heads", "origin", branch],
|
|
342
|
+
true
|
|
343
|
+
);
|
|
344
|
+
return out.trim().length > 0;
|
|
345
|
+
}
|
|
346
|
+
async function resolveGitRepoRoot(cwd) {
|
|
347
|
+
return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
|
|
348
|
+
}
|
|
349
|
+
async function ensureRemoteBaselineBranch(cwd, baselineBranch) {
|
|
350
|
+
await execGit(cwd, ["fetch", "origin", baselineBranch], true);
|
|
351
|
+
if (await remoteBranchExists(cwd, baselineBranch)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
throw new Error(
|
|
355
|
+
`[apm] \u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F origin/${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
async function resolveDefaultRemoteBranch(cwd) {
|
|
359
|
+
try {
|
|
360
|
+
const ref = (await execGit(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], true)).trim();
|
|
361
|
+
const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
362
|
+
if (match?.[1]) {
|
|
363
|
+
return match[1];
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
for (const candidate of ["main", "master"]) {
|
|
368
|
+
if (await remoteBranchExists(cwd, candidate)) {
|
|
369
|
+
return candidate;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
throw new Error(
|
|
373
|
+
"[apm] \u65E0\u6CD5\u786E\u5B9A\u8FDC\u7A0B\u9ED8\u8BA4\u5206\u652F\uFF08origin/HEAD\u3001main\u3001master \u5747\u4E0D\u53EF\u7528\uFF09"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
async function hasUpstream(cwd) {
|
|
377
|
+
try {
|
|
378
|
+
await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
|
|
379
|
+
return true;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
|
|
385
|
+
async function commitAndPushGitignore(workdir) {
|
|
386
|
+
if (!await isGitRepo(workdir)) {
|
|
387
|
+
console.log("[apm] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u8BF7\u624B\u52A8\u63D0\u4EA4 .gitignore");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
await execGit(workdir, ["add", "--", ".gitignore"]);
|
|
391
|
+
await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
|
|
392
|
+
console.log(`[apm] \u5DF2\u63D0\u4EA4 .gitignore: ${GITIGNORE_COMMIT_MESSAGE}`);
|
|
393
|
+
const originUrl = await tryReadGitOriginUrl(workdir);
|
|
394
|
+
if (!originUrl) {
|
|
395
|
+
console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (await hasUpstream(workdir)) {
|
|
399
|
+
await execGit(workdir, ["push"]);
|
|
400
|
+
} else {
|
|
401
|
+
await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
|
|
402
|
+
}
|
|
403
|
+
console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/baseline-resolve.ts
|
|
407
|
+
function formatBaselineDiagnostic(workdirPath, baselineWorkdirPath, diagnostic) {
|
|
408
|
+
return diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baselineWorkdirPath}\uFF09
|
|
409
|
+
\u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
|
|
410
|
+
}
|
|
411
|
+
async function matchRepositoryByGitRemote(api, workdirPath) {
|
|
412
|
+
const gitRoot = await resolveGitRepoRoot(workdirPath);
|
|
413
|
+
const gitUrl = await tryReadGitOriginUrl(gitRoot);
|
|
414
|
+
if (!gitUrl) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const matched = await api.cli.matchRepository({ url: gitUrl });
|
|
418
|
+
const repositoryId = matched.repositoryId?.trim();
|
|
419
|
+
const defaultBranch = matched.defaultBranch?.trim();
|
|
420
|
+
if (!repositoryId || !defaultBranch) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
console.log(
|
|
424
|
+
`[apm] \u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u672A\u5339\u914D\uFF0C\u5DF2\u901A\u8FC7 git remote \u5173\u8054\u4ED3\u5E93: ${gitUrl}`
|
|
425
|
+
);
|
|
426
|
+
return { repositoryId, defaultBranch };
|
|
427
|
+
}
|
|
428
|
+
async function resolveWorkspaceBaseline(api, workdirPath) {
|
|
429
|
+
const baseline = await api.cli.workspaceBaseline({ workdirPath });
|
|
430
|
+
const repositoryId = baseline.repositoryId?.trim();
|
|
431
|
+
const defaultBranch = baseline.defaultBranch?.trim();
|
|
432
|
+
if (repositoryId && defaultBranch) {
|
|
433
|
+
return {
|
|
434
|
+
repositoryId,
|
|
435
|
+
defaultBranch,
|
|
436
|
+
workdirPath: baseline.workdirPath,
|
|
437
|
+
matchedViaGitRemote: false
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const viaGit = await matchRepositoryByGitRemote(api, workdirPath);
|
|
441
|
+
if (viaGit) {
|
|
442
|
+
return {
|
|
443
|
+
...viaGit,
|
|
444
|
+
workdirPath: baseline.workdirPath,
|
|
445
|
+
matchedViaGitRemote: true
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
throw new Error(
|
|
449
|
+
`[apm] ${formatBaselineDiagnostic(
|
|
450
|
+
workdirPath,
|
|
451
|
+
baseline.workdirPath,
|
|
452
|
+
baseline.diagnostic
|
|
453
|
+
)}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/command-utils.ts
|
|
458
|
+
import {
|
|
459
|
+
copyFileSync,
|
|
460
|
+
existsSync,
|
|
461
|
+
mkdirSync as mkdirSync2,
|
|
462
|
+
readFileSync as readFileSync2,
|
|
463
|
+
readdirSync,
|
|
464
|
+
statSync,
|
|
465
|
+
writeFileSync as writeFileSync2
|
|
466
|
+
} from "fs";
|
|
467
|
+
import { basename, dirname, extname, join as join2, resolve as resolve2 } from "path";
|
|
468
|
+
import { fileURLToPath } from "url";
|
|
469
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
470
|
+
var CLI_TEMPLATE_DIR = resolve2(__dirname, "../template");
|
|
471
|
+
function workspaceApmDir(cwd = resolveWorkdirPath()) {
|
|
472
|
+
return resolve2(resolve2(cwd), ".apm");
|
|
473
|
+
}
|
|
474
|
+
function isWorkspaceApmInitialized(workdir) {
|
|
475
|
+
const apmDir = workspaceApmDir(workdir);
|
|
476
|
+
const fsApmDir = toFsPath(apmDir);
|
|
477
|
+
if (!existsSync(fsApmDir)) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const st = statSync(fsApmDir);
|
|
481
|
+
if (!st.isDirectory()) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u4E0B\u7684 .apm \u4E0D\u662F\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u63A5\u5165\u72B6\u6001\u3002`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return readdirSync(fsApmDir).length > 0;
|
|
487
|
+
}
|
|
488
|
+
var APM_GITIGNORE_PATTERNS = [
|
|
489
|
+
/^\.apm\/?$/,
|
|
490
|
+
/^\.apm\/\*\*$/,
|
|
491
|
+
/^\*\*\/\.apm\/?$/,
|
|
492
|
+
/^\*\*\/\.apm\/\*\*$/,
|
|
493
|
+
/^\/\.apm\/?$/,
|
|
494
|
+
/^\/\.apm\/\*\*$/
|
|
495
|
+
];
|
|
496
|
+
function normalizeGitignorePattern(line) {
|
|
497
|
+
const trimmed = line.trim();
|
|
498
|
+
if (!trimmed || trimmed.startsWith("#")) return "";
|
|
499
|
+
if (trimmed.startsWith("!")) return "";
|
|
500
|
+
const hashIndex = trimmed.indexOf("#");
|
|
501
|
+
return (hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed).trim();
|
|
502
|
+
}
|
|
503
|
+
function gitignoreIgnoresApm(line) {
|
|
504
|
+
const pattern = normalizeGitignorePattern(line);
|
|
505
|
+
if (!pattern) return false;
|
|
506
|
+
return APM_GITIGNORE_PATTERNS.some((re) => re.test(pattern));
|
|
507
|
+
}
|
|
508
|
+
var APM_GITIGNORE_LINE = "**/.apm/**";
|
|
509
|
+
function ensureApmGitignoredInRepo(workdir) {
|
|
510
|
+
const gitignorePath = join2(workdir, ".gitignore");
|
|
511
|
+
const fsGitignorePath = toFsPath(gitignorePath);
|
|
512
|
+
if (!existsSync(fsGitignorePath)) {
|
|
513
|
+
writeFileSync2(fsGitignorePath, `${APM_GITIGNORE_LINE}
|
|
514
|
+
`, "utf8");
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
const content = readFileSync2(fsGitignorePath, "utf8");
|
|
518
|
+
if (content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const suffix = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
522
|
+
writeFileSync2(
|
|
523
|
+
fsGitignorePath,
|
|
524
|
+
`${content}${suffix}${APM_GITIGNORE_LINE}
|
|
525
|
+
`,
|
|
526
|
+
"utf8"
|
|
527
|
+
);
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
function assertApmGitignoredInRepo(workdir) {
|
|
531
|
+
const gitignorePath = join2(workdir, ".gitignore");
|
|
532
|
+
const fsGitignorePath = toFsPath(gitignorePath);
|
|
533
|
+
if (!existsSync(fsGitignorePath)) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u7F3A\u5C11 .gitignore \u6587\u4EF6\uFF0C\u8BF7\u6DFB\u52A0\u5BF9 .apm \u7684\u5FFD\u7565\u89C4\u5219\uFF08\u4F8B\u5982 **/.apm/**\uFF09\uFF0C\u907F\u514D\u672C\u5730\u4F1A\u8BDD\u4E0E\u90E8\u7F72\u51ED\u636E\u88AB\u63D0\u4EA4\u3002`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const content = readFileSync2(fsGitignorePath, "utf8");
|
|
539
|
+
if (!content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u7684 .gitignore \u672A\u5FFD\u7565 .apm\uFF0C\u8BF7\u6DFB\u52A0 **/.apm/** \u6216 .apm/\uFF0C\u907F\u514D\u672C\u5730\u4F1A\u8BDD\u4E0E\u90E8\u7F72\u51ED\u636E\u88AB\u63D0\u4EA4\u3002`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function ensureDirExists(dir) {
|
|
546
|
+
mkdirSync2(dir, { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
|
|
549
|
+
const dir = workspaceApmDir(cwd);
|
|
550
|
+
const fsDir = toFsPath(dir);
|
|
551
|
+
if (!existsSync(fsDir)) {
|
|
552
|
+
mkdirSync2(fsDir, { recursive: true });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const st = statSync(fsDir);
|
|
556
|
+
if (!st.isDirectory()) {
|
|
557
|
+
throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${dir}`);
|
|
558
|
+
}
|
|
559
|
+
if (readdirSync(fsDir).length > 0) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
"[apm] .apm \u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\uFF0C\u8BF7\u5148\u5907\u4EFD\u3001\u6E05\u7A7A\u6216\u5220\u9664\u540E\u518D\u6267\u884C init"
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
var WORKSPACE_TEMPLATE_SUBDIRS = [
|
|
566
|
+
"sessions",
|
|
567
|
+
"skills",
|
|
568
|
+
"rules",
|
|
569
|
+
"deploy"
|
|
570
|
+
];
|
|
571
|
+
function shouldSkipTemplateEntry(name) {
|
|
572
|
+
return name === ".DS_Store" || name === "Thumbs.db";
|
|
573
|
+
}
|
|
574
|
+
function copyTemplateEntry(src, dest) {
|
|
575
|
+
const fsSrc = toFsPath(src);
|
|
576
|
+
const fsDest = toFsPath(dest);
|
|
577
|
+
const st = statSync(fsSrc);
|
|
578
|
+
if (st.isDirectory()) {
|
|
579
|
+
mkdirSync2(fsDest, { recursive: true });
|
|
580
|
+
for (const name of readdirSync(fsSrc)) {
|
|
581
|
+
if (shouldSkipTemplateEntry(name)) continue;
|
|
582
|
+
copyTemplateEntry(join2(src, name), join2(dest, name));
|
|
583
|
+
}
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (!st.isFile()) return;
|
|
587
|
+
mkdirSync2(toFsPath(dirname(dest)), { recursive: true });
|
|
588
|
+
copyFileSync(fsSrc, fsDest);
|
|
589
|
+
}
|
|
590
|
+
function assertTemplateCopiedToApm(apmDir, workdir) {
|
|
591
|
+
const required = [
|
|
592
|
+
"AGENTS.md",
|
|
593
|
+
"apm.config.json",
|
|
594
|
+
"rules",
|
|
595
|
+
"skills",
|
|
596
|
+
"sessions"
|
|
597
|
+
];
|
|
598
|
+
for (const item of required) {
|
|
599
|
+
const path = join2(apmDir, item);
|
|
600
|
+
if (!existsSync(toFsPath(path))) {
|
|
601
|
+
throw new Error(`[apm] \u521D\u59CB\u5316\u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5C11: ${path}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const leakedRules = join2(workdir, "rules");
|
|
605
|
+
const apmRules = join2(apmDir, "rules");
|
|
606
|
+
if (existsSync(toFsPath(leakedRules)) && !existsSync(toFsPath(join2(apmRules, "reply.md")))) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`[apm] \u6A21\u677F\u88AB\u590D\u5236\u5230\u9519\u8BEF\u4F4D\u7F6E: ${leakedRules}\uFF08\u5E94\u5728 ${apmRules}\uFF09`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function copyTemplateFiles(targetDir, workdir = resolveWorkdirPath()) {
|
|
613
|
+
const resolvedTarget = resolve2(targetDir);
|
|
614
|
+
const templateDir = resolve2(CLI_TEMPLATE_DIR);
|
|
615
|
+
const fsTemplateDir = toFsPath(templateDir);
|
|
616
|
+
if (!existsSync(fsTemplateDir)) {
|
|
617
|
+
throw new Error(`[apm] \u672A\u627E\u5230 CLI \u6A21\u677F\u76EE\u5F55: ${templateDir}`);
|
|
618
|
+
}
|
|
619
|
+
const dirStat = statSync(fsTemplateDir);
|
|
620
|
+
if (!dirStat.isDirectory()) {
|
|
621
|
+
throw new Error(`[apm] CLI \u6A21\u677F\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55: ${templateDir}`);
|
|
622
|
+
}
|
|
623
|
+
const entries = readdirSync(fsTemplateDir).filter(
|
|
624
|
+
(name) => !shouldSkipTemplateEntry(name)
|
|
625
|
+
);
|
|
626
|
+
if (entries.length === 0) {
|
|
627
|
+
throw new Error(`[apm] CLI \u6A21\u677F\u76EE\u5F55\u4E3A\u7A7A: ${templateDir}`);
|
|
628
|
+
}
|
|
629
|
+
mkdirSync2(toFsPath(resolvedTarget), { recursive: true });
|
|
630
|
+
for (const name of entries) {
|
|
631
|
+
copyTemplateEntry(join2(templateDir, name), join2(resolvedTarget, name));
|
|
632
|
+
}
|
|
633
|
+
for (const subdir of WORKSPACE_TEMPLATE_SUBDIRS) {
|
|
634
|
+
mkdirSync2(toFsPath(join2(resolvedTarget, subdir)), { recursive: true });
|
|
635
|
+
}
|
|
636
|
+
assertTemplateCopiedToApm(resolvedTarget, resolve2(workdir));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/workspace-repos.ts
|
|
640
|
+
import {
|
|
641
|
+
existsSync as existsSync2,
|
|
642
|
+
mkdirSync as mkdirSync3,
|
|
643
|
+
readFileSync as readFileSync3,
|
|
644
|
+
readdirSync as readdirSync2,
|
|
645
|
+
statSync as statSync2,
|
|
646
|
+
writeFileSync as writeFileSync3
|
|
647
|
+
} from "fs";
|
|
648
|
+
import { basename as basename2, join as join3, relative, resolve as resolve3 } from "path";
|
|
649
|
+
var WORKSPACE_REPOS_MANIFEST = "workspace-repos.json";
|
|
650
|
+
var WORKSPACE_REPOS_VERSION = 1;
|
|
651
|
+
var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([".apm", "node_modules"]);
|
|
652
|
+
function hasGitAt(dir) {
|
|
653
|
+
return existsSync2(toFsPath(join3(dir, ".git")));
|
|
654
|
+
}
|
|
655
|
+
function isSkippableChildName(name) {
|
|
656
|
+
if (SKIP_DIR_NAMES.has(name)) return true;
|
|
657
|
+
if (name.startsWith(".")) return true;
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
function manifestPath(workdir) {
|
|
661
|
+
return join3(workspaceApmDir(workdir), WORKSPACE_REPOS_MANIFEST);
|
|
662
|
+
}
|
|
663
|
+
function absoluteRepoPath(workdir, entry) {
|
|
664
|
+
if (entry.path === "." || entry.path === "") {
|
|
665
|
+
return workdir;
|
|
666
|
+
}
|
|
667
|
+
return resolve3(workdir, entry.path);
|
|
668
|
+
}
|
|
669
|
+
function toRelativeRepoPath(workdir, repoAbs) {
|
|
670
|
+
const rel = relative(workdir, repoAbs).replace(/\\/g, "/");
|
|
671
|
+
return rel === "" ? "." : rel;
|
|
672
|
+
}
|
|
673
|
+
function readManifest(workdir) {
|
|
674
|
+
const path = toFsPath(manifestPath(workdir));
|
|
675
|
+
if (!existsSync2(path)) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const raw = JSON.parse(
|
|
680
|
+
readFileSync3(path, "utf8")
|
|
681
|
+
);
|
|
682
|
+
if (raw?.version !== WORKSPACE_REPOS_VERSION) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
if (raw.kind !== "single" && raw.kind !== "multi") {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (!Array.isArray(raw.repos) || raw.repos.length === 0) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
if (typeof raw.workdir !== "string" || !raw.workdir.trim()) {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
return raw;
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function writeManifest(workdir, manifest) {
|
|
700
|
+
const apmDir = toFsPath(workspaceApmDir(workdir));
|
|
701
|
+
mkdirSync3(apmDir, { recursive: true });
|
|
702
|
+
const path = toFsPath(manifestPath(workdir));
|
|
703
|
+
writeFileSync3(path, `${JSON.stringify(manifest, null, 2)}
|
|
704
|
+
`, "utf8");
|
|
705
|
+
}
|
|
706
|
+
function manifestStillValid(workdir, manifest) {
|
|
707
|
+
if (normalizeWorkdirPath(manifest.workdir) !== normalizeWorkdirPath(workdir)) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
for (const entry of manifest.repos) {
|
|
711
|
+
if (typeof entry?.path !== "string" || !entry.path.trim()) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
const abs = absoluteRepoPath(workdir, entry);
|
|
715
|
+
if (!hasGitAt(abs)) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
function scanWorkspaceRepos(workdirInput) {
|
|
722
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
723
|
+
if (hasGitAt(workdir)) {
|
|
724
|
+
return {
|
|
725
|
+
version: WORKSPACE_REPOS_VERSION,
|
|
726
|
+
kind: "single",
|
|
727
|
+
workdir,
|
|
728
|
+
repos: [{ path: "." }],
|
|
729
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const fsWorkdir = toFsPath(workdir);
|
|
733
|
+
if (!existsSync2(fsWorkdir) || !statSync2(fsWorkdir).isDirectory()) {
|
|
734
|
+
throw new Error(`[apm] \u5DE5\u4F5C\u533A\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55: ${workdir}`);
|
|
735
|
+
}
|
|
736
|
+
const repos = [];
|
|
737
|
+
for (const name of readdirSync2(fsWorkdir)) {
|
|
738
|
+
if (isSkippableChildName(name)) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const childAbs = resolve3(workdir, name);
|
|
742
|
+
const childFs = toFsPath(childAbs);
|
|
743
|
+
if (!existsSync2(childFs) || !statSync2(childFs).isDirectory()) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (!hasGitAt(childAbs)) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
repos.push({ path: toRelativeRepoPath(workdir, childAbs) });
|
|
750
|
+
}
|
|
751
|
+
repos.sort((a, b) => a.path.localeCompare(b.path));
|
|
752
|
+
if (repos.length === 0) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
`[apm] \u5DE5\u4F5C\u533A ${workdir} \u6839\u76EE\u5F55\u65E0 .git\uFF0C\u4E14\u4E00\u7EA7\u5B50\u76EE\u5F55\u4E2D\u672A\u53D1\u73B0 git \u4ED3\u5E93`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
version: WORKSPACE_REPOS_VERSION,
|
|
759
|
+
kind: "multi",
|
|
760
|
+
workdir,
|
|
761
|
+
repos,
|
|
762
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function loadWorkspaceReposCache(workdirInput) {
|
|
766
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
767
|
+
const cached = readManifest(workdir);
|
|
768
|
+
if (!cached) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
`[apm] \u672A\u627E\u5230\u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 .apm/${WORKSPACE_REPOS_MANIFEST}\uFF0C\u8BF7\u5148\u6267\u884C\u4ED3\u5E93\u68C0\u6D4B`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (normalizeWorkdirPath(cached.workdir) !== normalizeWorkdirPath(workdir)) {
|
|
774
|
+
throw new Error(
|
|
775
|
+
`[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`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (!Array.isArray(cached.repos) || cached.repos.length === 0) {
|
|
779
|
+
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`);
|
|
780
|
+
}
|
|
781
|
+
console.log(
|
|
782
|
+
`[apm] \u8BFB\u53D6\u5DE5\u4F5C\u533A\u4ED3\u5E93\u7F13\u5B58 kind=${cached.kind} repos=${cached.repos.map((r) => r.path).join(", ")}`
|
|
783
|
+
);
|
|
784
|
+
return {
|
|
785
|
+
...cached,
|
|
786
|
+
workdir
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function resolveWorkspaceRepos(workdirInput, options) {
|
|
790
|
+
const workdir = resolveWorkdirPath(workdirInput);
|
|
791
|
+
if (!options?.forceRescan) {
|
|
792
|
+
const cached = readManifest(workdir);
|
|
793
|
+
if (cached && manifestStillValid(workdir, cached)) {
|
|
794
|
+
console.log(
|
|
795
|
+
`[apm] \u4F7F\u7528\u7F13\u5B58\u7684\u5DE5\u4F5C\u533A\u4ED3\u5E93\u6E05\u5355 kind=${cached.kind} repos=${cached.repos.length}`
|
|
796
|
+
);
|
|
797
|
+
return cached;
|
|
798
|
+
}
|
|
210
799
|
}
|
|
800
|
+
const scanned = scanWorkspaceRepos(workdir);
|
|
801
|
+
writeManifest(workdir, scanned);
|
|
802
|
+
console.log(
|
|
803
|
+
`[apm] \u5DF2\u626B\u63CF\u5DE5\u4F5C\u533A\u4ED3\u5E93 kind=${scanned.kind} repos=${scanned.repos.map((r) => r.path).join(", ")} \u2192 .apm/${WORKSPACE_REPOS_MANIFEST}`
|
|
804
|
+
);
|
|
805
|
+
return scanned;
|
|
211
806
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
unauthorizedCodes: [401]
|
|
221
|
-
});
|
|
807
|
+
function resolveWorkspaceRepoAbsolutePaths(manifest) {
|
|
808
|
+
return manifest.repos.map(
|
|
809
|
+
(entry) => absoluteRepoPath(manifest.workdir, entry)
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
function formatRepoLabel(workdir, repoAbs) {
|
|
813
|
+
const rel = toRelativeRepoPath(resolveWorkdirPath(workdir), repoAbs);
|
|
814
|
+
return rel === "." ? basename2(repoAbs) || repoAbs : rel;
|
|
222
815
|
}
|
|
223
816
|
|
|
224
|
-
// src/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
817
|
+
// src/commands/branch.ts
|
|
818
|
+
var TASK_BRANCH_PREFIX = "feat/task-";
|
|
819
|
+
async function localBranchExists(cwd, branch) {
|
|
820
|
+
try {
|
|
821
|
+
await execGit(
|
|
822
|
+
cwd,
|
|
823
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
|
|
824
|
+
true
|
|
825
|
+
);
|
|
826
|
+
return true;
|
|
827
|
+
} catch {
|
|
828
|
+
return false;
|
|
235
829
|
}
|
|
236
|
-
return `\\\\?\\${normalized}`;
|
|
237
830
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
831
|
+
async function commitWorkingTreeIfDirty(cwd, message) {
|
|
832
|
+
await ensureGitRepo(cwd);
|
|
833
|
+
if (!await isWorkingTreeDirty(cwd)) {
|
|
834
|
+
return false;
|
|
242
835
|
}
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
836
|
+
const commitMessage = message?.trim() || `chore(apm): \u540C\u6B65\u5DE5\u4F5C\u533A (${await getCurrentBranch(cwd)})`;
|
|
837
|
+
await execGit(cwd, ["add", "-A"]);
|
|
838
|
+
await execGit(cwd, ["commit", "-m", commitMessage]);
|
|
839
|
+
console.log(`[apm] \u5DF2\u63D0\u4EA4\u5DE5\u4F5C\u533A\u53D8\u66F4: ${commitMessage}`);
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
function branchNameForTask(taskId) {
|
|
843
|
+
const id = taskId.trim();
|
|
844
|
+
if (!id) {
|
|
845
|
+
throw new Error("[apm] \u4EFB\u52A1 ID \u4E0D\u80FD\u4E3A\u7A7A");
|
|
248
846
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
847
|
+
if (/[\s/\\]/.test(id)) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
"[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"
|
|
850
|
+
);
|
|
252
851
|
}
|
|
253
|
-
return
|
|
254
|
-
}
|
|
255
|
-
function
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
852
|
+
return `${TASK_BRANCH_PREFIX}${id}`;
|
|
853
|
+
}
|
|
854
|
+
async function ensureFeatureBranch(branch, baselineBranch, options) {
|
|
855
|
+
const cwd = options.cwd ?? process.cwd();
|
|
856
|
+
const gitRoot = await resolveGitRepoRoot(cwd);
|
|
857
|
+
const commitMessage = options.message?.trim() || `chore(apm): \u540C\u6B65\u5DE5\u4F5C\u533A (${branch})`;
|
|
858
|
+
await ensureGitRepo(gitRoot);
|
|
859
|
+
await ensureRemoteBaselineBranch(gitRoot, baselineBranch);
|
|
860
|
+
const current = await getCurrentBranch(gitRoot);
|
|
861
|
+
const dirty = await isWorkingTreeDirty(gitRoot);
|
|
862
|
+
if (dirty) {
|
|
863
|
+
if (current === branch) {
|
|
864
|
+
await commitWorkingTreeIfDirty(gitRoot, commitMessage);
|
|
865
|
+
} else {
|
|
866
|
+
await execGit(gitRoot, [
|
|
867
|
+
"stash",
|
|
868
|
+
"push",
|
|
869
|
+
"-u",
|
|
870
|
+
"-m",
|
|
871
|
+
`apm: switch to ${branch}`
|
|
872
|
+
]);
|
|
873
|
+
}
|
|
261
874
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
875
|
+
const onTargetBranch = await getCurrentBranch(gitRoot) === branch;
|
|
876
|
+
if (onTargetBranch) {
|
|
877
|
+
await execGit(gitRoot, ["fetch", "origin", baselineBranch]);
|
|
878
|
+
await execGit(gitRoot, ["merge", `origin/${baselineBranch}`, "--no-edit"]);
|
|
879
|
+
} else {
|
|
880
|
+
const remoteExists = await remoteBranchExists(gitRoot, branch);
|
|
881
|
+
if (remoteExists) {
|
|
882
|
+
await execGit(gitRoot, ["fetch", "origin", branch]);
|
|
883
|
+
await execGit(gitRoot, ["checkout", "-B", branch, `origin/${branch}`]);
|
|
884
|
+
} else if (await localBranchExists(gitRoot, branch)) {
|
|
885
|
+
if (await getCurrentBranch(gitRoot) !== branch) {
|
|
886
|
+
await execGit(gitRoot, ["checkout", branch]);
|
|
887
|
+
}
|
|
888
|
+
console.log(`[apm] \u5206\u652F ${branch} \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u521B\u5EFA`);
|
|
889
|
+
} else {
|
|
890
|
+
await execGit(gitRoot, ["fetch", "origin", baselineBranch]);
|
|
891
|
+
try {
|
|
892
|
+
await execGit(gitRoot, [
|
|
893
|
+
"checkout",
|
|
894
|
+
"-b",
|
|
895
|
+
branch,
|
|
896
|
+
`origin/${baselineBranch}`
|
|
897
|
+
]);
|
|
898
|
+
await execGit(gitRoot, ["push", "-u", "origin", branch]);
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (await localBranchExists(gitRoot, branch)) {
|
|
901
|
+
if (await getCurrentBranch(gitRoot) !== branch) {
|
|
902
|
+
await execGit(gitRoot, ["checkout", branch]);
|
|
903
|
+
}
|
|
904
|
+
console.log(`[apm] \u5206\u652F ${branch} \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u521B\u5EFA`);
|
|
905
|
+
} else {
|
|
906
|
+
throw err;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
267
910
|
}
|
|
268
|
-
|
|
911
|
+
console.log(`[apm] \u5DF2\u5C31\u7EEA\u5206\u652F ${branch}`);
|
|
912
|
+
return branch;
|
|
913
|
+
}
|
|
914
|
+
async function runTaskBranch(taskId, options = {}) {
|
|
915
|
+
const trimmedTaskId = taskId.trim();
|
|
916
|
+
if (!trimmedTaskId) {
|
|
917
|
+
throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
918
|
+
}
|
|
919
|
+
const cwd = options.cwd ?? process.cwd();
|
|
920
|
+
const workdirPath = resolveWorkdirPath(cwd);
|
|
921
|
+
const manifest = loadWorkspaceReposCache(workdirPath);
|
|
922
|
+
const repoRoots = resolveWorkspaceRepoAbsolutePaths(manifest);
|
|
923
|
+
const branch = branchNameForTask(trimmedTaskId);
|
|
924
|
+
for (const gitRoot of repoRoots) {
|
|
925
|
+
const label = formatRepoLabel(workdirPath, gitRoot);
|
|
926
|
+
const baselineBranch = await resolveDefaultRemoteBranch(gitRoot);
|
|
927
|
+
console.log(
|
|
928
|
+
`[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (baseline=${baselineBranch})`
|
|
929
|
+
);
|
|
930
|
+
await ensureFeatureBranch(branch, baselineBranch, {
|
|
931
|
+
...options,
|
|
932
|
+
cwd: gitRoot
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return { branch, kind: manifest.kind, repos: repoRoots };
|
|
269
936
|
}
|
|
270
937
|
|
|
271
938
|
// src/commands/connect/cursor-agent.ts
|
|
@@ -532,17 +1199,17 @@ ${JSON.stringify(event, null, 2)}
|
|
|
532
1199
|
}
|
|
533
1200
|
|
|
534
1201
|
// src/commands/connect/agent-session-registry.ts
|
|
535
|
-
import { existsSync, mkdirSync as
|
|
536
|
-
import { dirname, resolve as
|
|
1202
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1203
|
+
import { dirname as dirname2, resolve as resolve4 } from "node:path";
|
|
537
1204
|
function registryPath(workdir, sessionId) {
|
|
538
|
-
return
|
|
1205
|
+
return resolve4(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
|
|
539
1206
|
}
|
|
540
1207
|
function readRegistry(path) {
|
|
541
|
-
if (!
|
|
1208
|
+
if (!existsSync3(path)) {
|
|
542
1209
|
return {};
|
|
543
1210
|
}
|
|
544
1211
|
try {
|
|
545
|
-
const parsed = JSON.parse(
|
|
1212
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
546
1213
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
547
1214
|
const result = {};
|
|
548
1215
|
for (const [key, value] of Object.entries(
|
|
@@ -559,8 +1226,8 @@ function readRegistry(path) {
|
|
|
559
1226
|
return {};
|
|
560
1227
|
}
|
|
561
1228
|
function writeRegistry(path, registry) {
|
|
562
|
-
|
|
563
|
-
|
|
1229
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1230
|
+
writeFileSync4(path, `${JSON.stringify(registry, null, 2)}
|
|
564
1231
|
`, "utf8");
|
|
565
1232
|
}
|
|
566
1233
|
function loadSessionAgentId(workdir, sessionId, user) {
|
|
@@ -713,193 +1380,11 @@ function installAbortSignalDebug() {
|
|
|
713
1380
|
${stack}`
|
|
714
1381
|
);
|
|
715
1382
|
}
|
|
716
|
-
return original.call(this, type, listener, options);
|
|
717
|
-
};
|
|
718
|
-
console.log(
|
|
719
|
-
"[apm:abort-debug] \u5DF2\u542F\u7528 AbortSignal \u8C03\u8BD5\uFF08APM_DEBUG_ABORT_SIGNAL\uFF09"
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// src/command-utils.ts
|
|
724
|
-
import {
|
|
725
|
-
copyFileSync,
|
|
726
|
-
existsSync as existsSync2,
|
|
727
|
-
mkdirSync as mkdirSync3,
|
|
728
|
-
readFileSync as readFileSync3,
|
|
729
|
-
readdirSync,
|
|
730
|
-
statSync,
|
|
731
|
-
writeFileSync as writeFileSync3
|
|
732
|
-
} from "fs";
|
|
733
|
-
import { basename, dirname as dirname2, extname, join as join2, resolve as resolve3 } from "path";
|
|
734
|
-
import { fileURLToPath } from "url";
|
|
735
|
-
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
736
|
-
var CLI_TEMPLATE_DIR = resolve3(__dirname, "../template");
|
|
737
|
-
function workspaceApmDir(cwd = resolveWorkdirPath()) {
|
|
738
|
-
return resolve3(resolve3(cwd), ".apm");
|
|
739
|
-
}
|
|
740
|
-
function isWorkspaceApmInitialized(workdir) {
|
|
741
|
-
const apmDir = workspaceApmDir(workdir);
|
|
742
|
-
const fsApmDir = toFsPath(apmDir);
|
|
743
|
-
if (!existsSync2(fsApmDir)) {
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
const st = statSync(fsApmDir);
|
|
747
|
-
if (!st.isDirectory()) {
|
|
748
|
-
throw new Error(
|
|
749
|
-
`\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u4E0B\u7684 .apm \u4E0D\u662F\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u63A5\u5165\u72B6\u6001\u3002`
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
return readdirSync(fsApmDir).length > 0;
|
|
753
|
-
}
|
|
754
|
-
var APM_GITIGNORE_PATTERNS = [
|
|
755
|
-
/^\.apm\/?$/,
|
|
756
|
-
/^\.apm\/\*\*$/,
|
|
757
|
-
/^\*\*\/\.apm\/?$/,
|
|
758
|
-
/^\*\*\/\.apm\/\*\*$/,
|
|
759
|
-
/^\/\.apm\/?$/,
|
|
760
|
-
/^\/\.apm\/\*\*$/
|
|
761
|
-
];
|
|
762
|
-
function normalizeGitignorePattern(line) {
|
|
763
|
-
const trimmed = line.trim();
|
|
764
|
-
if (!trimmed || trimmed.startsWith("#")) return "";
|
|
765
|
-
if (trimmed.startsWith("!")) return "";
|
|
766
|
-
const hashIndex = trimmed.indexOf("#");
|
|
767
|
-
return (hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed).trim();
|
|
768
|
-
}
|
|
769
|
-
function gitignoreIgnoresApm(line) {
|
|
770
|
-
const pattern = normalizeGitignorePattern(line);
|
|
771
|
-
if (!pattern) return false;
|
|
772
|
-
return APM_GITIGNORE_PATTERNS.some((re) => re.test(pattern));
|
|
773
|
-
}
|
|
774
|
-
var APM_GITIGNORE_LINE = "**/.apm/**";
|
|
775
|
-
function ensureApmGitignoredInRepo(workdir) {
|
|
776
|
-
const gitignorePath = join2(workdir, ".gitignore");
|
|
777
|
-
const fsGitignorePath = toFsPath(gitignorePath);
|
|
778
|
-
if (!existsSync2(fsGitignorePath)) {
|
|
779
|
-
writeFileSync3(fsGitignorePath, `${APM_GITIGNORE_LINE}
|
|
780
|
-
`, "utf8");
|
|
781
|
-
return true;
|
|
782
|
-
}
|
|
783
|
-
const content = readFileSync3(fsGitignorePath, "utf8");
|
|
784
|
-
if (content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
const suffix = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
788
|
-
writeFileSync3(
|
|
789
|
-
fsGitignorePath,
|
|
790
|
-
`${content}${suffix}${APM_GITIGNORE_LINE}
|
|
791
|
-
`,
|
|
792
|
-
"utf8"
|
|
793
|
-
);
|
|
794
|
-
return true;
|
|
795
|
-
}
|
|
796
|
-
function assertApmGitignoredInRepo(workdir) {
|
|
797
|
-
const gitignorePath = join2(workdir, ".gitignore");
|
|
798
|
-
const fsGitignorePath = toFsPath(gitignorePath);
|
|
799
|
-
if (!existsSync2(fsGitignorePath)) {
|
|
800
|
-
throw new Error(
|
|
801
|
-
`\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u7F3A\u5C11 .gitignore \u6587\u4EF6\uFF0C\u8BF7\u6DFB\u52A0\u5BF9 .apm \u7684\u5FFD\u7565\u89C4\u5219\uFF08\u4F8B\u5982 **/.apm/**\uFF09\uFF0C\u907F\u514D\u672C\u5730\u4F1A\u8BDD\u4E0E\u90E8\u7F72\u51ED\u636E\u88AB\u63D0\u4EA4\u3002`
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
const content = readFileSync3(fsGitignorePath, "utf8");
|
|
805
|
-
if (!content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
|
|
806
|
-
throw new Error(
|
|
807
|
-
`\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u7684 .gitignore \u672A\u5FFD\u7565 .apm\uFF0C\u8BF7\u6DFB\u52A0 **/.apm/** \u6216 .apm/\uFF0C\u907F\u514D\u672C\u5730\u4F1A\u8BDD\u4E0E\u90E8\u7F72\u51ED\u636E\u88AB\u63D0\u4EA4\u3002`
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
async function ensureDirExists(dir) {
|
|
812
|
-
mkdirSync3(dir, { recursive: true });
|
|
813
|
-
}
|
|
814
|
-
async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
|
|
815
|
-
const dir = workspaceApmDir(cwd);
|
|
816
|
-
const fsDir = toFsPath(dir);
|
|
817
|
-
if (!existsSync2(fsDir)) {
|
|
818
|
-
mkdirSync3(fsDir, { recursive: true });
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
const st = statSync(fsDir);
|
|
822
|
-
if (!st.isDirectory()) {
|
|
823
|
-
throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${dir}`);
|
|
824
|
-
}
|
|
825
|
-
if (readdirSync(fsDir).length > 0) {
|
|
826
|
-
throw new Error(
|
|
827
|
-
"[apm] .apm \u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\uFF0C\u8BF7\u5148\u5907\u4EFD\u3001\u6E05\u7A7A\u6216\u5220\u9664\u540E\u518D\u6267\u884C init"
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
var WORKSPACE_TEMPLATE_SUBDIRS = [
|
|
832
|
-
"sessions",
|
|
833
|
-
"skills",
|
|
834
|
-
"rules",
|
|
835
|
-
"deploy"
|
|
836
|
-
];
|
|
837
|
-
function shouldSkipTemplateEntry(name) {
|
|
838
|
-
return name === ".DS_Store" || name === "Thumbs.db";
|
|
839
|
-
}
|
|
840
|
-
function copyTemplateEntry(src, dest) {
|
|
841
|
-
const fsSrc = toFsPath(src);
|
|
842
|
-
const fsDest = toFsPath(dest);
|
|
843
|
-
const st = statSync(fsSrc);
|
|
844
|
-
if (st.isDirectory()) {
|
|
845
|
-
mkdirSync3(fsDest, { recursive: true });
|
|
846
|
-
for (const name of readdirSync(fsSrc)) {
|
|
847
|
-
if (shouldSkipTemplateEntry(name)) continue;
|
|
848
|
-
copyTemplateEntry(join2(src, name), join2(dest, name));
|
|
849
|
-
}
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
if (!st.isFile()) return;
|
|
853
|
-
mkdirSync3(toFsPath(dirname2(dest)), { recursive: true });
|
|
854
|
-
copyFileSync(fsSrc, fsDest);
|
|
855
|
-
}
|
|
856
|
-
function assertTemplateCopiedToApm(apmDir, workdir) {
|
|
857
|
-
const required = [
|
|
858
|
-
"AGENTS.md",
|
|
859
|
-
"apm.config.json",
|
|
860
|
-
"rules",
|
|
861
|
-
"skills",
|
|
862
|
-
"sessions"
|
|
863
|
-
];
|
|
864
|
-
for (const item of required) {
|
|
865
|
-
const path = join2(apmDir, item);
|
|
866
|
-
if (!existsSync2(toFsPath(path))) {
|
|
867
|
-
throw new Error(`[apm] \u521D\u59CB\u5316\u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5C11: ${path}`);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
const leakedRules = join2(workdir, "rules");
|
|
871
|
-
const apmRules = join2(apmDir, "rules");
|
|
872
|
-
if (existsSync2(toFsPath(leakedRules)) && !existsSync2(toFsPath(join2(apmRules, "reply.md")))) {
|
|
873
|
-
throw new Error(
|
|
874
|
-
`[apm] \u6A21\u677F\u88AB\u590D\u5236\u5230\u9519\u8BEF\u4F4D\u7F6E: ${leakedRules}\uFF08\u5E94\u5728 ${apmRules}\uFF09`
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
async function copyTemplateFiles(targetDir, workdir = resolveWorkdirPath()) {
|
|
879
|
-
const resolvedTarget = resolve3(targetDir);
|
|
880
|
-
const templateDir = resolve3(CLI_TEMPLATE_DIR);
|
|
881
|
-
const fsTemplateDir = toFsPath(templateDir);
|
|
882
|
-
if (!existsSync2(fsTemplateDir)) {
|
|
883
|
-
throw new Error(`[apm] \u672A\u627E\u5230 CLI \u6A21\u677F\u76EE\u5F55: ${templateDir}`);
|
|
884
|
-
}
|
|
885
|
-
const dirStat = statSync(fsTemplateDir);
|
|
886
|
-
if (!dirStat.isDirectory()) {
|
|
887
|
-
throw new Error(`[apm] CLI \u6A21\u677F\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55: ${templateDir}`);
|
|
888
|
-
}
|
|
889
|
-
const entries = readdirSync(fsTemplateDir).filter(
|
|
890
|
-
(name) => !shouldSkipTemplateEntry(name)
|
|
891
|
-
);
|
|
892
|
-
if (entries.length === 0) {
|
|
893
|
-
throw new Error(`[apm] CLI \u6A21\u677F\u76EE\u5F55\u4E3A\u7A7A: ${templateDir}`);
|
|
894
|
-
}
|
|
895
|
-
mkdirSync3(toFsPath(resolvedTarget), { recursive: true });
|
|
896
|
-
for (const name of entries) {
|
|
897
|
-
copyTemplateEntry(join2(templateDir, name), join2(resolvedTarget, name));
|
|
898
|
-
}
|
|
899
|
-
for (const subdir of WORKSPACE_TEMPLATE_SUBDIRS) {
|
|
900
|
-
mkdirSync3(toFsPath(join2(resolvedTarget, subdir)), { recursive: true });
|
|
901
|
-
}
|
|
902
|
-
assertTemplateCopiedToApm(resolvedTarget, resolve3(workdir));
|
|
1383
|
+
return original.call(this, type, listener, options);
|
|
1384
|
+
};
|
|
1385
|
+
console.log(
|
|
1386
|
+
"[apm:abort-debug] \u5DF2\u542F\u7528 AbortSignal \u8C03\u8BD5\uFF08APM_DEBUG_ABORT_SIGNAL\uFF09"
|
|
1387
|
+
);
|
|
903
1388
|
}
|
|
904
1389
|
|
|
905
1390
|
// src/commands/append-message.ts
|
|
@@ -1078,11 +1563,99 @@ function createUpsertWebIdePlanTool(options) {
|
|
|
1078
1563
|
};
|
|
1079
1564
|
}
|
|
1080
1565
|
|
|
1566
|
+
// src/commands/connect/webide-test-case-tools.ts
|
|
1567
|
+
function asString2(value) {
|
|
1568
|
+
return typeof value === "string" ? value.trim() : "";
|
|
1569
|
+
}
|
|
1570
|
+
function asStringArray(value) {
|
|
1571
|
+
if (!Array.isArray(value)) return [];
|
|
1572
|
+
return value.map((item) => typeof item === "string" ? item.trim() : String(item)).filter(Boolean);
|
|
1573
|
+
}
|
|
1574
|
+
function parseCases(raw) {
|
|
1575
|
+
if (!Array.isArray(raw)) return [];
|
|
1576
|
+
const cases = [];
|
|
1577
|
+
for (const [index, item] of raw.entries()) {
|
|
1578
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1579
|
+
const row = item;
|
|
1580
|
+
const name = asString2(row.name);
|
|
1581
|
+
if (!name) continue;
|
|
1582
|
+
const steps = asStringArray(row.steps);
|
|
1583
|
+
if (steps.length === 0) continue;
|
|
1584
|
+
cases.push({
|
|
1585
|
+
name,
|
|
1586
|
+
description: asString2(row.description),
|
|
1587
|
+
steps,
|
|
1588
|
+
sortOrder: typeof row.sortOrder === "number" && Number.isFinite(row.sortOrder) ? row.sortOrder : index
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
return cases;
|
|
1592
|
+
}
|
|
1593
|
+
function createUpsertWebIdeTestCasesTool(options) {
|
|
1594
|
+
const { cfg, taskId } = options;
|
|
1595
|
+
return {
|
|
1596
|
+
description: "Persist generated automated test cases for this WebIDE task (full replace) and mark the workflow TEST_READY. Call once with the complete case list after reviewing the task and code changes.",
|
|
1597
|
+
inputSchema: {
|
|
1598
|
+
type: "object",
|
|
1599
|
+
properties: {
|
|
1600
|
+
cases: {
|
|
1601
|
+
type: "array",
|
|
1602
|
+
description: "Complete list of test cases to store on the platform",
|
|
1603
|
+
items: {
|
|
1604
|
+
type: "object",
|
|
1605
|
+
properties: {
|
|
1606
|
+
name: {
|
|
1607
|
+
type: "string",
|
|
1608
|
+
description: "Short stable case id / name"
|
|
1609
|
+
},
|
|
1610
|
+
description: {
|
|
1611
|
+
type: "string",
|
|
1612
|
+
description: "What this case verifies (Chinese ok)"
|
|
1613
|
+
},
|
|
1614
|
+
steps: {
|
|
1615
|
+
type: "array",
|
|
1616
|
+
items: { type: "string" },
|
|
1617
|
+
description: "Ordered human-readable steps"
|
|
1618
|
+
},
|
|
1619
|
+
sortOrder: {
|
|
1620
|
+
type: "number",
|
|
1621
|
+
description: "Optional display order (0-based)"
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
required: ["name", "description", "steps"]
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
},
|
|
1628
|
+
required: ["cases"]
|
|
1629
|
+
},
|
|
1630
|
+
execute: async (args) => {
|
|
1631
|
+
const cases = parseCases(args.cases);
|
|
1632
|
+
if (cases.length === 0) {
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
"UpsertWebIdeTestCases \u7F3A\u5C11\u6709\u6548 cases\uFF08\u6BCF\u6761\u9700 name \u4E0E\u975E\u7A7A steps\uFF09"
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const api = createApmApiClient(cfg);
|
|
1638
|
+
const result = await api.cli.webideReplaceTestCases({ taskId, cases });
|
|
1639
|
+
console.log(
|
|
1640
|
+
`[apm] UpsertWebIdeTestCases taskId=${taskId} count=${cases.length}`
|
|
1641
|
+
);
|
|
1642
|
+
return JSON.stringify(
|
|
1643
|
+
{ ok: true, count: result.count ?? cases.length },
|
|
1644
|
+
null,
|
|
1645
|
+
2
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1081
1651
|
// src/commands/connect/cursor-custom-tools.ts
|
|
1082
1652
|
var PLAN_MODE_ASK_QUESTION_HINT = `[SDK \u73AF\u5883\u8BF4\u660E]
|
|
1083
1653
|
AskQuestion \u5DF2\u901A\u8FC7 MCP \u670D\u52A1\u5668 custom-user-tools \u6CE8\u518C\uFF0C\u5DE5\u5177\u540D\u4E3A AskQuestion\u3002
|
|
1084
1654
|
\u9700\u8981\u5411\u7528\u6237\u786E\u8BA4\u65F6\uFF0C\u8BF7\u8C03\u7528 AskQuestion\uFF08\u7ECF CallMcpTool / custom-user-tools\uFF09\uFF0C\u4E0D\u8981\u5047\u8BBE IDE \u5185\u7F6E AskQuestion \u4E0D\u53EF\u7528\u3002
|
|
1085
1655
|
\u975E\u5FC5\u987B\u7684\u95EE\u9898\u53EF\u8DF3\u8FC7\uFF0C\u76F4\u63A5\u5B8C\u6210 createPlan\u3002`;
|
|
1656
|
+
var WORKSPACE_BOUNDARY_HINT = `[\u5DE5\u4F5C\u533A\u8FB9\u754C]
|
|
1657
|
+
\u6240\u6709\u6587\u4EF6\u8BFB\u5199\u3001\u641C\u7D22\uFF08Grep/Glob/Read\uFF09\u3001Shell \u5FC5\u987B\u4E25\u683C\u9650\u5236\u5728\u5F53\u524D\u5DE5\u4F5C\u533A cwd \u5185\u3002
|
|
1658
|
+
\u7981\u6B62\u8BBF\u95EE\u5DE5\u4F5C\u533A\u5916\u8DEF\u5F84\uFF08\u5982\u5176\u5B83\u9879\u76EE\u3001~/.cursor\u3001agent-transcripts\u3001\u7CFB\u7EDF\u76EE\u5F55\u7B49\uFF09\u3002`;
|
|
1086
1659
|
function createCursorCustomTools(cfg, messageId, options) {
|
|
1087
1660
|
const tools = {
|
|
1088
1661
|
...createAppendMessageCustomTools(
|
|
@@ -1104,6 +1677,10 @@ function createCursorCustomTools(cfg, messageId, options) {
|
|
|
1104
1677
|
cfg,
|
|
1105
1678
|
taskId: options.taskId
|
|
1106
1679
|
});
|
|
1680
|
+
tools.UpsertWebIdeTestCases = createUpsertWebIdeTestCasesTool({
|
|
1681
|
+
cfg,
|
|
1682
|
+
taskId: options.taskId
|
|
1683
|
+
});
|
|
1107
1684
|
}
|
|
1108
1685
|
return tools;
|
|
1109
1686
|
}
|
|
@@ -1115,14 +1692,19 @@ function withPlanModeToolHint(prompt, mode) {
|
|
|
1115
1692
|
|
|
1116
1693
|
${PLAN_MODE_ASK_QUESTION_HINT}`;
|
|
1117
1694
|
}
|
|
1695
|
+
function withWorkspaceBoundaryHint(prompt) {
|
|
1696
|
+
return `${prompt.trim()}
|
|
1697
|
+
|
|
1698
|
+
${WORKSPACE_BOUNDARY_HINT}`;
|
|
1699
|
+
}
|
|
1118
1700
|
|
|
1119
1701
|
// src/commands/connect/local-agent-store.ts
|
|
1120
|
-
import { mkdirSync as
|
|
1121
|
-
import { join as
|
|
1702
|
+
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
1703
|
+
import { join as join4 } from "node:path";
|
|
1122
1704
|
import { JsonlLocalAgentStore } from "@cursor/sdk";
|
|
1123
1705
|
function createWorkspaceLocalAgentStore(workdir) {
|
|
1124
|
-
const rootDir =
|
|
1125
|
-
|
|
1706
|
+
const rootDir = join4(workdir, ".apm", "cursor-agent-store");
|
|
1707
|
+
mkdirSync5(rootDir, { recursive: true });
|
|
1126
1708
|
return new JsonlLocalAgentStore(rootDir);
|
|
1127
1709
|
}
|
|
1128
1710
|
|
|
@@ -1160,7 +1742,8 @@ async function obtainAgent(ctx) {
|
|
|
1160
1742
|
local: {
|
|
1161
1743
|
cwd: ctx.cwd,
|
|
1162
1744
|
store: createWorkspaceLocalAgentStore(ctx.workdir),
|
|
1163
|
-
...ctx.customTools ? { customTools: ctx.customTools } : {}
|
|
1745
|
+
...ctx.customTools ? { customTools: ctx.customTools } : {},
|
|
1746
|
+
...ctx.enableSandbox ? { sandboxOptions: { enabled: true } } : {}
|
|
1164
1747
|
},
|
|
1165
1748
|
...ctx.mode ? { mode: ctx.mode } : {}
|
|
1166
1749
|
// mcpServers: createPlaywrightMcpServers(),
|
|
@@ -1169,14 +1752,14 @@ async function obtainAgent(ctx) {
|
|
|
1169
1752
|
const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
|
|
1170
1753
|
if (savedAgentId) {
|
|
1171
1754
|
try {
|
|
1172
|
-
const
|
|
1755
|
+
const agent = await Agent.resume(savedAgentId, agentOptions);
|
|
1173
1756
|
console.log(
|
|
1174
1757
|
`[apm] \u590D\u7528 Agent user=${ctx.user} agentId=${savedAgentId}${explicitAgentId ? "\uFF08\u53C2\u6570\u6307\u5B9A\uFF09" : ""}`
|
|
1175
1758
|
);
|
|
1176
1759
|
if (ctx.user) {
|
|
1177
|
-
saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user,
|
|
1760
|
+
saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
|
|
1178
1761
|
}
|
|
1179
|
-
return { agent
|
|
1762
|
+
return { agent, resumed: true };
|
|
1180
1763
|
} catch (err) {
|
|
1181
1764
|
console.warn(
|
|
1182
1765
|
`[apm] \u590D\u7528 Agent \u5931\u8D25\uFF08agentId=${savedAgentId}\uFF09\uFF0C\u56DE\u9000\u4E3A\u65B0\u5EFA:`,
|
|
@@ -1187,11 +1770,19 @@ async function obtainAgent(ctx) {
|
|
|
1187
1770
|
}
|
|
1188
1771
|
}
|
|
1189
1772
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1773
|
+
try {
|
|
1774
|
+
const agent = await Agent.create(agentOptions);
|
|
1775
|
+
if (ctx.user) {
|
|
1776
|
+
saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
|
|
1777
|
+
}
|
|
1778
|
+
return { agent, resumed: false };
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
if (ctx.enableSandbox && err instanceof Error && /sandbox/i.test(err.message)) {
|
|
1781
|
+
console.warn("[apm] sandbox \u4E0D\u53EF\u7528\uFF0C\u56DE\u9000\u4E3A\u65E0 sandbox:", err.message);
|
|
1782
|
+
return obtainAgent({ ...ctx, enableSandbox: false });
|
|
1783
|
+
}
|
|
1784
|
+
throw err;
|
|
1193
1785
|
}
|
|
1194
|
-
return { agent, resumed: false };
|
|
1195
1786
|
}
|
|
1196
1787
|
async function runCursorAgent(cfg, ctx, options) {
|
|
1197
1788
|
const signal = options?.signal;
|
|
@@ -1211,9 +1802,13 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1211
1802
|
enableWebIdePlanTools: options?.enableWebIdePlanTools,
|
|
1212
1803
|
taskId: options?.taskId
|
|
1213
1804
|
});
|
|
1214
|
-
const
|
|
1805
|
+
const enableSandbox = Boolean(options?.enableSandbox);
|
|
1806
|
+
let prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
|
|
1807
|
+
if (enableSandbox) {
|
|
1808
|
+
prompt = withWorkspaceBoundaryHint(prompt);
|
|
1809
|
+
}
|
|
1215
1810
|
console.log(
|
|
1216
|
-
`[apm] Cursor Agent \u5F00\u59CB messageId=${ctx.messageId} sessionId=${ctx.sessionId} cwd=${workdir}`
|
|
1811
|
+
`[apm] Cursor Agent \u5F00\u59CB messageId=${ctx.messageId} sessionId=${ctx.sessionId} cwd=${workdir} sandbox=${enableSandbox}`
|
|
1217
1812
|
);
|
|
1218
1813
|
const { agent, resumed } = await obtainAgent({
|
|
1219
1814
|
apiKey,
|
|
@@ -1224,7 +1819,8 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1224
1819
|
user: ctx.user,
|
|
1225
1820
|
mode: ctx.mode,
|
|
1226
1821
|
resumeAgentId: ctx.resumeAgentId,
|
|
1227
|
-
customTools
|
|
1822
|
+
customTools,
|
|
1823
|
+
enableSandbox
|
|
1228
1824
|
});
|
|
1229
1825
|
const eventSession = new EventSession(prompt);
|
|
1230
1826
|
const syncRemoteLog = options?.createRemoteLogSync ? options.createRemoteLogSync(agent.agentId) : options?.skipRemoteLogSync ? noopRemoteLogSync : createThrottledCursorMessageLogSync(
|
|
@@ -1348,17 +1944,17 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1348
1944
|
}
|
|
1349
1945
|
|
|
1350
1946
|
// src/commands/connect/webide-agent-registry.ts
|
|
1351
|
-
import { existsSync as
|
|
1352
|
-
import { dirname as dirname3, resolve as
|
|
1947
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1948
|
+
import { dirname as dirname3, resolve as resolve5 } from "node:path";
|
|
1353
1949
|
function registryPath2(workdir, taskId) {
|
|
1354
|
-
return
|
|
1950
|
+
return resolve5(workdir, ".apm", "webide", taskId, "cursor-agent.json");
|
|
1355
1951
|
}
|
|
1356
1952
|
function readRegistry2(path) {
|
|
1357
|
-
if (!
|
|
1953
|
+
if (!existsSync4(path)) {
|
|
1358
1954
|
return {};
|
|
1359
1955
|
}
|
|
1360
1956
|
try {
|
|
1361
|
-
const parsed = JSON.parse(
|
|
1957
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1362
1958
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1363
1959
|
const agentId = parsed.agentId;
|
|
1364
1960
|
if (typeof agentId === "string" && agentId.trim()) {
|
|
@@ -1370,8 +1966,8 @@ function readRegistry2(path) {
|
|
|
1370
1966
|
return {};
|
|
1371
1967
|
}
|
|
1372
1968
|
function writeRegistry2(path, registry) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1969
|
+
mkdirSync6(dirname3(path), { recursive: true });
|
|
1970
|
+
writeFileSync5(path, `${JSON.stringify(registry, null, 2)}
|
|
1375
1971
|
`, "utf8");
|
|
1376
1972
|
}
|
|
1377
1973
|
function loadWebIdeAgentId(workdir, taskId) {
|
|
@@ -1382,18 +1978,18 @@ function saveWebIdeAgentId(workdir, taskId, agentId) {
|
|
|
1382
1978
|
}
|
|
1383
1979
|
function clearWebIdeAgentId(workdir, taskId) {
|
|
1384
1980
|
const path = registryPath2(workdir, taskId);
|
|
1385
|
-
if (!
|
|
1981
|
+
if (!existsSync4(path)) return;
|
|
1386
1982
|
writeRegistry2(path, {});
|
|
1387
1983
|
}
|
|
1388
1984
|
|
|
1389
1985
|
// src/commands/connect/webide-ask-question.ts
|
|
1390
1986
|
import { setTimeout as delay } from "node:timers/promises";
|
|
1391
1987
|
var POLL_INTERVAL_MS = 2e3;
|
|
1392
|
-
function
|
|
1988
|
+
function asString3(value) {
|
|
1393
1989
|
return typeof value === "string" ? value.trim() : "";
|
|
1394
1990
|
}
|
|
1395
1991
|
function parseQuestions(args) {
|
|
1396
|
-
const title =
|
|
1992
|
+
const title = asString3(args.title) || void 0;
|
|
1397
1993
|
const raw = args.questions;
|
|
1398
1994
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
1399
1995
|
throw new Error("AskQuestion \u7F3A\u5C11 questions");
|
|
@@ -1402,16 +1998,16 @@ function parseQuestions(args) {
|
|
|
1402
1998
|
for (const item of raw) {
|
|
1403
1999
|
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1404
2000
|
const row = item;
|
|
1405
|
-
const id =
|
|
1406
|
-
const prompt =
|
|
2001
|
+
const id = asString3(row.id);
|
|
2002
|
+
const prompt = asString3(row.prompt);
|
|
1407
2003
|
const optionsRaw = row.options;
|
|
1408
2004
|
if (!id || !prompt || !Array.isArray(optionsRaw)) continue;
|
|
1409
2005
|
const options = [];
|
|
1410
2006
|
for (const opt of optionsRaw) {
|
|
1411
2007
|
if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
|
|
1412
2008
|
const o = opt;
|
|
1413
|
-
const oid =
|
|
1414
|
-
const label =
|
|
2009
|
+
const oid = asString3(o.id);
|
|
2010
|
+
const label = asString3(o.label);
|
|
1415
2011
|
if (oid && label) options.push({ id: oid, label });
|
|
1416
2012
|
}
|
|
1417
2013
|
if (options.length < 2) {
|
|
@@ -1500,14 +2096,14 @@ var MinioClient = class {
|
|
|
1500
2096
|
async deleteObjectsByPrefix(bucket, prefix) {
|
|
1501
2097
|
const objectsStream = this.inner.listObjectsV2(bucket, prefix, true);
|
|
1502
2098
|
const keys = [];
|
|
1503
|
-
await new Promise((
|
|
2099
|
+
await new Promise((resolve6, reject) => {
|
|
1504
2100
|
objectsStream.on("data", (obj) => {
|
|
1505
2101
|
if (obj.name) {
|
|
1506
2102
|
keys.push(obj.name);
|
|
1507
2103
|
}
|
|
1508
2104
|
});
|
|
1509
2105
|
objectsStream.on("error", reject);
|
|
1510
|
-
objectsStream.on("end",
|
|
2106
|
+
objectsStream.on("end", resolve6);
|
|
1511
2107
|
});
|
|
1512
2108
|
const chunkSize = 500;
|
|
1513
2109
|
for (let i = 0; i < keys.length; i += chunkSize) {
|
|
@@ -1675,147 +2271,12 @@ function resolveMessageReplyFallback(fallback) {
|
|
|
1675
2271
|
}
|
|
1676
2272
|
|
|
1677
2273
|
// src/commands/init.ts
|
|
1678
|
-
import { join as
|
|
1679
|
-
import { readFileSync as
|
|
1680
|
-
|
|
1681
|
-
// src/deployment-config-sync.ts
|
|
1682
|
-
import { join as join4 } from "path";
|
|
1683
|
-
import { writeFileSync as writeFileSync5 } from "fs";
|
|
1684
|
-
|
|
1685
|
-
// src/git-remote.ts
|
|
1686
|
-
import { execFile } from "child_process";
|
|
1687
|
-
import { promisify } from "util";
|
|
1688
|
-
var execFileAsync = promisify(execFile);
|
|
1689
|
-
async function tryReadGitOriginUrl(cwd) {
|
|
1690
|
-
try {
|
|
1691
|
-
const { stdout } = await execFileAsync(
|
|
1692
|
-
"git",
|
|
1693
|
-
["config", "--get", "remote.origin.url"],
|
|
1694
|
-
{ cwd, encoding: "utf8", maxBuffer: 1024 * 1024 }
|
|
1695
|
-
);
|
|
1696
|
-
const url = stdout.trim();
|
|
1697
|
-
return url || null;
|
|
1698
|
-
} catch {
|
|
1699
|
-
return null;
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// src/git-utils.ts
|
|
1704
|
-
import { execFile as execFile2 } from "child_process";
|
|
1705
|
-
import { promisify as promisify2 } from "util";
|
|
1706
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
1707
|
-
async function execGit(cwd, args, quiet = false) {
|
|
1708
|
-
try {
|
|
1709
|
-
const { stdout, stderr } = await execFileAsync2("git", args, {
|
|
1710
|
-
cwd,
|
|
1711
|
-
encoding: "utf8",
|
|
1712
|
-
maxBuffer: 10 * 1024 * 1024
|
|
1713
|
-
});
|
|
1714
|
-
if (!quiet && stderr.trim()) {
|
|
1715
|
-
process.stderr.write(stderr);
|
|
1716
|
-
}
|
|
1717
|
-
return stdout;
|
|
1718
|
-
} catch (err) {
|
|
1719
|
-
const e = err;
|
|
1720
|
-
const detail = (e.stderr ?? e.message ?? String(err)).trim();
|
|
1721
|
-
throw new Error(
|
|
1722
|
-
`[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
|
|
1723
|
-
);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
async function isGitRepo(cwd) {
|
|
1727
|
-
try {
|
|
1728
|
-
await execGit(cwd, ["rev-parse", "--git-dir"], true);
|
|
1729
|
-
return true;
|
|
1730
|
-
} catch {
|
|
1731
|
-
return false;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
async function resolveGitRepoRoot(cwd) {
|
|
1735
|
-
return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
|
|
1736
|
-
}
|
|
1737
|
-
async function hasUpstream(cwd) {
|
|
1738
|
-
try {
|
|
1739
|
-
await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
|
|
1740
|
-
return true;
|
|
1741
|
-
} catch {
|
|
1742
|
-
return false;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
|
|
1746
|
-
async function commitAndPushGitignore(workdir) {
|
|
1747
|
-
if (!await isGitRepo(workdir)) {
|
|
1748
|
-
console.log("[apm] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u8BF7\u624B\u52A8\u63D0\u4EA4 .gitignore");
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
await execGit(workdir, ["add", "--", ".gitignore"]);
|
|
1752
|
-
await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
|
|
1753
|
-
console.log(`[apm] \u5DF2\u63D0\u4EA4 .gitignore: ${GITIGNORE_COMMIT_MESSAGE}`);
|
|
1754
|
-
const originUrl = await tryReadGitOriginUrl(workdir);
|
|
1755
|
-
if (!originUrl) {
|
|
1756
|
-
console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
|
|
1757
|
-
return;
|
|
1758
|
-
}
|
|
1759
|
-
if (await hasUpstream(workdir)) {
|
|
1760
|
-
await execGit(workdir, ["push"]);
|
|
1761
|
-
} else {
|
|
1762
|
-
await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
|
|
1763
|
-
}
|
|
1764
|
-
console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// src/baseline-resolve.ts
|
|
1768
|
-
function formatBaselineDiagnostic(workdirPath, baselineWorkdirPath, diagnostic) {
|
|
1769
|
-
return diagnostic?.message ?? `\u672A\u5728\u5E73\u53F0\u627E\u5230\u4E0E\u5F53\u524D\u76EE\u5F55\u5339\u914D\u7684\u5DE5\u4F5C\u76EE\u5F55\u767B\u8BB0\uFF1A${workdirPath}\uFF08\u89C4\u8303\u5316\uFF1A${baselineWorkdirPath}\uFF09
|
|
1770
|
-
\u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
|
|
1771
|
-
}
|
|
1772
|
-
async function matchRepositoryByGitRemote(api, workdirPath) {
|
|
1773
|
-
const gitRoot = await resolveGitRepoRoot(workdirPath);
|
|
1774
|
-
const gitUrl = await tryReadGitOriginUrl(gitRoot);
|
|
1775
|
-
if (!gitUrl) {
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1778
|
-
const matched = await api.cli.matchRepository({ url: gitUrl });
|
|
1779
|
-
const repositoryId = matched.repositoryId?.trim();
|
|
1780
|
-
const defaultBranch = matched.defaultBranch?.trim();
|
|
1781
|
-
if (!repositoryId || !defaultBranch) {
|
|
1782
|
-
return null;
|
|
1783
|
-
}
|
|
1784
|
-
console.log(
|
|
1785
|
-
`[apm] \u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u672A\u5339\u914D\uFF0C\u5DF2\u901A\u8FC7 git remote \u5173\u8054\u4ED3\u5E93: ${gitUrl}`
|
|
1786
|
-
);
|
|
1787
|
-
return { repositoryId, defaultBranch };
|
|
1788
|
-
}
|
|
1789
|
-
async function resolveWorkspaceBaseline(api, workdirPath) {
|
|
1790
|
-
const baseline = await api.cli.workspaceBaseline({ workdirPath });
|
|
1791
|
-
const repositoryId = baseline.repositoryId?.trim();
|
|
1792
|
-
const defaultBranch = baseline.defaultBranch?.trim();
|
|
1793
|
-
if (repositoryId && defaultBranch) {
|
|
1794
|
-
return {
|
|
1795
|
-
repositoryId,
|
|
1796
|
-
defaultBranch,
|
|
1797
|
-
workdirPath: baseline.workdirPath,
|
|
1798
|
-
matchedViaGitRemote: false
|
|
1799
|
-
};
|
|
1800
|
-
}
|
|
1801
|
-
const viaGit = await matchRepositoryByGitRemote(api, workdirPath);
|
|
1802
|
-
if (viaGit) {
|
|
1803
|
-
return {
|
|
1804
|
-
...viaGit,
|
|
1805
|
-
workdirPath: baseline.workdirPath,
|
|
1806
|
-
matchedViaGitRemote: true
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
throw new Error(
|
|
1810
|
-
`[apm] ${formatBaselineDiagnostic(
|
|
1811
|
-
workdirPath,
|
|
1812
|
-
baseline.workdirPath,
|
|
1813
|
-
baseline.diagnostic
|
|
1814
|
-
)}`
|
|
1815
|
-
);
|
|
1816
|
-
}
|
|
2274
|
+
import { join as join7 } from "path";
|
|
2275
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
1817
2276
|
|
|
1818
2277
|
// src/deployment-config-sync.ts
|
|
2278
|
+
import { join as join5 } from "path";
|
|
2279
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
1819
2280
|
var TEMPLATE_HINT = "\u4FDD\u7559\u6A21\u677F .apm/apm.config.json";
|
|
1820
2281
|
var SYNC_HINT = "\u767B\u8BB0\u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u3001\u7ED1\u5B9A\u4ED3\u5E93\u540E\uFF0C\u53EF\u6267\u884C: apm sync-deploy-config";
|
|
1821
2282
|
async function resolveRepositoryIdForSync(api, workdirPath) {
|
|
@@ -1870,8 +2331,8 @@ ${diagnostic ?? ""}
|
|
|
1870
2331
|
return { synced: false, repositoryId };
|
|
1871
2332
|
}
|
|
1872
2333
|
const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
|
|
1873
|
-
const apmConfigPath = toFsPath(
|
|
1874
|
-
|
|
2334
|
+
const apmConfigPath = toFsPath(join5(targetApmDir, "apm.config.json"));
|
|
2335
|
+
writeFileSync6(apmConfigPath, `${JSON.stringify(parsed, null, 2)}
|
|
1875
2336
|
`, "utf8");
|
|
1876
2337
|
console.log(`[apm] \u5DF2\u540C\u6B65\u5E73\u53F0\u90E8\u7F72\u914D\u7F6E: ${config.name}`);
|
|
1877
2338
|
console.log("[apm] \u5DF2\u5199\u5165 .apm/apm.config.json");
|
|
@@ -1880,20 +2341,20 @@ ${diagnostic ?? ""}
|
|
|
1880
2341
|
|
|
1881
2342
|
// src/repository-project-documents-sync.ts
|
|
1882
2343
|
import {
|
|
1883
|
-
existsSync as
|
|
1884
|
-
readdirSync as
|
|
1885
|
-
readFileSync as
|
|
2344
|
+
existsSync as existsSync5,
|
|
2345
|
+
readdirSync as readdirSync3,
|
|
2346
|
+
readFileSync as readFileSync6,
|
|
1886
2347
|
rmSync,
|
|
1887
|
-
writeFileSync as
|
|
2348
|
+
writeFileSync as writeFileSync7
|
|
1888
2349
|
} from "fs";
|
|
1889
|
-
import { dirname as dirname4, join as
|
|
2350
|
+
import { dirname as dirname4, join as join6, relative as relative2, sep } from "path";
|
|
1890
2351
|
var MANIFEST_FILE = "manifest.json";
|
|
1891
2352
|
function projectDocumentsDir(apmRoot) {
|
|
1892
|
-
return
|
|
2353
|
+
return join6(apmRoot ?? workspaceApmDir(), "project");
|
|
1893
2354
|
}
|
|
1894
2355
|
function projectDocumentLocalPath(apmRoot, documentPath) {
|
|
1895
2356
|
const normalized = normalizeLocalDocumentPath(documentPath);
|
|
1896
|
-
return
|
|
2357
|
+
return join6(projectDocumentsDir(apmRoot), ...normalized.split("/"));
|
|
1897
2358
|
}
|
|
1898
2359
|
function normalizeLocalDocumentPath(path) {
|
|
1899
2360
|
const trimmed = path.trim().replace(/\\/g, "/");
|
|
@@ -1907,13 +2368,13 @@ function normalizeLocalDocumentPath(path) {
|
|
|
1907
2368
|
return segments.join("/");
|
|
1908
2369
|
}
|
|
1909
2370
|
function readLocalManifest(apmRoot) {
|
|
1910
|
-
const
|
|
1911
|
-
if (!
|
|
2371
|
+
const manifestPath2 = join6(projectDocumentsDir(apmRoot), MANIFEST_FILE);
|
|
2372
|
+
if (!existsSync5(manifestPath2)) {
|
|
1912
2373
|
return null;
|
|
1913
2374
|
}
|
|
1914
2375
|
try {
|
|
1915
2376
|
return JSON.parse(
|
|
1916
|
-
|
|
2377
|
+
readFileSync6(manifestPath2, "utf8")
|
|
1917
2378
|
);
|
|
1918
2379
|
} catch {
|
|
1919
2380
|
return null;
|
|
@@ -1990,20 +2451,20 @@ ${diagnostic ?? ""}`
|
|
|
1990
2451
|
for (const doc of list) {
|
|
1991
2452
|
const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
|
|
1992
2453
|
await ensureDirExists(dirname4(absPath));
|
|
1993
|
-
|
|
2454
|
+
writeFileSync7(absPath, doc.content, "utf8");
|
|
1994
2455
|
downloaded += 1;
|
|
1995
2456
|
}
|
|
1996
2457
|
}
|
|
1997
2458
|
let deleted = 0;
|
|
1998
2459
|
for (const path of deleteLocal) {
|
|
1999
2460
|
const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path));
|
|
2000
|
-
if (
|
|
2461
|
+
if (existsSync5(absPath)) {
|
|
2001
2462
|
rmSync(absPath, { force: true });
|
|
2002
2463
|
deleted += 1;
|
|
2003
2464
|
}
|
|
2004
2465
|
}
|
|
2005
|
-
|
|
2006
|
-
toFsPath(
|
|
2466
|
+
writeFileSync7(
|
|
2467
|
+
toFsPath(join6(projectDir, MANIFEST_FILE)),
|
|
2007
2468
|
`${JSON.stringify(remoteManifest, null, 2)}
|
|
2008
2469
|
`,
|
|
2009
2470
|
"utf8"
|
|
@@ -2038,11 +2499,11 @@ async function ensureWorkspaceInitialized(workdir, options) {
|
|
|
2038
2499
|
await syncRepositoryProjectDocumentsPull(workdir, apmDir);
|
|
2039
2500
|
const trimmedName = options?.name?.trim();
|
|
2040
2501
|
if (trimmedName) {
|
|
2041
|
-
const apmConfigPath = toFsPath(
|
|
2042
|
-
const config =
|
|
2502
|
+
const apmConfigPath = toFsPath(join7(apmDir, "apm.config.json"));
|
|
2503
|
+
const config = readFileSync7(apmConfigPath, "utf8");
|
|
2043
2504
|
const configJson = JSON.parse(config);
|
|
2044
2505
|
configJson.name = trimmedName;
|
|
2045
|
-
|
|
2506
|
+
writeFileSync8(
|
|
2046
2507
|
apmConfigPath,
|
|
2047
2508
|
`${JSON.stringify(configJson, null, 2)}
|
|
2048
2509
|
`,
|
|
@@ -2053,6 +2514,149 @@ async function ensureWorkspaceInitialized(workdir, options) {
|
|
|
2053
2514
|
return { didInit: true, syncResult };
|
|
2054
2515
|
}
|
|
2055
2516
|
|
|
2517
|
+
// src/commands/connect/pre-step-cache.ts
|
|
2518
|
+
function sessionWorkdirKey(sessionId, workdir) {
|
|
2519
|
+
return `${sessionId}\0${workdir}`;
|
|
2520
|
+
}
|
|
2521
|
+
var lastBranchKey = null;
|
|
2522
|
+
function shouldRunBranch(sessionId, workdir) {
|
|
2523
|
+
return lastBranchKey !== sessionWorkdirKey(sessionId, workdir);
|
|
2524
|
+
}
|
|
2525
|
+
function markBranchDone(sessionId, workdir) {
|
|
2526
|
+
lastBranchKey = sessionWorkdirKey(sessionId, workdir);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/commands/connect/webide-draft-prs.ts
|
|
2530
|
+
var DEVELOP_ACTIONS = /* @__PURE__ */ new Set(["start-develop", "skip-plan"]);
|
|
2531
|
+
function shouldEnsureWebIdeDraftPullRequests(action) {
|
|
2532
|
+
return DEVELOP_ACTIONS.has(action);
|
|
2533
|
+
}
|
|
2534
|
+
async function ensureWebIdeDraftPullRequests(cfg, taskId, workdir) {
|
|
2535
|
+
const api = createApmApiClient(cfg);
|
|
2536
|
+
let repoRoots;
|
|
2537
|
+
try {
|
|
2538
|
+
const manifest = loadWorkspaceReposCache(workdir);
|
|
2539
|
+
repoRoots = resolveWorkspaceRepoAbsolutePaths(manifest);
|
|
2540
|
+
} catch (err) {
|
|
2541
|
+
console.warn(
|
|
2542
|
+
"[apm] WebIDE \u8349\u7A3F PR\uFF1A\u8BFB\u53D6\u4ED3\u5E93\u6E05\u5355\u5931\u8D25:",
|
|
2543
|
+
err instanceof Error ? err.message : err
|
|
2544
|
+
);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
console.log(
|
|
2548
|
+
`[apm] WebIDE \u8349\u7A3F PR\uFF1A\u51C6\u5907\u4E3A ${repoRoots.length} \u4E2A\u4ED3\u5E93\u521B\u5EFA feat/task-${taskId}`
|
|
2549
|
+
);
|
|
2550
|
+
for (const gitRoot of repoRoots) {
|
|
2551
|
+
const label = formatRepoLabel(workdir, gitRoot);
|
|
2552
|
+
try {
|
|
2553
|
+
const originUrl = await tryReadGitOriginUrl(gitRoot);
|
|
2554
|
+
if (!originUrl) {
|
|
2555
|
+
console.warn(
|
|
2556
|
+
`[apm] WebIDE \u8349\u7A3F PR\uFF1A\u8DF3\u8FC7 ${label}\uFF08\u65E0 remote.origin.url\uFF09`
|
|
2557
|
+
);
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
const matched = await api.cli.matchRepository({ url: originUrl });
|
|
2561
|
+
const repositoryId = matched.repositoryId?.trim();
|
|
2562
|
+
if (!repositoryId) {
|
|
2563
|
+
console.warn(
|
|
2564
|
+
`[apm] WebIDE \u8349\u7A3F PR\uFF1A\u8DF3\u8FC7 ${label}\uFF08\u5E73\u53F0\u672A\u767B\u8BB0\u4ED3\u5E93 ${originUrl}\uFF09`
|
|
2565
|
+
);
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
const pr = await api.cli.createWebIdeDraftPullRequest({
|
|
2569
|
+
taskId,
|
|
2570
|
+
repositoryId
|
|
2571
|
+
});
|
|
2572
|
+
console.log(
|
|
2573
|
+
`[apm] WebIDE \u8349\u7A3F PR\uFF1A${label} \u2192 #${pr.number} ${pr.state} ${pr.url || ""}`
|
|
2574
|
+
);
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
console.warn(
|
|
2577
|
+
`[apm] WebIDE \u8349\u7A3F PR\uFF1A${label} \u5931\u8D25:`,
|
|
2578
|
+
err instanceof Error ? err.message : err
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/commands/connect/ensure-workspace-permissions.ts
|
|
2585
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
2586
|
+
import { join as join8 } from "path";
|
|
2587
|
+
var WEBIDE_MCP_ALLOWLIST = ["custom-user-tools:*"];
|
|
2588
|
+
function parsePermissionsConfig(raw) {
|
|
2589
|
+
try {
|
|
2590
|
+
const parsed = JSON.parse(raw);
|
|
2591
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2592
|
+
return void 0;
|
|
2593
|
+
}
|
|
2594
|
+
return parsed;
|
|
2595
|
+
} catch {
|
|
2596
|
+
return void 0;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
function mergeMcpAllowlist(existing) {
|
|
2600
|
+
const merged = /* @__PURE__ */ new Set([
|
|
2601
|
+
...existing ?? [],
|
|
2602
|
+
...WEBIDE_MCP_ALLOWLIST
|
|
2603
|
+
]);
|
|
2604
|
+
return [...merged];
|
|
2605
|
+
}
|
|
2606
|
+
function needsMcpAllowlistUpdate(existing) {
|
|
2607
|
+
const current = existing ?? [];
|
|
2608
|
+
return WEBIDE_MCP_ALLOWLIST.some((entry) => !current.includes(entry));
|
|
2609
|
+
}
|
|
2610
|
+
function ensureWorkspacePermissionsConfig(workdir) {
|
|
2611
|
+
const cursorDir = toFsPath(join8(workdir, ".cursor"));
|
|
2612
|
+
const permissionsPath = toFsPath(join8(cursorDir, "permissions.json"));
|
|
2613
|
+
let config = {};
|
|
2614
|
+
if (existsSync6(permissionsPath)) {
|
|
2615
|
+
const raw = readFileSync8(permissionsPath, "utf8");
|
|
2616
|
+
const parsed = parsePermissionsConfig(raw);
|
|
2617
|
+
if (!parsed) {
|
|
2618
|
+
console.warn(
|
|
2619
|
+
`[apm] \u5DE5\u4F5C\u533A permissions.json \u65E0\u6CD5\u89E3\u6790\uFF0C\u8DF3\u8FC7\u5199\u5165\uFF1A${permissionsPath}`
|
|
2620
|
+
);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
config = parsed;
|
|
2624
|
+
}
|
|
2625
|
+
if (!needsMcpAllowlistUpdate(config.mcpAllowlist)) {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
config.mcpAllowlist = mergeMcpAllowlist(config.mcpAllowlist);
|
|
2629
|
+
mkdirSync7(cursorDir, { recursive: true });
|
|
2630
|
+
writeFileSync9(
|
|
2631
|
+
permissionsPath,
|
|
2632
|
+
`${JSON.stringify(config, null, 2)}
|
|
2633
|
+
`,
|
|
2634
|
+
"utf8"
|
|
2635
|
+
);
|
|
2636
|
+
console.log(
|
|
2637
|
+
`[apm] \u5DF2\u66F4\u65B0\u5DE5\u4F5C\u533A permissions \u914D\u7F6E\uFF08mcpAllowlist\uFF09\uFF1A${permissionsPath}`
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// src/commands/connect/ensure-workspace-sandbox.ts
|
|
2642
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync10, existsSync as existsSync7 } from "fs";
|
|
2643
|
+
import { join as join9 } from "path";
|
|
2644
|
+
var DEFAULT_SANDBOX_JSON = `{
|
|
2645
|
+
"type": "workspace_readwrite",
|
|
2646
|
+
"networkPolicy": {
|
|
2647
|
+
"default": "allow"
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
`;
|
|
2651
|
+
function ensureWorkspaceSandboxConfig(workdir) {
|
|
2652
|
+
const cursorDir = toFsPath(join9(workdir, ".cursor"));
|
|
2653
|
+
const sandboxPath = toFsPath(join9(cursorDir, "sandbox.json"));
|
|
2654
|
+
if (existsSync7(sandboxPath)) return;
|
|
2655
|
+
mkdirSync8(cursorDir, { recursive: true });
|
|
2656
|
+
writeFileSync10(sandboxPath, DEFAULT_SANDBOX_JSON, "utf8");
|
|
2657
|
+
console.log(`[apm] \u5DF2\u5199\u5165\u5DE5\u4F5C\u533A sandbox \u914D\u7F6E\uFF1A${sandboxPath}`);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2056
2660
|
// src/commands/connect/handle-webide-message.ts
|
|
2057
2661
|
async function updateStatus(cfg, messageId, status) {
|
|
2058
2662
|
const api = createApmApiClient(cfg);
|
|
@@ -2070,6 +2674,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
|
|
|
2070
2674
|
const workdir = requireRemoteWorkdir(msg.workdir);
|
|
2071
2675
|
const messageId = msg.messageId;
|
|
2072
2676
|
const taskId = msg.taskId;
|
|
2677
|
+
const branchCacheKey = `webide:${taskId}`;
|
|
2073
2678
|
console.log(
|
|
2074
2679
|
`[apm] webide-message action=${msg.action} taskId=${taskId} messageId=${messageId}`
|
|
2075
2680
|
);
|
|
@@ -2080,6 +2685,25 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
|
|
|
2080
2685
|
if (!didInit) {
|
|
2081
2686
|
assertApmGitignoredInRepo(workdir);
|
|
2082
2687
|
}
|
|
2688
|
+
ensureWorkspaceSandboxConfig(workdir);
|
|
2689
|
+
ensureWorkspacePermissionsConfig(workdir);
|
|
2690
|
+
resolveWorkspaceRepos(workdir);
|
|
2691
|
+
if (shouldRunBranch(branchCacheKey, workdir)) {
|
|
2692
|
+
if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
|
|
2693
|
+
const branchResult = await runTaskBranch(taskId, { cwd: workdir });
|
|
2694
|
+
markBranchDone(branchCacheKey, workdir);
|
|
2695
|
+
console.log(
|
|
2696
|
+
`[apm] webide branch ready kind=${branchResult.kind} branch=${branchResult.branch} repos=${branchResult.repos.length}`
|
|
2697
|
+
);
|
|
2698
|
+
} else {
|
|
2699
|
+
console.log(
|
|
2700
|
+
`[apm] step=branch skipped taskId=${taskId} workdir=${workdir}`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
if (shouldEnsureWebIdeDraftPullRequests(msg.action)) {
|
|
2704
|
+
if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
|
|
2705
|
+
await ensureWebIdeDraftPullRequests(cfg, taskId, workdir);
|
|
2706
|
+
}
|
|
2083
2707
|
const savedAgentId = loadWebIdeAgentId(workdir, taskId);
|
|
2084
2708
|
const logSyncRef = { current: null };
|
|
2085
2709
|
const outcome = await runCursorAgent(
|
|
@@ -2103,6 +2727,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
|
|
|
2103
2727
|
signal
|
|
2104
2728
|
}),
|
|
2105
2729
|
enableWebIdePlanTools: true,
|
|
2730
|
+
enableSandbox: true,
|
|
2106
2731
|
taskId,
|
|
2107
2732
|
createRemoteLogSync: (agentId) => {
|
|
2108
2733
|
saveWebIdeAgentId(workdir, taskId, agentId);
|