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.
@@ -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
- // src/api/client.ts
214
- function createApmApiClient(cfg) {
215
- const baseURL = `${cfg.baseUrl.trim().replace(/\/+$/, "")}/api/v1`;
216
- return createApiClient(requestConfig, {
217
- baseURL,
218
- getToken: () => resolveApiKey(cfg) || void 0,
219
- successCodes: [0],
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/workdir-path.ts
225
- import { realpathSync } from "fs";
226
- import { platform } from "os";
227
- import { resolve } from "path";
228
- function toFsPath(inputPath) {
229
- const absolute = resolve(inputPath);
230
- if (platform() !== "win32") return absolute;
231
- if (absolute.startsWith("\\\\?\\")) return absolute;
232
- const normalized = absolute.replace(/\//g, "\\");
233
- if (normalized.startsWith("\\\\")) {
234
- return `\\\\?\\UNC\\${normalized.slice(2)}`;
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 normalizeWorkdirPath(path) {
239
- let normalized = path.trim().replace(/\\/g, "/").normalize("NFC");
240
- if (normalized.startsWith("//?/")) {
241
- normalized = normalized.slice(4);
831
+ async function commitWorkingTreeIfDirty(cwd, message) {
832
+ await ensureGitRepo(cwd);
833
+ if (!await isWorkingTreeDirty(cwd)) {
834
+ return false;
242
835
  }
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;
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
- normalized = normalized.replace(/\/+/g, "/");
250
- if (normalized.length > 1 && normalized.endsWith("/")) {
251
- normalized = normalized.slice(0, -1);
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 normalized;
254
- }
255
- function resolveWorkdirPath(cwd = process.cwd()) {
256
- const absolute = resolve(cwd);
257
- try {
258
- return normalizeWorkdirPath(realpathSync.native(absolute));
259
- } catch {
260
- return normalizeWorkdirPath(absolute);
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
- 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");
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
- return resolveWorkdirPath(trimmed);
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 mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
536
- 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";
537
1204
  function registryPath(workdir, sessionId) {
538
- return resolve2(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
1205
+ return resolve4(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
539
1206
  }
540
1207
  function readRegistry(path) {
541
- if (!existsSync(path)) {
1208
+ if (!existsSync3(path)) {
542
1209
  return {};
543
1210
  }
544
1211
  try {
545
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
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
- mkdirSync2(dirname(path), { recursive: true });
563
- writeFileSync2(path, `${JSON.stringify(registry, null, 2)}
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 mkdirSync4 } from "node:fs";
1121
- import { join as join3 } from "node:path";
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 = join3(workdir, ".apm", "cursor-agent-store");
1125
- mkdirSync4(rootDir, { recursive: true });
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 agent2 = await Agent.resume(savedAgentId, agentOptions);
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, agent2.agentId);
1760
+ saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
1178
1761
  }
1179
- return { agent: agent2, resumed: true };
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
- const agent = await Agent.create(agentOptions);
1191
- if (ctx.user) {
1192
- 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;
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 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
+ }
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 existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1352
- 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";
1353
1949
  function registryPath2(workdir, taskId) {
1354
- return resolve4(workdir, ".apm", "webide", taskId, "cursor-agent.json");
1950
+ return resolve5(workdir, ".apm", "webide", taskId, "cursor-agent.json");
1355
1951
  }
1356
1952
  function readRegistry2(path) {
1357
- if (!existsSync3(path)) {
1953
+ if (!existsSync4(path)) {
1358
1954
  return {};
1359
1955
  }
1360
1956
  try {
1361
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
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
- mkdirSync5(dirname3(path), { recursive: true });
1374
- writeFileSync4(path, `${JSON.stringify(registry, null, 2)}
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 (!existsSync3(path)) return;
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 asString2(value) {
1988
+ function asString3(value) {
1393
1989
  return typeof value === "string" ? value.trim() : "";
1394
1990
  }
1395
1991
  function parseQuestions(args) {
1396
- const title = asString2(args.title) || void 0;
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 = asString2(row.id);
1406
- const prompt = asString2(row.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 = asString2(o.id);
1414
- const label = asString2(o.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((resolve5, reject) => {
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", resolve5);
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 join6 } from "path";
1679
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
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(join4(targetApmDir, "apm.config.json"));
1874
- writeFileSync5(apmConfigPath, `${JSON.stringify(parsed, null, 2)}
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 existsSync4,
1884
- readdirSync as readdirSync2,
1885
- readFileSync as readFileSync5,
2344
+ existsSync as existsSync5,
2345
+ readdirSync as readdirSync3,
2346
+ readFileSync as readFileSync6,
1886
2347
  rmSync,
1887
- writeFileSync as writeFileSync6
2348
+ writeFileSync as writeFileSync7
1888
2349
  } from "fs";
1889
- 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";
1890
2351
  var MANIFEST_FILE = "manifest.json";
1891
2352
  function projectDocumentsDir(apmRoot) {
1892
- return join5(apmRoot ?? workspaceApmDir(), "project");
2353
+ return join6(apmRoot ?? workspaceApmDir(), "project");
1893
2354
  }
1894
2355
  function projectDocumentLocalPath(apmRoot, documentPath) {
1895
2356
  const normalized = normalizeLocalDocumentPath(documentPath);
1896
- return join5(projectDocumentsDir(apmRoot), ...normalized.split("/"));
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 manifestPath = join5(projectDocumentsDir(apmRoot), MANIFEST_FILE);
1911
- if (!existsSync4(manifestPath)) {
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
- readFileSync5(manifestPath, "utf8")
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
- writeFileSync6(absPath, doc.content, "utf8");
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 (existsSync4(absPath)) {
2461
+ if (existsSync5(absPath)) {
2001
2462
  rmSync(absPath, { force: true });
2002
2463
  deleted += 1;
2003
2464
  }
2004
2465
  }
2005
- writeFileSync6(
2006
- toFsPath(join5(projectDir, MANIFEST_FILE)),
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(join6(apmDir, "apm.config.json"));
2042
- const config = readFileSync6(apmConfigPath, "utf8");
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
- writeFileSync7(
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);