ai-project-manage-cli 7.1.11 → 7.1.13

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.
@@ -1,5 +1,5 @@
1
1
  // src/commands/connect/webide-message-worker.ts
2
- import { parentPort, workerData } from "node:worker_threads";
2
+ import { parentPort as parentPort2, workerData } from "node:worker_threads";
3
3
 
4
4
  // src/api/client.ts
5
5
  import { createApiClient } from "listpage-http";
@@ -100,6 +100,22 @@ var requestConfig = {
100
100
  method: "PUT",
101
101
  path: "/cli/webide/test-cases"
102
102
  }),
103
+ webideListTerminals: defineEndpoint({
104
+ method: "GET",
105
+ path: "/cli/webide/terminals"
106
+ }),
107
+ webideUpsertTerminal: defineEndpoint({
108
+ method: "POST",
109
+ path: "/cli/webide/terminals"
110
+ }),
111
+ webideUpdateTerminalStatus: defineEndpoint({
112
+ method: "PUT",
113
+ path: "/cli/webide/terminals/status"
114
+ }),
115
+ webideUpdateTerminalMeta: defineEndpoint({
116
+ method: "PUT",
117
+ path: "/cli/webide/terminals/meta"
118
+ }),
103
119
  branchBaseline: defineEndpoint({
104
120
  method: "GET",
105
121
  path: "/cli/tasks/branch-baseline"
@@ -136,6 +152,10 @@ var requestConfig = {
136
152
  method: "POST",
137
153
  path: "/cli/webide/draft-pull-requests"
138
154
  }),
155
+ webideMergePullRequests: defineEndpoint({
156
+ method: "POST",
157
+ path: "/cli/webide/pull-requests/merge"
158
+ }),
139
159
  getRepositoryProjectDocumentManifest: defineEndpoint({
140
160
  method: "GET",
141
161
  path: "/cli/repository-project-documents/manifest"
@@ -381,6 +401,22 @@ async function hasUpstream(cwd) {
381
401
  return false;
382
402
  }
383
403
  }
404
+ async function pushCurrentBranch(cwd) {
405
+ if (!await isGitRepo(cwd)) {
406
+ return false;
407
+ }
408
+ const originUrl = await tryReadGitOriginUrl(cwd);
409
+ if (!originUrl) {
410
+ console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8DF3\u8FC7 push");
411
+ return false;
412
+ }
413
+ if (await hasUpstream(cwd)) {
414
+ await execGit(cwd, ["push"]);
415
+ } else {
416
+ await execGit(cwd, ["push", "-u", "origin", "HEAD"]);
417
+ }
418
+ return true;
419
+ }
384
420
  var GITIGNORE_COMMIT_MESSAGE = "chore(apm): ignore .apm directory";
385
421
  async function commitAndPushGitignore(workdir) {
386
422
  if (!await isGitRepo(workdir)) {
@@ -390,17 +426,12 @@ async function commitAndPushGitignore(workdir) {
390
426
  await execGit(workdir, ["add", "--", ".gitignore"]);
391
427
  await execGit(workdir, ["commit", "-m", GITIGNORE_COMMIT_MESSAGE]);
392
428
  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"]);
429
+ const pushed = await pushCurrentBranch(workdir);
430
+ if (pushed) {
431
+ console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
400
432
  } else {
401
- await execGit(workdir, ["push", "-u", "origin", "HEAD"]);
433
+ console.log("[apm] \u672A\u914D\u7F6E remote.origin\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8 push .gitignore");
402
434
  }
403
- console.log("[apm] \u5DF2\u63A8\u9001 .gitignore");
404
435
  }
405
436
 
406
437
  // src/baseline-resolve.ts
@@ -854,6 +885,21 @@ async function commitWorkspaceReposIfDirty(workdir, message) {
854
885
  }
855
886
  return committed;
856
887
  }
888
+ async function pushWorkspaceRepos(workdir) {
889
+ const workdirPath = resolveWorkdirPath(workdir);
890
+ const manifest = loadWorkspaceReposCache(workdirPath);
891
+ const repoRoots = resolveWorkspaceRepoAbsolutePaths(manifest);
892
+ let pushed = 0;
893
+ for (const gitRoot of repoRoots) {
894
+ const label = formatRepoLabel(workdirPath, gitRoot);
895
+ const did = await pushCurrentBranch(gitRoot);
896
+ if (did) {
897
+ pushed += 1;
898
+ console.log(`[apm] WebIDE \u5DF2 push @ ${label}`);
899
+ }
900
+ }
901
+ return pushed;
902
+ }
857
903
  function branchNameForTask(taskId) {
858
904
  const id = taskId.trim();
859
905
  if (!id) {
@@ -1525,6 +1571,35 @@ ${payload}`;
1525
1571
  };
1526
1572
  }
1527
1573
 
1574
+ // src/commands/connect/webide-merge-tools.ts
1575
+ function createMergeWebIdePullRequestsTool(options) {
1576
+ const { cfg, taskId } = options;
1577
+ return {
1578
+ description: "Merge all open pull requests for this WebIDE task. Call once with confirmedConflictFree=true. No baseline sync or review is required before calling.",
1579
+ inputSchema: {
1580
+ type: "object",
1581
+ properties: {
1582
+ confirmedConflictFree: {
1583
+ type: "boolean",
1584
+ description: "Must be true to confirm you intend to merge this task's open PRs now."
1585
+ }
1586
+ },
1587
+ required: ["confirmedConflictFree"]
1588
+ },
1589
+ execute: async (args) => {
1590
+ if (args.confirmedConflictFree !== true) {
1591
+ throw new Error(
1592
+ "MergeWebIdePullRequests \u8981\u6C42 confirmedConflictFree=true\u3002"
1593
+ );
1594
+ }
1595
+ const api = createApmApiClient(cfg);
1596
+ console.log(`[apm] MergeWebIdePullRequests taskId=${taskId}`);
1597
+ const result = await api.cli.webideMergePullRequests({ taskId });
1598
+ return JSON.stringify(result, null, 2);
1599
+ }
1600
+ };
1601
+ }
1602
+
1528
1603
  // src/commands/connect/webide-plan-tools.ts
1529
1604
  function asString(value) {
1530
1605
  return typeof value === "string" ? value.trim() : "";
@@ -1679,6 +1754,164 @@ function createUpsertWebIdeTestCasesTool(options) {
1679
1754
  };
1680
1755
  }
1681
1756
 
1757
+ // src/commands/connect/webide-terminal-tools.ts
1758
+ function asString3(value) {
1759
+ return typeof value === "string" ? value.trim() : "";
1760
+ }
1761
+ function asPorts(value) {
1762
+ if (!Array.isArray(value)) return void 0;
1763
+ const ports = value.map((p) => typeof p === "number" ? p : Number(p)).filter((p) => Number.isFinite(p) && p > 0 && p < 65536).map((p) => Math.trunc(p));
1764
+ return ports.length ? ports : void 0;
1765
+ }
1766
+ function createWebIdeTerminalTools(options) {
1767
+ const { cfg, taskId, workdir, rpc } = options;
1768
+ const StartWebIdeTerminal = {
1769
+ description: "Start a long-running WebIDE terminal (e.g. frontend/backend dev server). Process outlives this message. Same key already RUNNING/READY returns idempotently. Do NOT use Shell for long-lived dev servers. Returns terminalId, pid, ports.",
1770
+ inputSchema: {
1771
+ type: "object",
1772
+ properties: {
1773
+ key: {
1774
+ type: "string",
1775
+ description: "Stable key, e.g. frontend / backend"
1776
+ },
1777
+ name: { type: "string", description: "Display name" },
1778
+ cwd: {
1779
+ type: "string",
1780
+ description: "Working directory (absolute or under workspace)"
1781
+ },
1782
+ command: {
1783
+ type: "string",
1784
+ description: "Shell command to run, e.g. pnpm dev"
1785
+ },
1786
+ readyPattern: {
1787
+ type: "string",
1788
+ description: "Optional regex; when matched in logs, status becomes READY"
1789
+ },
1790
+ portsHint: {
1791
+ type: "array",
1792
+ items: { type: "number" },
1793
+ description: "Optional expected listen ports"
1794
+ }
1795
+ },
1796
+ required: ["key", "cwd", "command"]
1797
+ },
1798
+ execute: async (args) => {
1799
+ const key = asString3(args.key);
1800
+ const cwd = asString3(args.cwd) || workdir;
1801
+ const command = asString3(args.command);
1802
+ if (!key || !command) {
1803
+ return {
1804
+ content: [{ type: "text", text: "key / command \u4E0D\u80FD\u4E3A\u7A7A" }],
1805
+ isError: true
1806
+ };
1807
+ }
1808
+ try {
1809
+ const result = rpc ? await rpc.start({
1810
+ taskId,
1811
+ key,
1812
+ name: asString3(args.name) || void 0,
1813
+ cwd,
1814
+ command,
1815
+ readyPattern: asString3(args.readyPattern) || void 0,
1816
+ portsHint: asPorts(args.portsHint)
1817
+ }) : await (async () => {
1818
+ throw new Error("Terminal RPC \u4E0D\u53EF\u7528\uFF08\u9700\u5728 connect \u4E3B\u8FDB\u7A0B\uFF09");
1819
+ })();
1820
+ return JSON.stringify(result, null, 2);
1821
+ } catch (err) {
1822
+ const detail = err instanceof Error ? err.message : String(err);
1823
+ return {
1824
+ content: [{ type: "text", text: `\u542F\u52A8\u7EC8\u7AEF\u5931\u8D25: ${detail}` }],
1825
+ isError: true
1826
+ };
1827
+ }
1828
+ }
1829
+ };
1830
+ const StopWebIdeTerminal = {
1831
+ description: "Stop a WebIDE terminal and kill its process tree. Soft-terminates (status EXITED, record kept).",
1832
+ inputSchema: {
1833
+ type: "object",
1834
+ properties: {
1835
+ key: { type: "string", description: "Terminal key" },
1836
+ terminalId: { type: "string", description: "Server terminal id" }
1837
+ }
1838
+ },
1839
+ execute: async (args) => {
1840
+ const key = asString3(args.key) || void 0;
1841
+ const terminalId = asString3(args.terminalId) || void 0;
1842
+ if (!key && !terminalId) {
1843
+ return {
1844
+ content: [{ type: "text", text: "\u9700\u8981 key \u6216 terminalId" }],
1845
+ isError: true
1846
+ };
1847
+ }
1848
+ try {
1849
+ const result = rpc ? await rpc.stop({ taskId, key, terminalId }) : await (async () => {
1850
+ throw new Error("Terminal RPC \u4E0D\u53EF\u7528\uFF08\u9700\u5728 connect \u4E3B\u8FDB\u7A0B\uFF09");
1851
+ })();
1852
+ return JSON.stringify(result, null, 2);
1853
+ } catch (err) {
1854
+ const detail = err instanceof Error ? err.message : String(err);
1855
+ return {
1856
+ content: [{ type: "text", text: `\u505C\u6B62\u7EC8\u7AEF\u5931\u8D25: ${detail}` }],
1857
+ isError: true
1858
+ };
1859
+ }
1860
+ }
1861
+ };
1862
+ const StopAllWebIdeTerminals = {
1863
+ description: "Stop all living WebIDE terminals for the current task (kill process trees). Use before re-running local dev to avoid port conflicts. Soft-terminates records.",
1864
+ inputSchema: {
1865
+ type: "object",
1866
+ properties: {}
1867
+ },
1868
+ execute: async () => {
1869
+ try {
1870
+ const result = rpc ? await rpc.stopAll({ taskId }) : await (async () => {
1871
+ throw new Error("Terminal RPC \u4E0D\u53EF\u7528\uFF08\u9700\u5728 connect \u4E3B\u8FDB\u7A0B\uFF09");
1872
+ })();
1873
+ return JSON.stringify(result, null, 2);
1874
+ } catch (err) {
1875
+ const detail = err instanceof Error ? err.message : String(err);
1876
+ return {
1877
+ content: [{ type: "text", text: `\u5168\u90E8\u505C\u6B62\u5931\u8D25: ${detail}` }],
1878
+ isError: true
1879
+ };
1880
+ }
1881
+ }
1882
+ };
1883
+ const ListWebIdeTerminals = {
1884
+ description: "List living WebIDE terminals for the current task (STARTING/RUNNING/READY/STOPPING only). Does NOT include EXITED/FAILED. Returns id, key, status, pid, ports.",
1885
+ inputSchema: {
1886
+ type: "object",
1887
+ properties: {}
1888
+ },
1889
+ execute: async () => {
1890
+ try {
1891
+ if (rpc) {
1892
+ const result2 = await rpc.list({ taskId });
1893
+ return JSON.stringify(result2, null, 2);
1894
+ }
1895
+ const api = createApmApiClient(cfg);
1896
+ const result = await api.cli.webideListTerminals({ taskId });
1897
+ return JSON.stringify(result, null, 2);
1898
+ } catch (err) {
1899
+ const detail = err instanceof Error ? err.message : String(err);
1900
+ return {
1901
+ content: [{ type: "text", text: `\u5217\u51FA\u7EC8\u7AEF\u5931\u8D25: ${detail}` }],
1902
+ isError: true
1903
+ };
1904
+ }
1905
+ }
1906
+ };
1907
+ return {
1908
+ StartWebIdeTerminal,
1909
+ StopWebIdeTerminal,
1910
+ StopAllWebIdeTerminals,
1911
+ ListWebIdeTerminals
1912
+ };
1913
+ }
1914
+
1682
1915
  // src/commands/connect/cursor-custom-tools.ts
1683
1916
  var PLAN_MODE_ASK_QUESTION_HINT = `[SDK \u73AF\u5883\u8BF4\u660E]
1684
1917
  AskQuestion \u5DF2\u901A\u8FC7 MCP \u670D\u52A1\u5668 custom-user-tools \u6CE8\u518C\uFF0C\u5DE5\u5177\u540D\u4E3A AskQuestion\u3002
@@ -1712,6 +1945,21 @@ function createCursorCustomTools(cfg, messageId, options) {
1712
1945
  cfg,
1713
1946
  taskId: options.taskId
1714
1947
  });
1948
+ tools.MergeWebIdePullRequests = createMergeWebIdePullRequestsTool({
1949
+ cfg,
1950
+ taskId: options.taskId
1951
+ });
1952
+ if (options.workdir) {
1953
+ Object.assign(
1954
+ tools,
1955
+ createWebIdeTerminalTools({
1956
+ cfg,
1957
+ taskId: options.taskId,
1958
+ workdir: options.workdir,
1959
+ rpc: options.terminalRpc
1960
+ })
1961
+ );
1962
+ }
1715
1963
  }
1716
1964
  return tools;
1717
1965
  }
@@ -1835,7 +2083,9 @@ async function runCursorAgent(cfg, ctx, options) {
1835
2083
  appendMessageContent: options?.appendMessageContent,
1836
2084
  askQuestionExecute: options?.askQuestionExecute,
1837
2085
  enableWebIdePlanTools: options?.enableWebIdePlanTools,
1838
- taskId: options?.taskId
2086
+ taskId: options?.taskId,
2087
+ workdir,
2088
+ terminalRpc: options?.terminalRpc
1839
2089
  });
1840
2090
  const enableSandbox = Boolean(options?.enableSandbox);
1841
2091
  let prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
@@ -2018,11 +2268,11 @@ function clearWebIdeAgentId(workdir, taskId) {
2018
2268
  // src/commands/connect/webide-ask-question.ts
2019
2269
  import { setTimeout as delay } from "node:timers/promises";
2020
2270
  var POLL_INTERVAL_MS = 2e3;
2021
- function asString3(value) {
2271
+ function asString4(value) {
2022
2272
  return typeof value === "string" ? value.trim() : "";
2023
2273
  }
2024
2274
  function parseQuestions(args) {
2025
- const title = asString3(args.title) || void 0;
2275
+ const title = asString4(args.title) || void 0;
2026
2276
  const raw = args.questions;
2027
2277
  if (!Array.isArray(raw) || raw.length === 0) {
2028
2278
  throw new Error("AskQuestion \u7F3A\u5C11 questions");
@@ -2031,16 +2281,16 @@ function parseQuestions(args) {
2031
2281
  for (const item of raw) {
2032
2282
  if (!item || typeof item !== "object" || Array.isArray(item)) continue;
2033
2283
  const row = item;
2034
- const id = asString3(row.id);
2035
- const prompt = asString3(row.prompt);
2284
+ const id = asString4(row.id);
2285
+ const prompt = asString4(row.prompt);
2036
2286
  const optionsRaw = row.options;
2037
2287
  if (!id || !prompt || !Array.isArray(optionsRaw)) continue;
2038
2288
  const options = [];
2039
2289
  for (const opt of optionsRaw) {
2040
2290
  if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
2041
2291
  const o = opt;
2042
- const oid = asString3(o.id);
2043
- const label = asString3(o.label);
2292
+ const oid = asString4(o.id);
2293
+ const label = asString4(o.label);
2044
2294
  if (oid && label) options.push({ id: oid, label });
2045
2295
  }
2046
2296
  if (options.length < 2) {
@@ -2074,11 +2324,11 @@ function createWebIdeAskQuestionExecute(options) {
2074
2324
  const byId = new Map(
2075
2325
  list.assumptions.map((a) => [a.questionId, a])
2076
2326
  );
2077
- const pending = parsed.questions.filter((q) => {
2327
+ const pending2 = parsed.questions.filter((q) => {
2078
2328
  const row = byId.get(q.id);
2079
2329
  return !row || row.status !== "RESOLVED";
2080
2330
  });
2081
- if (pending.length === 0) {
2331
+ if (pending2.length === 0) {
2082
2332
  const answers = parsed.questions.map((q) => {
2083
2333
  const row = byId.get(q.id);
2084
2334
  return {
@@ -2559,12 +2809,18 @@ function markBranchDone(sessionId, workdir) {
2559
2809
  lastBranchKey = sessionWorkdirKey(sessionId, workdir);
2560
2810
  }
2561
2811
 
2562
- // src/commands/connect/webide-draft-prs.ts
2563
- var DEVELOP_ACTIONS = /* @__PURE__ */ new Set(["start-develop", "skip-plan"]);
2564
- function shouldEnsureWebIdeDraftPullRequests(action) {
2565
- return DEVELOP_ACTIONS.has(action);
2812
+ // src/commands/connect/webide-pull-requests.ts
2813
+ var TEST_START_ACTIONS = /* @__PURE__ */ new Set([
2814
+ "generate-cases",
2815
+ "run-auto-test",
2816
+ // 跳过/直接进手工测试不发 Cursor,PR 延后到部署或验收
2817
+ "deploy",
2818
+ "accept"
2819
+ ]);
2820
+ function shouldEnsureWebIdePullRequests(action) {
2821
+ return TEST_START_ACTIONS.has(action);
2566
2822
  }
2567
- async function ensureWebIdeDraftPullRequests(cfg, taskId, workdir) {
2823
+ async function ensureWebIdePullRequests(cfg, taskId, workdir) {
2568
2824
  const api = createApmApiClient(cfg);
2569
2825
  let repoRoots;
2570
2826
  try {
@@ -2572,29 +2828,27 @@ async function ensureWebIdeDraftPullRequests(cfg, taskId, workdir) {
2572
2828
  repoRoots = resolveWorkspaceRepoAbsolutePaths(manifest);
2573
2829
  } catch (err) {
2574
2830
  console.warn(
2575
- "[apm] WebIDE \u8349\u7A3F PR\uFF1A\u8BFB\u53D6\u4ED3\u5E93\u6E05\u5355\u5931\u8D25:",
2831
+ "[apm] WebIDE PR\uFF1A\u8BFB\u53D6\u4ED3\u5E93\u6E05\u5355\u5931\u8D25:",
2576
2832
  err instanceof Error ? err.message : err
2577
2833
  );
2578
2834
  return;
2579
2835
  }
2580
2836
  console.log(
2581
- `[apm] WebIDE \u8349\u7A3F PR\uFF1A\u51C6\u5907\u4E3A ${repoRoots.length} \u4E2A\u4ED3\u5E93\u521B\u5EFA feat/task-${taskId}`
2837
+ `[apm] WebIDE PR\uFF1A\u51C6\u5907\u4E3A ${repoRoots.length} \u4E2A\u4ED3\u5E93\u521B\u5EFA feat/task-${taskId}`
2582
2838
  );
2583
2839
  for (const gitRoot of repoRoots) {
2584
2840
  const label = formatRepoLabel(workdir, gitRoot);
2585
2841
  try {
2586
2842
  const originUrl = await tryReadGitOriginUrl(gitRoot);
2587
2843
  if (!originUrl) {
2588
- console.warn(
2589
- `[apm] WebIDE \u8349\u7A3F PR\uFF1A\u8DF3\u8FC7 ${label}\uFF08\u65E0 remote.origin.url\uFF09`
2590
- );
2844
+ console.warn(`[apm] WebIDE PR\uFF1A\u8DF3\u8FC7 ${label}\uFF08\u65E0 remote.origin.url\uFF09`);
2591
2845
  continue;
2592
2846
  }
2593
2847
  const matched = await api.cli.matchRepository({ url: originUrl });
2594
2848
  const repositoryId = matched.repositoryId?.trim();
2595
2849
  if (!repositoryId) {
2596
2850
  console.warn(
2597
- `[apm] WebIDE \u8349\u7A3F PR\uFF1A\u8DF3\u8FC7 ${label}\uFF08\u5E73\u53F0\u672A\u767B\u8BB0\u4ED3\u5E93 ${originUrl}\uFF09`
2851
+ `[apm] WebIDE PR\uFF1A\u8DF3\u8FC7 ${label}\uFF08\u5E73\u53F0\u672A\u767B\u8BB0\u4ED3\u5E93 ${originUrl}\uFF09`
2598
2852
  );
2599
2853
  continue;
2600
2854
  }
@@ -2603,11 +2857,11 @@ async function ensureWebIdeDraftPullRequests(cfg, taskId, workdir) {
2603
2857
  repositoryId
2604
2858
  });
2605
2859
  console.log(
2606
- `[apm] WebIDE \u8349\u7A3F PR\uFF1A${label} \u2192 #${pr.number} ${pr.state} ${pr.url || ""}`
2860
+ `[apm] WebIDE PR\uFF1A${label} \u2192 #${pr.number} ${pr.state} ${pr.url || ""}`
2607
2861
  );
2608
2862
  } catch (err) {
2609
2863
  console.warn(
2610
- `[apm] WebIDE \u8349\u7A3F PR\uFF1A${label} \u5931\u8D25:`,
2864
+ `[apm] WebIDE PR\uFF1A${label} \u5931\u8D25:`,
2611
2865
  err instanceof Error ? err.message : err
2612
2866
  );
2613
2867
  }
@@ -2824,7 +3078,9 @@ function markSkillsSyncedForCliVersion(workdir, cliVersion) {
2824
3078
  var WEBIDE_CODE_CHANGE_ACTIONS = /* @__PURE__ */ new Set([
2825
3079
  "skip-plan",
2826
3080
  "start-develop",
2827
- "fix-and-retest"
3081
+ "fix-and-retest",
3082
+ "report-defect",
3083
+ "deploy"
2828
3084
  ]);
2829
3085
  function shouldCommitAfterWebIdeMessage(action) {
2830
3086
  return WEBIDE_CODE_CHANGE_ACTIONS.has(action);
@@ -2841,7 +3097,7 @@ async function appendContent(cfg, messageId, content) {
2841
3097
  const api = createApmApiClient(cfg);
2842
3098
  await api.cli.webideAppendMessageContent({ id: messageId, content });
2843
3099
  }
2844
- async function handleWebIdeInboundMessage(cfg, msg, signal) {
3100
+ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
2845
3101
  const workdir = requireRemoteWorkdir(msg.workdir);
2846
3102
  const messageId = msg.messageId;
2847
3103
  const taskId = msg.taskId;
@@ -2878,9 +3134,14 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2878
3134
  `[apm] step=branch skipped taskId=${taskId} workdir=${workdir}`
2879
3135
  );
2880
3136
  }
2881
- if (shouldEnsureWebIdeDraftPullRequests(msg.action)) {
3137
+ if (shouldEnsureWebIdePullRequests(msg.action)) {
2882
3138
  if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
2883
- await ensureWebIdeDraftPullRequests(cfg, taskId, workdir);
3139
+ const pushed = await pushWorkspaceRepos(workdir);
3140
+ console.log(
3141
+ `[apm] webide \u6D4B\u8BD5\u524D push action=${msg.action} repos=${pushed}`
3142
+ );
3143
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3144
+ await ensureWebIdePullRequests(cfg, taskId, workdir);
2884
3145
  }
2885
3146
  const savedAgentId = loadWebIdeAgentId(workdir, taskId);
2886
3147
  const logSyncRef = { current: null };
@@ -2908,6 +3169,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2908
3169
  // 本地 SDK sandbox 会拦截 custom-user-tools MCP,WebIDE 先关闭
2909
3170
  enableSandbox: false,
2910
3171
  taskId,
3172
+ terminalRpc: options?.terminalRpc,
2911
3173
  createRemoteLogSync: (agentId) => {
2912
3174
  saveWebIdeAgentId(workdir, taskId, agentId);
2913
3175
  logSyncRef.current = createThrottledWebIdeMessageLogSync(
@@ -2974,6 +3236,11 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2974
3236
  console.log(
2975
3237
  `[apm] webide \u515C\u5E95 commit action=${msg.action} repos=${committed}`
2976
3238
  );
3239
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3240
+ const pushed = await pushWorkspaceRepos(workdir);
3241
+ console.log(
3242
+ `[apm] webide \u81EA\u52A8 push action=${msg.action} repos=${pushed}`
3243
+ );
2977
3244
  }
2978
3245
  await logSyncRef.current?.markRun(outcome.runId, "finished");
2979
3246
  await updateStatus(cfg, messageId, "SUCCESS");
@@ -2997,19 +3264,68 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
2997
3264
  }
2998
3265
  }
2999
3266
 
3267
+ // src/commands/connect/webide-terminal-rpc.ts
3268
+ import { parentPort } from "node:worker_threads";
3269
+ var nextId = 1;
3270
+ var pending = /* @__PURE__ */ new Map();
3271
+ function installTerminalRpcListener() {
3272
+ parentPort?.on("message", (raw) => {
3273
+ if (!raw || raw.type !== "terminal-rpc-result") return;
3274
+ const p = pending.get(raw.id);
3275
+ if (!p) return;
3276
+ pending.delete(raw.id);
3277
+ if (raw.ok) p.resolve(raw.result);
3278
+ else p.reject(new Error(raw.error || "terminal rpc failed"));
3279
+ });
3280
+ }
3281
+ function call(op, args) {
3282
+ const id = nextId++;
3283
+ return new Promise((resolve6, reject) => {
3284
+ if (!parentPort) {
3285
+ reject(new Error("parentPort \u4E0D\u53EF\u7528"));
3286
+ return;
3287
+ }
3288
+ pending.set(id, { resolve: resolve6, reject });
3289
+ parentPort.postMessage({
3290
+ type: "terminal-rpc",
3291
+ id,
3292
+ op,
3293
+ args
3294
+ });
3295
+ setTimeout(() => {
3296
+ if (pending.has(id)) {
3297
+ pending.delete(id);
3298
+ reject(new Error(`terminal rpc timeout op=${op}`));
3299
+ }
3300
+ }, 12e4);
3301
+ });
3302
+ }
3303
+ function createWorkerTerminalRpc() {
3304
+ return {
3305
+ start: (args) => call("start", args),
3306
+ stop: (args) => call("stop", args),
3307
+ stopAll: (args) => call("stopAll", args),
3308
+ list: (args) => call("list", args)
3309
+ };
3310
+ }
3311
+
3000
3312
  // src/commands/connect/webide-message-worker.ts
3001
3313
  var controllers = /* @__PURE__ */ new Map();
3314
+ installTerminalRpcListener();
3315
+ var terminalRpc = createWorkerTerminalRpc();
3002
3316
  async function runJob(cfg, msg) {
3003
3317
  const controller = new AbortController();
3004
3318
  controllers.set(msg.messageId, controller);
3005
3319
  try {
3006
- await handleWebIdeInboundMessage(cfg, msg, controller.signal);
3007
- parentPort?.postMessage({
3320
+ await handleWebIdeInboundMessage(cfg, msg, controller.signal, {
3321
+ terminalRpc
3322
+ });
3323
+ parentPort2?.postMessage({
3008
3324
  type: "done",
3009
3325
  messageId: msg.messageId
3010
3326
  });
3011
3327
  } catch (err) {
3012
- parentPort?.postMessage({
3328
+ parentPort2?.postMessage({
3013
3329
  type: "error",
3014
3330
  messageId: msg.messageId,
3015
3331
  error: err instanceof Error ? err.message : String(err)
@@ -3018,7 +3334,7 @@ async function runJob(cfg, msg) {
3018
3334
  controllers.delete(msg.messageId);
3019
3335
  }
3020
3336
  }
3021
- parentPort?.on("message", (raw) => {
3337
+ parentPort2?.on("message", (raw) => {
3022
3338
  if (raw.type === "cancel") {
3023
3339
  controllers.get(raw.messageId)?.abort();
3024
3340
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "7.1.11",
3
+ "version": "7.1.13",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,8 +29,9 @@
29
29
  - 规则: `.apm/rules/*`
30
30
  - reply.md:当需要回复消息时需要读取这个文档,记住回复规则
31
31
  - write_doc.md:当需要写文档时需要读取这个文档,记住写文档的规则
32
- - webide_git_commit.md:WebIDE 开发/修码时读取,按规范分轮 commit
32
+ - webide_git_commit.md:WebIDE 开发/修码时读取,按规范分轮 commit,结束前 push
33
33
  - webide_testcase.md:WebIDE 生成测试用例时读取,按规范编写并提交用例
34
+ - webide_merge.md:WebIDE 验收通过后读取;直接调用 MergeWebIdePullRequests 合并代码
34
35
 
35
36
  - 仓库项目上下文(`.apm/project/`):
36
37
 
@@ -10,4 +10,7 @@
10
10
 
11
11
  ### 结束前检查
12
12
 
13
- 本轮全部工作结束前,确认工作区干净(`git status` 无未提交变更);若仍有残留改动,补一次 commit。
13
+ 本轮全部工作结束前:
14
+
15
+ 1. 确认工作区干净(`git status` 无未提交变更);若仍有残留改动,补一次 commit。
16
+ 2. 将当前分支推送到远程:已有 upstream 用 `git push`,否则 `git push -u origin HEAD`。多仓工作区时,只 push 实际有新 commit 的仓库。
@@ -0,0 +1,16 @@
1
+ ## WebIDE 合并规范
2
+
3
+ 适用场景:WebIDE 验收通过(`accept`)后合并本任务 PR。
4
+
5
+ ### 目标
6
+
7
+ 将本任务各仓库特性分支上的变更合并进基线分支(通常为 `main`)。
8
+
9
+ ### 步骤
10
+
11
+ 1. 调用一次 `MergeWebIdePullRequests`(`confirmedConflictFree=true`),由平台合并本任务全部 OPEN PR。
12
+ 2. 用 `AppendMessage` 汇总合并结果(PR 编号、仓库)。
13
+
14
+ ### 禁止
15
+
16
+ - 在合并之外自行执行同步基线、代码审核、部署等额外流程
@@ -0,0 +1,35 @@
1
+ # WebIDE 长驻终端
2
+
3
+ 长驻进程(如前后端 `dev`)必须用自定义工具管理,**禁止**用 Shell 启动会长期占用的 dev server。
4
+
5
+ ## 工具
6
+
7
+ - `StartWebIdeTerminal`:`{ key, name?, cwd, command, readyPattern?, portsHint? }`
8
+ - `key` 稳定标识,如 `frontend` / `backend`;同 key 已在跑(RUNNING/READY)则幂等返回
9
+ - 进程挂在 CLI 主进程,跨多轮消息存活
10
+ - `StopWebIdeTerminal`:`{ key }` 或 `{ terminalId }`;杀进程树,状态改为 EXITED(不删记录)
11
+ - `StopAllWebIdeTerminals`:停掉本 task 全部仍存活的终端(重跑本地环境前使用)
12
+ - `ListWebIdeTerminals`:列出**存活**终端(STARTING / RUNNING / READY / STOPPING),**不含** EXITED / FAILED
13
+
14
+ ## 启动命令来源
15
+
16
+ 必须 Read `.apm/project/deploy.md`,按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
17
+
18
+ - 文档不存在或未写明启动方式 → 用 AppendMessage 说明无法启动本地环境,**禁止猜测命令**。
19
+
20
+ ## 时机
21
+
22
+ 1. **首次**(`start-develop` / 跳过计划后的开发轮次):实现完成后需要本地环境时
23
+ - 先 `ListWebIdeTerminals`
24
+ - 若无活终端 → Read `deploy.md` → 按前后端分别 `StartWebIdeTerminal`
25
+ - 等待 READY / 端口出现后,告知用户可在 WebIDE「终端」面板观察日志(勿粘贴整段 stdout)
26
+ 2. **后续改码**(热更新):有活终端则**复用,不重起**
27
+ 3. **显式重跑**(端口冲突、命令变更、或平台「重跑本地环境」指令):
28
+ `StopAllWebIdeTerminals` → 再 Read `deploy.md` → 重新 Start
29
+ 不要在已死进程上糊弄;需要换命令时也是先停再建
30
+
31
+ ## 规范
32
+
33
+ 1. 禁止用 Shell / 后台进程方式起 `dev` / `serve` 等长驻命令。
34
+ 2. 观察日志请用户看 WebIDE「终端」面板。
35
+ 3. 任务结束或不再需要时可 Stop;不要留下孤儿进程。