ai-project-manage-cli 7.1.4 → 7.1.6

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.
@@ -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"
@@ -240,32 +248,691 @@ function normalizeWorkdirPath(path) {
240
248
  if (normalized.startsWith("//?/")) {
241
249
  normalized = normalized.slice(4);
242
250
  }
243
- const windowsDrive = /^([A-Za-z]:)\/*(.*)$/.exec(normalized);
244
- if (windowsDrive) {
245
- const drive = windowsDrive[1].toLowerCase();
246
- const rest = windowsDrive[2].replace(/\/+/g, "/").replace(/\/$/, "");
247
- return rest ? `${drive}/${rest}` : drive;
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`);
248
780
  }
249
- normalized = normalized.replace(/\/+/g, "/");
250
- if (normalized.length > 1 && normalized.endsWith("/")) {
251
- normalized = normalized.slice(0, -1);
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
+ }
252
799
  }
253
- return normalized;
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;
254
806
  }
255
- function resolveWorkdirPath(cwd = process.cwd()) {
256
- const absolute = resolve(cwd);
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;
815
+ }
816
+
817
+ // src/commands/branch.ts
818
+ var TASK_BRANCH_PREFIX = "feat/task-";
819
+ async function localBranchExists(cwd, branch) {
257
820
  try {
258
- return normalizeWorkdirPath(realpathSync.native(absolute));
821
+ await execGit(
822
+ cwd,
823
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
824
+ true
825
+ );
826
+ return true;
259
827
  } catch {
260
- return normalizeWorkdirPath(absolute);
828
+ return false;
261
829
  }
262
830
  }
263
- function requireRemoteWorkdir(workdir) {
264
- const trimmed = typeof workdir === "string" ? workdir.trim() : "";
265
- if (!trimmed) {
266
- throw new Error("[apm] \u8FDC\u7A0B\u6D88\u606F\u7F3A\u5C11\u5DE5\u4F5C\u76EE\u5F55 workdir");
831
+ async function commitWorkingTreeIfDirty(cwd, message) {
832
+ await ensureGitRepo(cwd);
833
+ if (!await isWorkingTreeDirty(cwd)) {
834
+ return false;
267
835
  }
268
- return resolveWorkdirPath(trimmed);
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");
846
+ }
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
+ );
851
+ }
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
+ }
874
+ }
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
+ }
910
+ }
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
@@ -327,16 +994,16 @@ var EventSession = class {
327
994
  return;
328
995
  }
329
996
  if (formatedEvent.type === "tool_call") {
330
- const existingIndex = this.events.findIndex(
331
- (e) => e.type === "tool_call" && e.call_id === formatedEvent.call_id
332
- );
333
- if (existingIndex >= 0) {
334
- const existingToolCall = this.events[existingIndex];
335
- existingToolCall.args = formatedEvent.args;
336
- existingToolCall.result = formatedEvent.result;
337
- existingToolCall.status = formatedEvent.status;
338
- this.markDirty(existingIndex);
339
- return;
997
+ const callId = typeof formatedEvent.call_id === "string" ? formatedEvent.call_id.trim() : "";
998
+ if (callId) {
999
+ const existingIndex = this.events.findIndex(
1000
+ (e) => e.type === "tool_call" && typeof e.call_id === "string" && e.call_id === callId
1001
+ );
1002
+ if (existingIndex >= 0) {
1003
+ mergeToolCallEvent(this.events[existingIndex], formatedEvent);
1004
+ this.markDirty(existingIndex);
1005
+ return;
1006
+ }
340
1007
  }
341
1008
  this.events.push(formatedEvent);
342
1009
  this.markDirty(this.events.length - 1);
@@ -398,15 +1065,18 @@ var EventSession = class {
398
1065
  type: "thinking",
399
1066
  content: event.text || event.content || ""
400
1067
  };
401
- case "tool_call":
1068
+ case "tool_call": {
1069
+ const raw = event;
1070
+ const callId = String(raw.call_id ?? raw.callId ?? "").trim();
402
1071
  return {
403
1072
  type: "tool_call",
404
1073
  args: event.args,
405
1074
  result: event.result,
406
1075
  status: event.status,
407
- call_id: event.call_id,
1076
+ call_id: callId,
408
1077
  name: event.name
409
1078
  };
1079
+ }
410
1080
  case "task":
411
1081
  return {
412
1082
  type: "task",
@@ -471,6 +1141,24 @@ var EventSession = class {
471
1141
  return this.events.map((event) => formatLogEvent(event.type, event)).join("\n");
472
1142
  }
473
1143
  };
1144
+ function mergeToolCallEvent(existing, incoming) {
1145
+ if (incoming.name) {
1146
+ existing.name = incoming.name;
1147
+ }
1148
+ if (incoming.status) {
1149
+ existing.status = incoming.status;
1150
+ }
1151
+ if (incoming.args != null && typeof incoming.args === "object" && !Array.isArray(incoming.args) && Object.keys(incoming.args).length > 0) {
1152
+ existing.args = { ...existing.args ?? {}, ...incoming.args };
1153
+ }
1154
+ if (incoming.result != null) {
1155
+ if (typeof incoming.result === "object" && !Array.isArray(incoming.result) && typeof existing.result === "object" && existing.result != null && !Array.isArray(existing.result)) {
1156
+ existing.result = { ...existing.result, ...incoming.result };
1157
+ } else {
1158
+ existing.result = incoming.result;
1159
+ }
1160
+ }
1161
+ }
474
1162
  function formatLogEvent(type, event) {
475
1163
  if (type === "input") {
476
1164
  return `## \u7528\u6237\u8F93\u5165
@@ -511,17 +1199,17 @@ ${JSON.stringify(event, null, 2)}
511
1199
  }
512
1200
 
513
1201
  // src/commands/connect/agent-session-registry.ts
514
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
515
- import { dirname, resolve as resolve2 } from "node:path";
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";
516
1204
  function registryPath(workdir, sessionId) {
517
- return resolve2(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
1205
+ return resolve4(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
518
1206
  }
519
1207
  function readRegistry(path) {
520
- if (!existsSync(path)) {
1208
+ if (!existsSync3(path)) {
521
1209
  return {};
522
1210
  }
523
1211
  try {
524
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1212
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
525
1213
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
526
1214
  const result = {};
527
1215
  for (const [key, value] of Object.entries(
@@ -538,8 +1226,8 @@ function readRegistry(path) {
538
1226
  return {};
539
1227
  }
540
1228
  function writeRegistry(path, registry) {
541
- mkdirSync2(dirname(path), { recursive: true });
542
- writeFileSync2(path, `${JSON.stringify(registry, null, 2)}
1229
+ mkdirSync4(dirname2(path), { recursive: true });
1230
+ writeFileSync4(path, `${JSON.stringify(registry, null, 2)}
543
1231
  `, "utf8");
544
1232
  }
545
1233
  function loadSessionAgentId(workdir, sessionId, user) {
@@ -670,215 +1358,33 @@ function installAbortSignalDebug() {
670
1358
  setMaxListeners(maxFromEnv);
671
1359
  console.log(
672
1360
  `[apm:abort-debug] setMaxListeners(${maxFromEnv}) via APM_ABORT_SIGNAL_MAX_LISTENERS`
673
- );
674
- }
675
- process.on("warning", (warning) => {
676
- if (warning.name !== "MaxListenersExceededWarning") return;
677
- console.warn(`[apm:abort-debug] ${warning.name}: ${warning.message}`);
678
- if (warning.stack) {
679
- console.warn(warning.stack);
680
- }
681
- });
682
- const proto = AbortSignal.prototype;
683
- const original = proto.addEventListener;
684
- proto.addEventListener = function(type, listener, options) {
685
- if (type === "abort") {
686
- const sig = this;
687
- const before = getEventListeners(sig, "abort").length;
688
- const max = getMaxListeners(sig);
689
- const stack = new Error("[apm:abort-debug] addEventListener stack").stack?.split("\n").slice(2, 8).join("\n") ?? "";
690
- console.log(
691
- `[apm:abort-debug] addEventListener("abort") before=${before} max=${max}
692
- ${stack}`
693
- );
694
- }
695
- return original.call(this, type, listener, options);
696
- };
697
- console.log(
698
- "[apm:abort-debug] \u5DF2\u542F\u7528 AbortSignal \u8C03\u8BD5\uFF08APM_DEBUG_ABORT_SIGNAL\uFF09"
699
- );
700
- }
701
-
702
- // src/command-utils.ts
703
- import {
704
- copyFileSync,
705
- existsSync as existsSync2,
706
- mkdirSync as mkdirSync3,
707
- readFileSync as readFileSync3,
708
- readdirSync,
709
- statSync,
710
- writeFileSync as writeFileSync3
711
- } from "fs";
712
- import { basename, dirname as dirname2, extname, join as join2, resolve as resolve3 } from "path";
713
- import { fileURLToPath } from "url";
714
- var __dirname = dirname2(fileURLToPath(import.meta.url));
715
- var CLI_TEMPLATE_DIR = resolve3(__dirname, "../template");
716
- function workspaceApmDir(cwd = resolveWorkdirPath()) {
717
- return resolve3(resolve3(cwd), ".apm");
718
- }
719
- function isWorkspaceApmInitialized(workdir) {
720
- const apmDir = workspaceApmDir(workdir);
721
- const fsApmDir = toFsPath(apmDir);
722
- if (!existsSync2(fsApmDir)) {
723
- return false;
724
- }
725
- const st = statSync(fsApmDir);
726
- if (!st.isDirectory()) {
727
- throw new Error(
728
- `\u5DE5\u4F5C\u76EE\u5F55 ${workdir} \u4E0B\u7684 .apm \u4E0D\u662F\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u63A5\u5165\u72B6\u6001\u3002`
729
- );
730
- }
731
- return readdirSync(fsApmDir).length > 0;
732
- }
733
- var APM_GITIGNORE_PATTERNS = [
734
- /^\.apm\/?$/,
735
- /^\.apm\/\*\*$/,
736
- /^\*\*\/\.apm\/?$/,
737
- /^\*\*\/\.apm\/\*\*$/,
738
- /^\/\.apm\/?$/,
739
- /^\/\.apm\/\*\*$/
740
- ];
741
- function normalizeGitignorePattern(line) {
742
- const trimmed = line.trim();
743
- if (!trimmed || trimmed.startsWith("#")) return "";
744
- if (trimmed.startsWith("!")) return "";
745
- const hashIndex = trimmed.indexOf("#");
746
- return (hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed).trim();
747
- }
748
- function gitignoreIgnoresApm(line) {
749
- const pattern = normalizeGitignorePattern(line);
750
- if (!pattern) return false;
751
- return APM_GITIGNORE_PATTERNS.some((re) => re.test(pattern));
752
- }
753
- var APM_GITIGNORE_LINE = "**/.apm/**";
754
- function ensureApmGitignoredInRepo(workdir) {
755
- const gitignorePath = join2(workdir, ".gitignore");
756
- const fsGitignorePath = toFsPath(gitignorePath);
757
- if (!existsSync2(fsGitignorePath)) {
758
- writeFileSync3(fsGitignorePath, `${APM_GITIGNORE_LINE}
759
- `, "utf8");
760
- return true;
761
- }
762
- const content = readFileSync3(fsGitignorePath, "utf8");
763
- if (content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
764
- return false;
765
- }
766
- const suffix = content.endsWith("\n") || content.length === 0 ? "" : "\n";
767
- writeFileSync3(
768
- fsGitignorePath,
769
- `${content}${suffix}${APM_GITIGNORE_LINE}
770
- `,
771
- "utf8"
772
- );
773
- return true;
774
- }
775
- function assertApmGitignoredInRepo(workdir) {
776
- const gitignorePath = join2(workdir, ".gitignore");
777
- const fsGitignorePath = toFsPath(gitignorePath);
778
- if (!existsSync2(fsGitignorePath)) {
779
- throw new Error(
780
- `\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`
781
- );
782
- }
783
- const content = readFileSync3(fsGitignorePath, "utf8");
784
- if (!content.split(/\r?\n/).some(gitignoreIgnoresApm)) {
785
- throw new Error(
786
- `\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`
787
- );
788
- }
789
- }
790
- async function ensureDirExists(dir) {
791
- mkdirSync3(dir, { recursive: true });
792
- }
793
- async function ensureWorkspaceApmDirForInit(cwd = resolveWorkdirPath()) {
794
- const dir = workspaceApmDir(cwd);
795
- const fsDir = toFsPath(dir);
796
- if (!existsSync2(fsDir)) {
797
- mkdirSync3(fsDir, { recursive: true });
798
- return;
799
- }
800
- const st = statSync(fsDir);
801
- if (!st.isDirectory()) {
802
- throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${dir}`);
803
- }
804
- if (readdirSync(fsDir).length > 0) {
805
- throw new Error(
806
- "[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"
807
- );
808
- }
809
- }
810
- var WORKSPACE_TEMPLATE_SUBDIRS = [
811
- "sessions",
812
- "skills",
813
- "rules",
814
- "deploy"
815
- ];
816
- function shouldSkipTemplateEntry(name) {
817
- return name === ".DS_Store" || name === "Thumbs.db";
818
- }
819
- function copyTemplateEntry(src, dest) {
820
- const fsSrc = toFsPath(src);
821
- const fsDest = toFsPath(dest);
822
- const st = statSync(fsSrc);
823
- if (st.isDirectory()) {
824
- mkdirSync3(fsDest, { recursive: true });
825
- for (const name of readdirSync(fsSrc)) {
826
- if (shouldSkipTemplateEntry(name)) continue;
827
- copyTemplateEntry(join2(src, name), join2(dest, name));
828
- }
829
- return;
830
- }
831
- if (!st.isFile()) return;
832
- mkdirSync3(toFsPath(dirname2(dest)), { recursive: true });
833
- copyFileSync(fsSrc, fsDest);
834
- }
835
- function assertTemplateCopiedToApm(apmDir, workdir) {
836
- const required = [
837
- "AGENTS.md",
838
- "apm.config.json",
839
- "rules",
840
- "skills",
841
- "sessions"
842
- ];
843
- for (const item of required) {
844
- const path = join2(apmDir, item);
845
- if (!existsSync2(toFsPath(path))) {
846
- throw new Error(`[apm] \u521D\u59CB\u5316\u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5C11: ${path}`);
847
- }
848
- }
849
- const leakedRules = join2(workdir, "rules");
850
- const apmRules = join2(apmDir, "rules");
851
- if (existsSync2(toFsPath(leakedRules)) && !existsSync2(toFsPath(join2(apmRules, "reply.md")))) {
852
- throw new Error(
853
- `[apm] \u6A21\u677F\u88AB\u590D\u5236\u5230\u9519\u8BEF\u4F4D\u7F6E: ${leakedRules}\uFF08\u5E94\u5728 ${apmRules}\uFF09`
854
- );
855
- }
856
- }
857
- async function copyTemplateFiles(targetDir, workdir = resolveWorkdirPath()) {
858
- const resolvedTarget = resolve3(targetDir);
859
- const templateDir = resolve3(CLI_TEMPLATE_DIR);
860
- const fsTemplateDir = toFsPath(templateDir);
861
- if (!existsSync2(fsTemplateDir)) {
862
- throw new Error(`[apm] \u672A\u627E\u5230 CLI \u6A21\u677F\u76EE\u5F55: ${templateDir}`);
863
- }
864
- const dirStat = statSync(fsTemplateDir);
865
- if (!dirStat.isDirectory()) {
866
- throw new Error(`[apm] CLI \u6A21\u677F\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55: ${templateDir}`);
867
- }
868
- const entries = readdirSync(fsTemplateDir).filter(
869
- (name) => !shouldSkipTemplateEntry(name)
870
- );
871
- if (entries.length === 0) {
872
- throw new Error(`[apm] CLI \u6A21\u677F\u76EE\u5F55\u4E3A\u7A7A: ${templateDir}`);
873
- }
874
- mkdirSync3(toFsPath(resolvedTarget), { recursive: true });
875
- for (const name of entries) {
876
- copyTemplateEntry(join2(templateDir, name), join2(resolvedTarget, name));
877
- }
878
- for (const subdir of WORKSPACE_TEMPLATE_SUBDIRS) {
879
- mkdirSync3(toFsPath(join2(resolvedTarget, subdir)), { recursive: true });
1361
+ );
880
1362
  }
881
- assertTemplateCopiedToApm(resolvedTarget, resolve3(workdir));
1363
+ process.on("warning", (warning) => {
1364
+ if (warning.name !== "MaxListenersExceededWarning") return;
1365
+ console.warn(`[apm:abort-debug] ${warning.name}: ${warning.message}`);
1366
+ if (warning.stack) {
1367
+ console.warn(warning.stack);
1368
+ }
1369
+ });
1370
+ const proto = AbortSignal.prototype;
1371
+ const original = proto.addEventListener;
1372
+ proto.addEventListener = function(type, listener, options) {
1373
+ if (type === "abort") {
1374
+ const sig = this;
1375
+ const before = getEventListeners(sig, "abort").length;
1376
+ const max = getMaxListeners(sig);
1377
+ const stack = new Error("[apm:abort-debug] addEventListener stack").stack?.split("\n").slice(2, 8).join("\n") ?? "";
1378
+ console.log(
1379
+ `[apm:abort-debug] addEventListener("abort") before=${before} max=${max}
1380
+ ${stack}`
1381
+ );
1382
+ }
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
+ );
882
1388
  }
883
1389
 
884
1390
  // src/commands/append-message.ts
@@ -1057,11 +1563,99 @@ function createUpsertWebIdePlanTool(options) {
1057
1563
  };
1058
1564
  }
1059
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
+
1060
1651
  // src/commands/connect/cursor-custom-tools.ts
1061
1652
  var PLAN_MODE_ASK_QUESTION_HINT = `[SDK \u73AF\u5883\u8BF4\u660E]
1062
1653
  AskQuestion \u5DF2\u901A\u8FC7 MCP \u670D\u52A1\u5668 custom-user-tools \u6CE8\u518C\uFF0C\u5DE5\u5177\u540D\u4E3A AskQuestion\u3002
1063
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
1064
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`;
1065
1659
  function createCursorCustomTools(cfg, messageId, options) {
1066
1660
  const tools = {
1067
1661
  ...createAppendMessageCustomTools(
@@ -1083,6 +1677,10 @@ function createCursorCustomTools(cfg, messageId, options) {
1083
1677
  cfg,
1084
1678
  taskId: options.taskId
1085
1679
  });
1680
+ tools.UpsertWebIdeTestCases = createUpsertWebIdeTestCasesTool({
1681
+ cfg,
1682
+ taskId: options.taskId
1683
+ });
1086
1684
  }
1087
1685
  return tools;
1088
1686
  }
@@ -1094,14 +1692,19 @@ function withPlanModeToolHint(prompt, mode) {
1094
1692
 
1095
1693
  ${PLAN_MODE_ASK_QUESTION_HINT}`;
1096
1694
  }
1695
+ function withWorkspaceBoundaryHint(prompt) {
1696
+ return `${prompt.trim()}
1697
+
1698
+ ${WORKSPACE_BOUNDARY_HINT}`;
1699
+ }
1097
1700
 
1098
1701
  // src/commands/connect/local-agent-store.ts
1099
- import { mkdirSync as mkdirSync4 } from "node:fs";
1100
- import { join as join3 } from "node:path";
1702
+ import { mkdirSync as mkdirSync5 } from "node:fs";
1703
+ import { join as join4 } from "node:path";
1101
1704
  import { JsonlLocalAgentStore } from "@cursor/sdk";
1102
1705
  function createWorkspaceLocalAgentStore(workdir) {
1103
- const rootDir = join3(workdir, ".apm", "cursor-agent-store");
1104
- mkdirSync4(rootDir, { recursive: true });
1706
+ const rootDir = join4(workdir, ".apm", "cursor-agent-store");
1707
+ mkdirSync5(rootDir, { recursive: true });
1105
1708
  return new JsonlLocalAgentStore(rootDir);
1106
1709
  }
1107
1710
 
@@ -1139,7 +1742,8 @@ async function obtainAgent(ctx) {
1139
1742
  local: {
1140
1743
  cwd: ctx.cwd,
1141
1744
  store: createWorkspaceLocalAgentStore(ctx.workdir),
1142
- ...ctx.customTools ? { customTools: ctx.customTools } : {}
1745
+ ...ctx.customTools ? { customTools: ctx.customTools } : {},
1746
+ ...ctx.enableSandbox ? { sandboxOptions: { enabled: true } } : {}
1143
1747
  },
1144
1748
  ...ctx.mode ? { mode: ctx.mode } : {}
1145
1749
  // mcpServers: createPlaywrightMcpServers(),
@@ -1148,14 +1752,14 @@ async function obtainAgent(ctx) {
1148
1752
  const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
1149
1753
  if (savedAgentId) {
1150
1754
  try {
1151
- const agent2 = await Agent.resume(savedAgentId, agentOptions);
1755
+ const agent = await Agent.resume(savedAgentId, agentOptions);
1152
1756
  console.log(
1153
1757
  `[apm] \u590D\u7528 Agent user=${ctx.user} agentId=${savedAgentId}${explicitAgentId ? "\uFF08\u53C2\u6570\u6307\u5B9A\uFF09" : ""}`
1154
1758
  );
1155
1759
  if (ctx.user) {
1156
- saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent2.agentId);
1760
+ saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
1157
1761
  }
1158
- return { agent: agent2, resumed: true };
1762
+ return { agent, resumed: true };
1159
1763
  } catch (err) {
1160
1764
  console.warn(
1161
1765
  `[apm] \u590D\u7528 Agent \u5931\u8D25\uFF08agentId=${savedAgentId}\uFF09\uFF0C\u56DE\u9000\u4E3A\u65B0\u5EFA:`,
@@ -1166,11 +1770,19 @@ async function obtainAgent(ctx) {
1166
1770
  }
1167
1771
  }
1168
1772
  }
1169
- const agent = await Agent.create(agentOptions);
1170
- if (ctx.user) {
1171
- saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
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;
1172
1785
  }
1173
- return { agent, resumed: false };
1174
1786
  }
1175
1787
  async function runCursorAgent(cfg, ctx, options) {
1176
1788
  const signal = options?.signal;
@@ -1190,9 +1802,13 @@ async function runCursorAgent(cfg, ctx, options) {
1190
1802
  enableWebIdePlanTools: options?.enableWebIdePlanTools,
1191
1803
  taskId: options?.taskId
1192
1804
  });
1193
- const prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
1805
+ const enableSandbox = Boolean(options?.enableSandbox);
1806
+ let prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
1807
+ if (enableSandbox) {
1808
+ prompt = withWorkspaceBoundaryHint(prompt);
1809
+ }
1194
1810
  console.log(
1195
- `[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}`
1196
1812
  );
1197
1813
  const { agent, resumed } = await obtainAgent({
1198
1814
  apiKey,
@@ -1203,7 +1819,8 @@ async function runCursorAgent(cfg, ctx, options) {
1203
1819
  user: ctx.user,
1204
1820
  mode: ctx.mode,
1205
1821
  resumeAgentId: ctx.resumeAgentId,
1206
- customTools
1822
+ customTools,
1823
+ enableSandbox
1207
1824
  });
1208
1825
  const eventSession = new EventSession(prompt);
1209
1826
  const syncRemoteLog = options?.createRemoteLogSync ? options.createRemoteLogSync(agent.agentId) : options?.skipRemoteLogSync ? noopRemoteLogSync : createThrottledCursorMessageLogSync(
@@ -1327,17 +1944,17 @@ async function runCursorAgent(cfg, ctx, options) {
1327
1944
  }
1328
1945
 
1329
1946
  // src/commands/connect/webide-agent-registry.ts
1330
- import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1331
- import { dirname as dirname3, resolve as resolve4 } from "node:path";
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";
1332
1949
  function registryPath2(workdir, taskId) {
1333
- return resolve4(workdir, ".apm", "webide", taskId, "cursor-agent.json");
1950
+ return resolve5(workdir, ".apm", "webide", taskId, "cursor-agent.json");
1334
1951
  }
1335
1952
  function readRegistry2(path) {
1336
- if (!existsSync3(path)) {
1953
+ if (!existsSync4(path)) {
1337
1954
  return {};
1338
1955
  }
1339
1956
  try {
1340
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
1957
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
1341
1958
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1342
1959
  const agentId = parsed.agentId;
1343
1960
  if (typeof agentId === "string" && agentId.trim()) {
@@ -1349,8 +1966,8 @@ function readRegistry2(path) {
1349
1966
  return {};
1350
1967
  }
1351
1968
  function writeRegistry2(path, registry) {
1352
- mkdirSync5(dirname3(path), { recursive: true });
1353
- writeFileSync4(path, `${JSON.stringify(registry, null, 2)}
1969
+ mkdirSync6(dirname3(path), { recursive: true });
1970
+ writeFileSync5(path, `${JSON.stringify(registry, null, 2)}
1354
1971
  `, "utf8");
1355
1972
  }
1356
1973
  function loadWebIdeAgentId(workdir, taskId) {
@@ -1361,18 +1978,18 @@ function saveWebIdeAgentId(workdir, taskId, agentId) {
1361
1978
  }
1362
1979
  function clearWebIdeAgentId(workdir, taskId) {
1363
1980
  const path = registryPath2(workdir, taskId);
1364
- if (!existsSync3(path)) return;
1981
+ if (!existsSync4(path)) return;
1365
1982
  writeRegistry2(path, {});
1366
1983
  }
1367
1984
 
1368
1985
  // src/commands/connect/webide-ask-question.ts
1369
1986
  import { setTimeout as delay } from "node:timers/promises";
1370
1987
  var POLL_INTERVAL_MS = 2e3;
1371
- function asString2(value) {
1988
+ function asString3(value) {
1372
1989
  return typeof value === "string" ? value.trim() : "";
1373
1990
  }
1374
1991
  function parseQuestions(args) {
1375
- const title = asString2(args.title) || void 0;
1992
+ const title = asString3(args.title) || void 0;
1376
1993
  const raw = args.questions;
1377
1994
  if (!Array.isArray(raw) || raw.length === 0) {
1378
1995
  throw new Error("AskQuestion \u7F3A\u5C11 questions");
@@ -1381,16 +1998,16 @@ function parseQuestions(args) {
1381
1998
  for (const item of raw) {
1382
1999
  if (!item || typeof item !== "object" || Array.isArray(item)) continue;
1383
2000
  const row = item;
1384
- const id = asString2(row.id);
1385
- const prompt = asString2(row.prompt);
2001
+ const id = asString3(row.id);
2002
+ const prompt = asString3(row.prompt);
1386
2003
  const optionsRaw = row.options;
1387
2004
  if (!id || !prompt || !Array.isArray(optionsRaw)) continue;
1388
2005
  const options = [];
1389
2006
  for (const opt of optionsRaw) {
1390
2007
  if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
1391
2008
  const o = opt;
1392
- const oid = asString2(o.id);
1393
- const label = asString2(o.label);
2009
+ const oid = asString3(o.id);
2010
+ const label = asString3(o.label);
1394
2011
  if (oid && label) options.push({ id: oid, label });
1395
2012
  }
1396
2013
  if (options.length < 2) {
@@ -1479,14 +2096,14 @@ var MinioClient = class {
1479
2096
  async deleteObjectsByPrefix(bucket, prefix) {
1480
2097
  const objectsStream = this.inner.listObjectsV2(bucket, prefix, true);
1481
2098
  const keys = [];
1482
- await new Promise((resolve5, reject) => {
2099
+ await new Promise((resolve6, reject) => {
1483
2100
  objectsStream.on("data", (obj) => {
1484
2101
  if (obj.name) {
1485
2102
  keys.push(obj.name);
1486
2103
  }
1487
2104
  });
1488
2105
  objectsStream.on("error", reject);
1489
- objectsStream.on("end", resolve5);
2106
+ objectsStream.on("end", resolve6);
1490
2107
  });
1491
2108
  const chunkSize = 500;
1492
2109
  for (let i = 0; i < keys.length; i += chunkSize) {
@@ -1654,147 +2271,12 @@ function resolveMessageReplyFallback(fallback) {
1654
2271
  }
1655
2272
 
1656
2273
  // src/commands/init.ts
1657
- import { join as join6 } from "path";
1658
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
1659
-
1660
- // src/deployment-config-sync.ts
1661
- import { join as join4 } from "path";
1662
- import { writeFileSync as writeFileSync5 } from "fs";
1663
-
1664
- // src/git-remote.ts
1665
- import { execFile } from "child_process";
1666
- import { promisify } from "util";
1667
- var execFileAsync = promisify(execFile);
1668
- async function tryReadGitOriginUrl(cwd) {
1669
- try {
1670
- const { stdout } = await execFileAsync(
1671
- "git",
1672
- ["config", "--get", "remote.origin.url"],
1673
- { cwd, encoding: "utf8", maxBuffer: 1024 * 1024 }
1674
- );
1675
- const url = stdout.trim();
1676
- return url || null;
1677
- } catch {
1678
- return null;
1679
- }
1680
- }
1681
-
1682
- // src/git-utils.ts
1683
- import { execFile as execFile2 } from "child_process";
1684
- import { promisify as promisify2 } from "util";
1685
- var execFileAsync2 = promisify2(execFile2);
1686
- async function execGit(cwd, args, quiet = false) {
1687
- try {
1688
- const { stdout, stderr } = await execFileAsync2("git", args, {
1689
- cwd,
1690
- encoding: "utf8",
1691
- maxBuffer: 10 * 1024 * 1024
1692
- });
1693
- if (!quiet && stderr.trim()) {
1694
- process.stderr.write(stderr);
1695
- }
1696
- return stdout;
1697
- } catch (err) {
1698
- const e = err;
1699
- const detail = (e.stderr ?? e.message ?? String(err)).trim();
1700
- throw new Error(
1701
- `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
1702
- );
1703
- }
1704
- }
1705
- async function isGitRepo(cwd) {
1706
- try {
1707
- await execGit(cwd, ["rev-parse", "--git-dir"], true);
1708
- return true;
1709
- } catch {
1710
- return false;
1711
- }
1712
- }
1713
- async function resolveGitRepoRoot(cwd) {
1714
- return (await execGit(cwd, ["rev-parse", "--show-toplevel"], true)).trim();
1715
- }
1716
- async function hasUpstream(cwd) {
1717
- try {
1718
- await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
1719
- return true;
1720
- } catch {
1721
- return false;
1722
- }
1723
- }
1724
- var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
1725
- async function commitAndPushGitignore(workdir) {
1726
- if (!await isGitRepo(workdir)) {
1727
- console.log("[apm] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u8BF7\u624B\u52A8\u63D0\u4EA4 .gitignore");
1728
- return;
1729
- }
1730
- await execGit(workdir, ["add", "--", ".gitignore"]);
1731
- await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
1732
- console.log(`[apm] \u5DF2\u63D0\u4EA4 .gitignore: ${GITIGNORE_COMMIT_MESSAGE}`);
1733
- const originUrl = await tryReadGitOriginUrl(workdir);
1734
- if (!originUrl) {
1735
- console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
1736
- return;
1737
- }
1738
- if (await hasUpstream(workdir)) {
1739
- await execGit(workdir, ["push"]);
1740
- } else {
1741
- await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
1742
- }
1743
- console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
1744
- }
1745
-
1746
- // src/baseline-resolve.ts
1747
- function formatBaselineDiagnostic(workdirPath, baselineWorkdirPath, diagnostic) {
1748
- 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
1749
- \u8BF7\u5148\u5728\u5E73\u53F0\u767B\u8BB0\u8BE5\u8DEF\u5F84\u5BF9\u5E94\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u3002`;
1750
- }
1751
- async function matchRepositoryByGitRemote(api, workdirPath) {
1752
- const gitRoot = await resolveGitRepoRoot(workdirPath);
1753
- const gitUrl = await tryReadGitOriginUrl(gitRoot);
1754
- if (!gitUrl) {
1755
- return null;
1756
- }
1757
- const matched = await api.cli.matchRepository({ url: gitUrl });
1758
- const repositoryId = matched.repositoryId?.trim();
1759
- const defaultBranch = matched.defaultBranch?.trim();
1760
- if (!repositoryId || !defaultBranch) {
1761
- return null;
1762
- }
1763
- console.log(
1764
- `[apm] \u5DE5\u4F5C\u7A7A\u95F4\u8DEF\u5F84\u672A\u5339\u914D\uFF0C\u5DF2\u901A\u8FC7 git remote \u5173\u8054\u4ED3\u5E93: ${gitUrl}`
1765
- );
1766
- return { repositoryId, defaultBranch };
1767
- }
1768
- async function resolveWorkspaceBaseline(api, workdirPath) {
1769
- const baseline = await api.cli.workspaceBaseline({ workdirPath });
1770
- const repositoryId = baseline.repositoryId?.trim();
1771
- const defaultBranch = baseline.defaultBranch?.trim();
1772
- if (repositoryId && defaultBranch) {
1773
- return {
1774
- repositoryId,
1775
- defaultBranch,
1776
- workdirPath: baseline.workdirPath,
1777
- matchedViaGitRemote: false
1778
- };
1779
- }
1780
- const viaGit = await matchRepositoryByGitRemote(api, workdirPath);
1781
- if (viaGit) {
1782
- return {
1783
- ...viaGit,
1784
- workdirPath: baseline.workdirPath,
1785
- matchedViaGitRemote: true
1786
- };
1787
- }
1788
- throw new Error(
1789
- `[apm] ${formatBaselineDiagnostic(
1790
- workdirPath,
1791
- baseline.workdirPath,
1792
- baseline.diagnostic
1793
- )}`
1794
- );
1795
- }
2274
+ import { join as join7 } from "path";
2275
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
1796
2276
 
1797
2277
  // src/deployment-config-sync.ts
2278
+ import { join as join5 } from "path";
2279
+ import { writeFileSync as writeFileSync6 } from "fs";
1798
2280
  var TEMPLATE_HINT = "\u4FDD\u7559\u6A21\u677F .apm/apm.config.json";
1799
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";
1800
2282
  async function resolveRepositoryIdForSync(api, workdirPath) {
@@ -1849,8 +2331,8 @@ ${diagnostic ?? ""}
1849
2331
  return { synced: false, repositoryId };
1850
2332
  }
1851
2333
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1852
- const apmConfigPath = toFsPath(join4(targetApmDir, "apm.config.json"));
1853
- writeFileSync5(apmConfigPath, `${JSON.stringify(parsed, null, 2)}
2334
+ const apmConfigPath = toFsPath(join5(targetApmDir, "apm.config.json"));
2335
+ writeFileSync6(apmConfigPath, `${JSON.stringify(parsed, null, 2)}
1854
2336
  `, "utf8");
1855
2337
  console.log(`[apm] \u5DF2\u540C\u6B65\u5E73\u53F0\u90E8\u7F72\u914D\u7F6E: ${config.name}`);
1856
2338
  console.log("[apm] \u5DF2\u5199\u5165 .apm/apm.config.json");
@@ -1859,20 +2341,20 @@ ${diagnostic ?? ""}
1859
2341
 
1860
2342
  // src/repository-project-documents-sync.ts
1861
2343
  import {
1862
- existsSync as existsSync4,
1863
- readdirSync as readdirSync2,
1864
- readFileSync as readFileSync5,
2344
+ existsSync as existsSync5,
2345
+ readdirSync as readdirSync3,
2346
+ readFileSync as readFileSync6,
1865
2347
  rmSync,
1866
- writeFileSync as writeFileSync6
2348
+ writeFileSync as writeFileSync7
1867
2349
  } from "fs";
1868
- import { dirname as dirname4, join as join5, relative, sep } from "path";
2350
+ import { dirname as dirname4, join as join6, relative as relative2, sep } from "path";
1869
2351
  var MANIFEST_FILE = "manifest.json";
1870
2352
  function projectDocumentsDir(apmRoot) {
1871
- return join5(apmRoot ?? workspaceApmDir(), "project");
2353
+ return join6(apmRoot ?? workspaceApmDir(), "project");
1872
2354
  }
1873
2355
  function projectDocumentLocalPath(apmRoot, documentPath) {
1874
2356
  const normalized = normalizeLocalDocumentPath(documentPath);
1875
- return join5(projectDocumentsDir(apmRoot), ...normalized.split("/"));
2357
+ return join6(projectDocumentsDir(apmRoot), ...normalized.split("/"));
1876
2358
  }
1877
2359
  function normalizeLocalDocumentPath(path) {
1878
2360
  const trimmed = path.trim().replace(/\\/g, "/");
@@ -1886,13 +2368,13 @@ function normalizeLocalDocumentPath(path) {
1886
2368
  return segments.join("/");
1887
2369
  }
1888
2370
  function readLocalManifest(apmRoot) {
1889
- const manifestPath = join5(projectDocumentsDir(apmRoot), MANIFEST_FILE);
1890
- if (!existsSync4(manifestPath)) {
2371
+ const manifestPath2 = join6(projectDocumentsDir(apmRoot), MANIFEST_FILE);
2372
+ if (!existsSync5(manifestPath2)) {
1891
2373
  return null;
1892
2374
  }
1893
2375
  try {
1894
2376
  return JSON.parse(
1895
- readFileSync5(manifestPath, "utf8")
2377
+ readFileSync6(manifestPath2, "utf8")
1896
2378
  );
1897
2379
  } catch {
1898
2380
  return null;
@@ -1969,20 +2451,20 @@ ${diagnostic ?? ""}`
1969
2451
  for (const doc of list) {
1970
2452
  const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
1971
2453
  await ensureDirExists(dirname4(absPath));
1972
- writeFileSync6(absPath, doc.content, "utf8");
2454
+ writeFileSync7(absPath, doc.content, "utf8");
1973
2455
  downloaded += 1;
1974
2456
  }
1975
2457
  }
1976
2458
  let deleted = 0;
1977
2459
  for (const path of deleteLocal) {
1978
2460
  const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path));
1979
- if (existsSync4(absPath)) {
2461
+ if (existsSync5(absPath)) {
1980
2462
  rmSync(absPath, { force: true });
1981
2463
  deleted += 1;
1982
2464
  }
1983
2465
  }
1984
- writeFileSync6(
1985
- toFsPath(join5(projectDir, MANIFEST_FILE)),
2466
+ writeFileSync7(
2467
+ toFsPath(join6(projectDir, MANIFEST_FILE)),
1986
2468
  `${JSON.stringify(remoteManifest, null, 2)}
1987
2469
  `,
1988
2470
  "utf8"
@@ -2017,11 +2499,11 @@ async function ensureWorkspaceInitialized(workdir, options) {
2017
2499
  await syncRepositoryProjectDocumentsPull(workdir, apmDir);
2018
2500
  const trimmedName = options?.name?.trim();
2019
2501
  if (trimmedName) {
2020
- const apmConfigPath = toFsPath(join6(apmDir, "apm.config.json"));
2021
- const config = readFileSync6(apmConfigPath, "utf8");
2502
+ const apmConfigPath = toFsPath(join7(apmDir, "apm.config.json"));
2503
+ const config = readFileSync7(apmConfigPath, "utf8");
2022
2504
  const configJson = JSON.parse(config);
2023
2505
  configJson.name = trimmedName;
2024
- writeFileSync7(
2506
+ writeFileSync8(
2025
2507
  apmConfigPath,
2026
2508
  `${JSON.stringify(configJson, null, 2)}
2027
2509
  `,
@@ -2032,6 +2514,92 @@ async function ensureWorkspaceInitialized(workdir, options) {
2032
2514
  return { didInit: true, syncResult };
2033
2515
  }
2034
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-sandbox.ts
2585
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9, existsSync as existsSync6 } from "fs";
2586
+ import { join as join8 } from "path";
2587
+ var DEFAULT_SANDBOX_JSON = `{
2588
+ "type": "workspace_readwrite",
2589
+ "networkPolicy": {
2590
+ "default": "allow"
2591
+ }
2592
+ }
2593
+ `;
2594
+ function ensureWorkspaceSandboxConfig(workdir) {
2595
+ const cursorDir = toFsPath(join8(workdir, ".cursor"));
2596
+ const sandboxPath = toFsPath(join8(cursorDir, "sandbox.json"));
2597
+ if (existsSync6(sandboxPath)) return;
2598
+ mkdirSync7(cursorDir, { recursive: true });
2599
+ writeFileSync9(sandboxPath, DEFAULT_SANDBOX_JSON, "utf8");
2600
+ console.log(`[apm] \u5DF2\u5199\u5165\u5DE5\u4F5C\u533A sandbox \u914D\u7F6E\uFF1A${sandboxPath}`);
2601
+ }
2602
+
2035
2603
  // src/commands/connect/handle-webide-message.ts
2036
2604
  async function updateStatus(cfg, messageId, status) {
2037
2605
  const api = createApmApiClient(cfg);
@@ -2049,6 +2617,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2049
2617
  const workdir = requireRemoteWorkdir(msg.workdir);
2050
2618
  const messageId = msg.messageId;
2051
2619
  const taskId = msg.taskId;
2620
+ const branchCacheKey = `webide:${taskId}`;
2052
2621
  console.log(
2053
2622
  `[apm] webide-message action=${msg.action} taskId=${taskId} messageId=${messageId}`
2054
2623
  );
@@ -2059,6 +2628,24 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2059
2628
  if (!didInit) {
2060
2629
  assertApmGitignoredInRepo(workdir);
2061
2630
  }
2631
+ ensureWorkspaceSandboxConfig(workdir);
2632
+ resolveWorkspaceRepos(workdir);
2633
+ if (shouldRunBranch(branchCacheKey, workdir)) {
2634
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
2635
+ const branchResult = await runTaskBranch(taskId, { cwd: workdir });
2636
+ markBranchDone(branchCacheKey, workdir);
2637
+ console.log(
2638
+ `[apm] webide branch ready kind=${branchResult.kind} branch=${branchResult.branch} repos=${branchResult.repos.length}`
2639
+ );
2640
+ } else {
2641
+ console.log(
2642
+ `[apm] step=branch skipped taskId=${taskId} workdir=${workdir}`
2643
+ );
2644
+ }
2645
+ if (shouldEnsureWebIdeDraftPullRequests(msg.action)) {
2646
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
2647
+ await ensureWebIdeDraftPullRequests(cfg, taskId, workdir);
2648
+ }
2062
2649
  const savedAgentId = loadWebIdeAgentId(workdir, taskId);
2063
2650
  const logSyncRef = { current: null };
2064
2651
  const outcome = await runCursorAgent(
@@ -2082,6 +2669,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2082
2669
  signal
2083
2670
  }),
2084
2671
  enableWebIdePlanTools: true,
2672
+ enableSandbox: true,
2085
2673
  taskId,
2086
2674
  createRemoteLogSync: (agentId) => {
2087
2675
  saveWebIdeAgentId(workdir, taskId, agentId);