ai-project-manage-cli 7.1.12 → 7.1.15

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,10 +100,34 @@ 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
+ }),
119
+ acquireSteelBrowser: defineEndpoint({
120
+ method: "POST",
121
+ path: "/cli/steel-browser/acquire"
122
+ }),
103
123
  branchBaseline: defineEndpoint({
104
124
  method: "GET",
105
125
  path: "/cli/tasks/branch-baseline"
106
126
  }),
127
+ projectBaseBranch: defineEndpoint({
128
+ method: "GET",
129
+ path: "/cli/tasks/project-base-branch"
130
+ }),
107
131
  listSessionsForBranchCleanup: defineEndpoint({
108
132
  method: "GET",
109
133
  path: "/cli/sessions/branch-cleanup"
@@ -140,21 +164,21 @@ var requestConfig = {
140
164
  method: "POST",
141
165
  path: "/cli/webide/pull-requests/merge"
142
166
  }),
143
- getRepositoryProjectDocumentManifest: defineEndpoint({
167
+ getProjectDocumentManifest: defineEndpoint({
144
168
  method: "GET",
145
- path: "/cli/repository-project-documents/manifest"
169
+ path: "/cli/project-documents/manifest"
146
170
  }),
147
- listRepositoryProjectDocuments: defineEndpoint({
171
+ listProjectDocuments: defineEndpoint({
148
172
  method: "GET",
149
- path: "/cli/repository-project-documents"
173
+ path: "/cli/project-documents"
150
174
  }),
151
- upsertRepositoryProjectDocument: defineEndpoint({
175
+ upsertProjectDocument: defineEndpoint({
152
176
  method: "PUT",
153
- path: "/cli/repository-project-documents/upsert"
177
+ path: "/cli/project-documents/upsert"
154
178
  }),
155
- removeRepositoryProjectDocument: defineEndpoint({
179
+ removeProjectDocument: defineEndpoint({
156
180
  method: "DELETE",
157
- path: "/cli/repository-project-documents"
181
+ path: "/cli/project-documents"
158
182
  }),
159
183
  createTaskDeployment: defineEndpoint({
160
184
  method: "POST",
@@ -191,6 +215,7 @@ import { homedir } from "os";
191
215
  import { join } from "path";
192
216
  var APM_CONFIG_DIR = join(homedir(), ".config", "apm");
193
217
  var APM_CONFIG_PATH = join(APM_CONFIG_DIR, "config.json");
218
+ var DEFAULT_BASE_URL = "http://127.0.0.1:3000";
194
219
  function resolveClientMachineId(cfg) {
195
220
  return (cfg.clientMachineId ?? cfg.userId ?? "").trim();
196
221
  }
@@ -221,6 +246,33 @@ async function tryReadApmConfig() {
221
246
  return null;
222
247
  }
223
248
  }
249
+ function defaultApmConfig() {
250
+ return {
251
+ baseUrl: DEFAULT_BASE_URL,
252
+ clientMachineId: "",
253
+ apiKey: ""
254
+ };
255
+ }
256
+ async function ensureApmConfig() {
257
+ const existing = await tryReadApmConfig();
258
+ if (existing) return existing;
259
+ const defaults = defaultApmConfig();
260
+ await writeApmConfig(defaults);
261
+ return defaults;
262
+ }
263
+ async function writeApmConfig(cfg) {
264
+ const clientMachineId = resolveClientMachineId(cfg);
265
+ const apiKey = resolveApiKey(cfg);
266
+ const normalized = {
267
+ baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
268
+ apiKey,
269
+ ...clientMachineId ? { clientMachineId } : {}
270
+ };
271
+ mkdirSync(APM_CONFIG_DIR, { recursive: true });
272
+ writeFileSync(APM_CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", {
273
+ mode: 384
274
+ });
275
+ }
224
276
 
225
277
  // src/api/client.ts
226
278
  function createApmApiClient(cfg) {
@@ -359,24 +411,6 @@ async function ensureRemoteBaselineBranch(cwd, baselineBranch) {
359
411
  `[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`
360
412
  );
361
413
  }
362
- async function resolveDefaultRemoteBranch(cwd) {
363
- try {
364
- const ref = (await execGit(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], true)).trim();
365
- const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
366
- if (match?.[1]) {
367
- return match[1];
368
- }
369
- } catch {
370
- }
371
- for (const candidate of ["main", "master"]) {
372
- if (await remoteBranchExists(cwd, candidate)) {
373
- return candidate;
374
- }
375
- }
376
- throw new Error(
377
- "[apm] \u65E0\u6CD5\u786E\u5B9A\u8FDC\u7A0B\u9ED8\u8BA4\u5206\u652F\uFF08origin/HEAD\u3001main\u3001master \u5747\u4E0D\u53EF\u7528\uFF09"
378
- );
379
- }
380
414
  async function hasUpstream(cwd) {
381
415
  try {
382
416
  await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
@@ -557,6 +591,16 @@ function assertApmGitignoredInRepo(workdir) {
557
591
  );
558
592
  }
559
593
  }
594
+ async function ensureLoggedConfig() {
595
+ const cfg = await ensureApmConfig();
596
+ if (!resolveApiKey(cfg)) {
597
+ console.error(
598
+ "[apm] \u672A\u68C0\u6D4B\u5230 API Key\uFF0C\u8BF7\u5148\u6267\u884C: apm login --api-key cm_xxx"
599
+ );
600
+ process.exit(1);
601
+ }
602
+ return cfg;
603
+ }
560
604
  async function ensureDirExists(dir) {
561
605
  mkdirSync2(dir, { recursive: true });
562
606
  }
@@ -961,6 +1005,17 @@ async function runTaskBranch(taskId, options = {}) {
961
1005
  if (!trimmedTaskId) {
962
1006
  throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
963
1007
  }
1008
+ const cfg = await ensureLoggedConfig();
1009
+ const api = createApmApiClient(cfg);
1010
+ const projectBaseline = await api.cli.projectBaseBranch({
1011
+ taskId: trimmedTaskId
1012
+ });
1013
+ const baselineBranch = projectBaseline.baseBranch?.trim() ?? "";
1014
+ if (!baselineBranch) {
1015
+ throw new Error(
1016
+ "[apm] \u9879\u76EE\u672A\u914D\u7F6E\u57FA\u7840\u5206\u652F\uFF0C\u65E0\u6CD5\u521B\u5EFA WebIDE \u7279\u6027\u5206\u652F"
1017
+ );
1018
+ }
964
1019
  const cwd = options.cwd ?? process.cwd();
965
1020
  const workdirPath = resolveWorkdirPath(cwd);
966
1021
  const manifest = loadWorkspaceReposCache(workdirPath);
@@ -968,9 +1023,8 @@ async function runTaskBranch(taskId, options = {}) {
968
1023
  const branch = branchNameForTask(trimmedTaskId);
969
1024
  for (const gitRoot of repoRoots) {
970
1025
  const label = formatRepoLabel(workdirPath, gitRoot);
971
- const baselineBranch = await resolveDefaultRemoteBranch(gitRoot);
972
1026
  console.log(
973
- `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (baseline=${baselineBranch})`
1027
+ `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (project baseline=${baselineBranch})`
974
1028
  );
975
1029
  await ensureFeatureBranch(branch, baselineBranch, {
976
1030
  ...options,
@@ -1559,13 +1613,13 @@ ${payload}`;
1559
1613
  function createMergeWebIdePullRequestsTool(options) {
1560
1614
  const { cfg, taskId } = options;
1561
1615
  return {
1562
- description: "Merge all open pull requests for this WebIDE task after conflicts are resolved locally and pushed. Call once with no arguments. Do NOT call if conflicts contradict the task requirements \u2014 report the conflict and fail instead.",
1616
+ description: "Merge all open pull requests for this WebIDE task. Call once with confirmedConflictFree=true. No baseline sync or review is required before calling.",
1563
1617
  inputSchema: {
1564
1618
  type: "object",
1565
1619
  properties: {
1566
1620
  confirmedConflictFree: {
1567
1621
  type: "boolean",
1568
- description: "Must be true: you have checked each repo, resolved mergeable conflicts consistently with requirements, and pushed. If conflicts contradict requirements, do not call this tool."
1622
+ description: "Must be true to confirm you intend to merge this task's open PRs now."
1569
1623
  }
1570
1624
  },
1571
1625
  required: ["confirmedConflictFree"]
@@ -1573,7 +1627,7 @@ function createMergeWebIdePullRequestsTool(options) {
1573
1627
  execute: async (args) => {
1574
1628
  if (args.confirmedConflictFree !== true) {
1575
1629
  throw new Error(
1576
- "MergeWebIdePullRequests \u8981\u6C42 confirmedConflictFree=true\u3002\u82E5\u5B58\u5728\u4E0E\u9700\u6C42\u76F8\u6096\u7684\u51B2\u7A81\uFF0C\u8BF7\u52FF\u8C03\u7528\u672C\u5DE5\u5177\uFF0C\u7528 AppendMessage \u8BF4\u660E\u5E76\u5931\u8D25\u7ED3\u675F\u3002"
1630
+ "MergeWebIdePullRequests \u8981\u6C42 confirmedConflictFree=true\u3002"
1577
1631
  );
1578
1632
  }
1579
1633
  const api = createApmApiClient(cfg);
@@ -1627,7 +1681,7 @@ function createRecommendPlanTool(options) {
1627
1681
  function createUpsertWebIdePlanTool(options) {
1628
1682
  const { cfg, taskId } = options;
1629
1683
  return {
1630
- description: "Persist the implementation plan markdown for this WebIDE task and mark the plan as ready for human confirmation. Call once with the full plan document.",
1684
+ description: "Persist or replace the implementation plan markdown for this WebIDE task and mark it ready for human confirmation. Call with the full plan document (not a diff). Use after generating or revising the plan.",
1631
1685
  inputSchema: {
1632
1686
  type: "object",
1633
1687
  properties: {
@@ -1738,6 +1792,164 @@ function createUpsertWebIdeTestCasesTool(options) {
1738
1792
  };
1739
1793
  }
1740
1794
 
1795
+ // src/commands/connect/webide-terminal-tools.ts
1796
+ function asString3(value) {
1797
+ return typeof value === "string" ? value.trim() : "";
1798
+ }
1799
+ function asPorts(value) {
1800
+ if (!Array.isArray(value)) return void 0;
1801
+ 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));
1802
+ return ports.length ? ports : void 0;
1803
+ }
1804
+ function createWebIdeTerminalTools(options) {
1805
+ const { cfg, taskId, workdir, rpc } = options;
1806
+ const StartWebIdeTerminal = {
1807
+ 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.",
1808
+ inputSchema: {
1809
+ type: "object",
1810
+ properties: {
1811
+ key: {
1812
+ type: "string",
1813
+ description: "Stable key, e.g. frontend / backend"
1814
+ },
1815
+ name: { type: "string", description: "Display name" },
1816
+ cwd: {
1817
+ type: "string",
1818
+ description: "Working directory (absolute or under workspace)"
1819
+ },
1820
+ command: {
1821
+ type: "string",
1822
+ description: "Shell command to run, e.g. pnpm dev"
1823
+ },
1824
+ readyPattern: {
1825
+ type: "string",
1826
+ description: "Optional regex; when matched in logs, status becomes READY"
1827
+ },
1828
+ portsHint: {
1829
+ type: "array",
1830
+ items: { type: "number" },
1831
+ description: "Optional expected listen ports"
1832
+ }
1833
+ },
1834
+ required: ["key", "cwd", "command"]
1835
+ },
1836
+ execute: async (args) => {
1837
+ const key = asString3(args.key);
1838
+ const cwd = asString3(args.cwd) || workdir;
1839
+ const command = asString3(args.command);
1840
+ if (!key || !command) {
1841
+ return {
1842
+ content: [{ type: "text", text: "key / command \u4E0D\u80FD\u4E3A\u7A7A" }],
1843
+ isError: true
1844
+ };
1845
+ }
1846
+ try {
1847
+ const result = rpc ? await rpc.start({
1848
+ taskId,
1849
+ key,
1850
+ name: asString3(args.name) || void 0,
1851
+ cwd,
1852
+ command,
1853
+ readyPattern: asString3(args.readyPattern) || void 0,
1854
+ portsHint: asPorts(args.portsHint)
1855
+ }) : await (async () => {
1856
+ throw new Error("Terminal RPC \u4E0D\u53EF\u7528\uFF08\u9700\u5728 connect \u4E3B\u8FDB\u7A0B\uFF09");
1857
+ })();
1858
+ return JSON.stringify(result, null, 2);
1859
+ } catch (err) {
1860
+ const detail = err instanceof Error ? err.message : String(err);
1861
+ return {
1862
+ content: [{ type: "text", text: `\u542F\u52A8\u7EC8\u7AEF\u5931\u8D25: ${detail}` }],
1863
+ isError: true
1864
+ };
1865
+ }
1866
+ }
1867
+ };
1868
+ const StopWebIdeTerminal = {
1869
+ description: "Stop a WebIDE terminal and kill its process tree. Soft-terminates (status EXITED, record kept).",
1870
+ inputSchema: {
1871
+ type: "object",
1872
+ properties: {
1873
+ key: { type: "string", description: "Terminal key" },
1874
+ terminalId: { type: "string", description: "Server terminal id" }
1875
+ }
1876
+ },
1877
+ execute: async (args) => {
1878
+ const key = asString3(args.key) || void 0;
1879
+ const terminalId = asString3(args.terminalId) || void 0;
1880
+ if (!key && !terminalId) {
1881
+ return {
1882
+ content: [{ type: "text", text: "\u9700\u8981 key \u6216 terminalId" }],
1883
+ isError: true
1884
+ };
1885
+ }
1886
+ try {
1887
+ const result = rpc ? await rpc.stop({ taskId, key, terminalId }) : await (async () => {
1888
+ throw new Error("Terminal RPC \u4E0D\u53EF\u7528\uFF08\u9700\u5728 connect \u4E3B\u8FDB\u7A0B\uFF09");
1889
+ })();
1890
+ return JSON.stringify(result, null, 2);
1891
+ } catch (err) {
1892
+ const detail = err instanceof Error ? err.message : String(err);
1893
+ return {
1894
+ content: [{ type: "text", text: `\u505C\u6B62\u7EC8\u7AEF\u5931\u8D25: ${detail}` }],
1895
+ isError: true
1896
+ };
1897
+ }
1898
+ }
1899
+ };
1900
+ const StopAllWebIdeTerminals = {
1901
+ 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.",
1902
+ inputSchema: {
1903
+ type: "object",
1904
+ properties: {}
1905
+ },
1906
+ execute: async () => {
1907
+ try {
1908
+ const result = rpc ? await rpc.stopAll({ taskId }) : await (async () => {
1909
+ throw new Error("Terminal RPC \u4E0D\u53EF\u7528\uFF08\u9700\u5728 connect \u4E3B\u8FDB\u7A0B\uFF09");
1910
+ })();
1911
+ return JSON.stringify(result, null, 2);
1912
+ } catch (err) {
1913
+ const detail = err instanceof Error ? err.message : String(err);
1914
+ return {
1915
+ content: [{ type: "text", text: `\u5168\u90E8\u505C\u6B62\u5931\u8D25: ${detail}` }],
1916
+ isError: true
1917
+ };
1918
+ }
1919
+ }
1920
+ };
1921
+ const ListWebIdeTerminals = {
1922
+ 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.",
1923
+ inputSchema: {
1924
+ type: "object",
1925
+ properties: {}
1926
+ },
1927
+ execute: async () => {
1928
+ try {
1929
+ if (rpc) {
1930
+ const result2 = await rpc.list({ taskId });
1931
+ return JSON.stringify(result2, null, 2);
1932
+ }
1933
+ const api = createApmApiClient(cfg);
1934
+ const result = await api.cli.webideListTerminals({ taskId });
1935
+ return JSON.stringify(result, null, 2);
1936
+ } catch (err) {
1937
+ const detail = err instanceof Error ? err.message : String(err);
1938
+ return {
1939
+ content: [{ type: "text", text: `\u5217\u51FA\u7EC8\u7AEF\u5931\u8D25: ${detail}` }],
1940
+ isError: true
1941
+ };
1942
+ }
1943
+ }
1944
+ };
1945
+ return {
1946
+ StartWebIdeTerminal,
1947
+ StopWebIdeTerminal,
1948
+ StopAllWebIdeTerminals,
1949
+ ListWebIdeTerminals
1950
+ };
1951
+ }
1952
+
1741
1953
  // src/commands/connect/cursor-custom-tools.ts
1742
1954
  var PLAN_MODE_ASK_QUESTION_HINT = `[SDK \u73AF\u5883\u8BF4\u660E]
1743
1955
  AskQuestion \u5DF2\u901A\u8FC7 MCP \u670D\u52A1\u5668 custom-user-tools \u6CE8\u518C\uFF0C\u5DE5\u5177\u540D\u4E3A AskQuestion\u3002
@@ -1775,6 +1987,17 @@ function createCursorCustomTools(cfg, messageId, options) {
1775
1987
  cfg,
1776
1988
  taskId: options.taskId
1777
1989
  });
1990
+ if (options.workdir) {
1991
+ Object.assign(
1992
+ tools,
1993
+ createWebIdeTerminalTools({
1994
+ cfg,
1995
+ taskId: options.taskId,
1996
+ workdir: options.workdir,
1997
+ rpc: options.terminalRpc
1998
+ })
1999
+ );
2000
+ }
1778
2001
  }
1779
2002
  return tools;
1780
2003
  }
@@ -1843,8 +2066,8 @@ async function obtainAgent(ctx) {
1843
2066
  settingSources: ["project"]
1844
2067
  } : {}
1845
2068
  },
1846
- ...ctx.mode ? { mode: ctx.mode } : {}
1847
- // mcpServers: createPlaywrightMcpServers(),
2069
+ ...ctx.mode ? { mode: ctx.mode } : {},
2070
+ ...ctx.mcpServers ? { mcpServers: ctx.mcpServers } : {}
1848
2071
  };
1849
2072
  const explicitAgentId = ctx.resumeAgentId?.trim();
1850
2073
  const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
@@ -1898,7 +2121,9 @@ async function runCursorAgent(cfg, ctx, options) {
1898
2121
  appendMessageContent: options?.appendMessageContent,
1899
2122
  askQuestionExecute: options?.askQuestionExecute,
1900
2123
  enableWebIdePlanTools: options?.enableWebIdePlanTools,
1901
- taskId: options?.taskId
2124
+ taskId: options?.taskId,
2125
+ workdir,
2126
+ terminalRpc: options?.terminalRpc
1902
2127
  });
1903
2128
  const enableSandbox = Boolean(options?.enableSandbox);
1904
2129
  let prompt = withPlanModeToolHint(ctx.prompt, ctx.mode);
@@ -1939,7 +2164,6 @@ async function runCursorAgent(cfg, ctx, options) {
1939
2164
  try {
1940
2165
  const run = await agent.send(prompt, {
1941
2166
  ...ctx.mode ? { mode: ctx.mode } : {},
1942
- // mcpServers: createPlaywrightMcpServers(),
1943
2167
  local: {
1944
2168
  ...options?.forceSend ? { force: true } : {},
1945
2169
  customTools
@@ -2081,11 +2305,11 @@ function clearWebIdeAgentId(workdir, taskId) {
2081
2305
  // src/commands/connect/webide-ask-question.ts
2082
2306
  import { setTimeout as delay } from "node:timers/promises";
2083
2307
  var POLL_INTERVAL_MS = 2e3;
2084
- function asString3(value) {
2308
+ function asString4(value) {
2085
2309
  return typeof value === "string" ? value.trim() : "";
2086
2310
  }
2087
2311
  function parseQuestions(args) {
2088
- const title = asString3(args.title) || void 0;
2312
+ const title = asString4(args.title) || void 0;
2089
2313
  const raw = args.questions;
2090
2314
  if (!Array.isArray(raw) || raw.length === 0) {
2091
2315
  throw new Error("AskQuestion \u7F3A\u5C11 questions");
@@ -2094,16 +2318,16 @@ function parseQuestions(args) {
2094
2318
  for (const item of raw) {
2095
2319
  if (!item || typeof item !== "object" || Array.isArray(item)) continue;
2096
2320
  const row = item;
2097
- const id = asString3(row.id);
2098
- const prompt = asString3(row.prompt);
2321
+ const id = asString4(row.id);
2322
+ const prompt = asString4(row.prompt);
2099
2323
  const optionsRaw = row.options;
2100
2324
  if (!id || !prompt || !Array.isArray(optionsRaw)) continue;
2101
2325
  const options = [];
2102
2326
  for (const opt of optionsRaw) {
2103
2327
  if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
2104
2328
  const o = opt;
2105
- const oid = asString3(o.id);
2106
- const label = asString3(o.label);
2329
+ const oid = asString4(o.id);
2330
+ const label = asString4(o.label);
2107
2331
  if (oid && label) options.push({ id: oid, label });
2108
2332
  }
2109
2333
  if (options.length < 2) {
@@ -2137,11 +2361,11 @@ function createWebIdeAskQuestionExecute(options) {
2137
2361
  const byId = new Map(
2138
2362
  list.assumptions.map((a) => [a.questionId, a])
2139
2363
  );
2140
- const pending = parsed.questions.filter((q) => {
2364
+ const pending2 = parsed.questions.filter((q) => {
2141
2365
  const row = byId.get(q.id);
2142
2366
  return !row || row.status !== "RESOLVED";
2143
2367
  });
2144
- if (pending.length === 0) {
2368
+ if (pending2.length === 0) {
2145
2369
  const answers = parsed.questions.map((q) => {
2146
2370
  const row = byId.get(q.id);
2147
2371
  return {
@@ -2435,7 +2659,7 @@ ${diagnostic ?? ""}
2435
2659
  return { synced: true, repositoryId, configName: config.name };
2436
2660
  }
2437
2661
 
2438
- // src/repository-project-documents-sync.ts
2662
+ // src/project-documents-sync.ts
2439
2663
  import {
2440
2664
  existsSync as existsSync5,
2441
2665
  readdirSync as readdirSync3,
@@ -2446,7 +2670,7 @@ import {
2446
2670
  import { dirname as dirname4, join as join6, relative as relative2, sep } from "path";
2447
2671
  var MANIFEST_FILE = "manifest.json";
2448
2672
  function projectDocumentsDir(apmRoot) {
2449
- return join6(apmRoot ?? workspaceApmDir(), "project");
2673
+ return join6(apmRoot ?? workspaceApmDir(), "docs");
2450
2674
  }
2451
2675
  function projectDocumentLocalPath(apmRoot, documentPath) {
2452
2676
  const normalized = normalizeLocalDocumentPath(documentPath);
@@ -2497,41 +2721,65 @@ function diffManifestPaths(remote, local) {
2497
2721
  }
2498
2722
  return { download, deleteLocal };
2499
2723
  }
2500
- async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
2724
+ async function resolveProjectIdForSync(api, workdirPath) {
2725
+ try {
2726
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
2727
+ const projectId = baseline.projectId?.trim() || null;
2728
+ if (projectId) {
2729
+ return { projectId, diagnostic: null };
2730
+ }
2731
+ const projectIds = baseline.projectIds ?? [];
2732
+ if (projectIds.length === 0) {
2733
+ return {
2734
+ projectId: null,
2735
+ diagnostic: baseline.diagnostic?.message ?? `\u5DE5\u4F5C\u7A7A\u95F4\u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF08\u76EE\u5F55\uFF1A${workdirPath}\uFF09\u3002
2736
+ \u8BF7\u5728\u5E73\u53F0\u5C06\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u5230\u552F\u4E00\u9879\u76EE\u3002`
2737
+ };
2738
+ }
2739
+ return {
2740
+ projectId: null,
2741
+ diagnostic: `\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u4E86 ${projectIds.length} \u4E2A\u9879\u76EE\uFF0C\u65E0\u6CD5\u786E\u5B9A\u540C\u6B65\u76EE\u6807\u3002
2742
+ \u8BF7\u786E\u4FDD\u8BE5\u5DE5\u4F5C\u7A7A\u95F4\u53EA\u5173\u8054\u4E00\u4E2A\u9879\u76EE\u540E\u518D\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
2743
+ };
2744
+ } catch (err) {
2745
+ const detail = err instanceof Error ? err.message : String(err);
2746
+ return {
2747
+ projectId: null,
2748
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
2749
+ };
2750
+ }
2751
+ }
2752
+ async function syncProjectDocumentsPull(workdirPath, apmDir) {
2501
2753
  const empty = {
2502
2754
  synced: false,
2503
- repositoryId: null,
2755
+ projectId: null,
2504
2756
  downloaded: 0,
2505
2757
  deleted: 0
2506
2758
  };
2507
2759
  const cfg = await tryReadApmConfig();
2508
2760
  if (!cfg || !resolveApiKey(cfg)) {
2509
2761
  console.log(
2510
- "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
2762
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
2511
2763
  );
2512
2764
  return empty;
2513
2765
  }
2514
2766
  const api = createApmApiClient(cfg);
2515
- const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
2767
+ const { projectId, diagnostic } = await resolveProjectIdForSync(
2516
2768
  api,
2517
2769
  workdirPath
2518
2770
  );
2519
- if (!repositoryId) {
2520
- console.log(
2521
- `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
2522
- ${diagnostic ?? ""}`
2523
- );
2771
+ if (!projectId) {
2772
+ console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
2773
+ ${diagnostic ?? ""}`);
2524
2774
  return empty;
2525
2775
  }
2526
2776
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
2527
- const projectDir = projectDocumentsDir(targetApmDir);
2528
- await ensureDirExists(projectDir);
2529
- const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
2777
+ const docsDir = projectDocumentsDir(targetApmDir);
2778
+ await ensureDirExists(docsDir);
2779
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
2530
2780
  if (!remoteManifest) {
2531
- console.log(
2532
- `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
2533
- );
2534
- return { ...empty, repositoryId };
2781
+ console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
2782
+ return { ...empty, projectId };
2535
2783
  }
2536
2784
  const localManifest = readLocalManifest(targetApmDir);
2537
2785
  const { download, deleteLocal } = diffManifestPaths(
@@ -2540,12 +2788,14 @@ ${diagnostic ?? ""}`
2540
2788
  );
2541
2789
  let downloaded = 0;
2542
2790
  if (download.length > 0) {
2543
- const { list } = await api.cli.listRepositoryProjectDocuments({
2544
- repositoryId,
2791
+ const { list } = await api.cli.listProjectDocuments({
2792
+ projectId,
2545
2793
  paths: download.join(",")
2546
2794
  });
2547
2795
  for (const doc of list) {
2548
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
2796
+ const absPath = toFsPath(
2797
+ projectDocumentLocalPath(targetApmDir, doc.path)
2798
+ );
2549
2799
  await ensureDirExists(dirname4(absPath));
2550
2800
  writeFileSync7(absPath, doc.content, "utf8");
2551
2801
  downloaded += 1;
@@ -2560,17 +2810,17 @@ ${diagnostic ?? ""}`
2560
2810
  }
2561
2811
  }
2562
2812
  writeFileSync7(
2563
- toFsPath(join6(projectDir, MANIFEST_FILE)),
2813
+ toFsPath(join6(docsDir, MANIFEST_FILE)),
2564
2814
  `${JSON.stringify(remoteManifest, null, 2)}
2565
2815
  `,
2566
2816
  "utf8"
2567
2817
  );
2568
2818
  console.log(
2569
- `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2819
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2570
2820
  );
2571
2821
  return {
2572
2822
  synced: true,
2573
- repositoryId,
2823
+ projectId,
2574
2824
  downloaded,
2575
2825
  deleted
2576
2826
  };
@@ -2592,7 +2842,7 @@ async function ensureWorkspaceInitialized(workdir, options) {
2592
2842
  const apmDir = workspaceApmDir(workdir);
2593
2843
  await copyTemplateFiles(apmDir, workdir);
2594
2844
  const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
2595
- await syncRepositoryProjectDocumentsPull(workdir, apmDir);
2845
+ await syncProjectDocumentsPull(workdir, apmDir);
2596
2846
  const trimmedName = options?.name?.trim();
2597
2847
  if (trimmedName) {
2598
2848
  const apmConfigPath = toFsPath(join7(apmDir, "apm.config.json"));
@@ -2626,8 +2876,9 @@ function markBranchDone(sessionId, workdir) {
2626
2876
  var TEST_START_ACTIONS = /* @__PURE__ */ new Set([
2627
2877
  "generate-cases",
2628
2878
  "run-auto-test",
2629
- "skip-test",
2630
- "enter-manual-test"
2879
+ // 跳过/直接进手工测试不发 Cursor,PR 延后到部署或验收
2880
+ "deploy",
2881
+ "accept"
2631
2882
  ]);
2632
2883
  function shouldEnsureWebIdePullRequests(action) {
2633
2884
  return TEST_START_ACTIONS.has(action);
@@ -2680,70 +2931,6 @@ async function ensureWebIdePullRequests(cfg, taskId, workdir) {
2680
2931
  }
2681
2932
  }
2682
2933
 
2683
- // src/commands/connect/webide-baseline-sync.ts
2684
- async function abortMergeOrRebase(cwd) {
2685
- try {
2686
- await execGit(cwd, ["merge", "--abort"], true);
2687
- } catch {
2688
- }
2689
- try {
2690
- await execGit(cwd, ["rebase", "--abort"], true);
2691
- } catch {
2692
- }
2693
- }
2694
- async function syncWebIdeBaselineOrFail(workdir) {
2695
- let repoRoots;
2696
- try {
2697
- const manifest = loadWorkspaceReposCache(workdir);
2698
- repoRoots = resolveWorkspaceRepoAbsolutePaths(manifest);
2699
- } catch (err) {
2700
- return {
2701
- ok: false,
2702
- error: `\u8BFB\u53D6\u5DE5\u4F5C\u533A\u4ED3\u5E93\u6E05\u5355\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
2703
- };
2704
- }
2705
- const failures = [];
2706
- for (const repoRoot of repoRoots) {
2707
- const label = formatRepoLabel(workdir, repoRoot);
2708
- try {
2709
- const gitRoot = await resolveGitRepoRoot(repoRoot);
2710
- const baselineBranch = await resolveDefaultRemoteBranch(gitRoot);
2711
- await ensureRemoteBaselineBranch(gitRoot, baselineBranch);
2712
- const currentBranch = await getCurrentBranch(gitRoot);
2713
- console.log(
2714
- `[apm] webide accept sync ${label}: ${currentBranch} \u2190 origin/${baselineBranch}`
2715
- );
2716
- try {
2717
- await execGit(
2718
- gitRoot,
2719
- ["merge", `origin/${baselineBranch}`, "--no-edit"],
2720
- true
2721
- );
2722
- } catch (mergeErr) {
2723
- await abortMergeOrRebase(gitRoot);
2724
- const detail = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
2725
- failures.push(
2726
- `${label}\uFF1A\u5408\u5E76 origin/${baselineBranch} \u51B2\u7A81\uFF0C\u5DF2\u6267\u884C merge --abort \u64A4\u56DE\u672C\u6B21\u540C\u6B65\u3002${detail}`
2727
- );
2728
- }
2729
- } catch (err) {
2730
- failures.push(
2731
- `${label}\uFF1A${err instanceof Error ? err.message : String(err)}`
2732
- );
2733
- }
2734
- }
2735
- if (failures.length > 0) {
2736
- return {
2737
- ok: false,
2738
- error: [
2739
- "\u62C9\u53D6\u6700\u65B0\u57FA\u7EBF\u5931\u8D25\uFF0C\u5DF2\u64A4\u56DE\u672C\u6B21\u540C\u6B65\u64CD\u4F5C\uFF0C\u8BF7\u4EBA\u5DE5\u5904\u7406\u51B2\u7A81\u540E\u91CD\u8BD5\u9A8C\u6536\u5408\u5E76\u3002",
2740
- ...failures.map((f) => `- ${f}`)
2741
- ].join("\n")
2742
- };
2743
- }
2744
- return { ok: true };
2745
- }
2746
-
2747
2934
  // src/version.ts
2748
2935
  import { readFileSync as readFileSync8 } from "fs";
2749
2936
  import { dirname as dirname5, join as join8 } from "path";
@@ -2955,7 +3142,8 @@ var WEBIDE_CODE_CHANGE_ACTIONS = /* @__PURE__ */ new Set([
2955
3142
  "skip-plan",
2956
3143
  "start-develop",
2957
3144
  "fix-and-retest",
2958
- "report-defect"
3145
+ "report-defect",
3146
+ "deploy"
2959
3147
  ]);
2960
3148
  function shouldCommitAfterWebIdeMessage(action) {
2961
3149
  return WEBIDE_CODE_CHANGE_ACTIONS.has(action);
@@ -2972,7 +3160,7 @@ async function appendContent(cfg, messageId, content) {
2972
3160
  const api = createApmApiClient(cfg);
2973
3161
  await api.cli.webideAppendMessageContent({ id: messageId, content });
2974
3162
  }
2975
- async function handleWebIdeInboundMessage(cfg, msg, signal) {
3163
+ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
2976
3164
  const workdir = requireRemoteWorkdir(msg.workdir);
2977
3165
  const messageId = msg.messageId;
2978
3166
  const taskId = msg.taskId;
@@ -3018,22 +3206,6 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
3018
3206
  if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3019
3207
  await ensureWebIdePullRequests(cfg, taskId, workdir);
3020
3208
  }
3021
- if (msg.action === "accept") {
3022
- if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3023
- const sync = await syncWebIdeBaselineOrFail(workdir);
3024
- if (!sync.ok) {
3025
- await setError(cfg, messageId, sync.error);
3026
- return;
3027
- }
3028
- try {
3029
- await pushWorkspaceRepos(workdir);
3030
- } catch (err) {
3031
- console.warn(
3032
- "[apm] webide accept \u540C\u6B65\u540E push \u5931\u8D25:",
3033
- err instanceof Error ? err.message : err
3034
- );
3035
- }
3036
- }
3037
3209
  const savedAgentId = loadWebIdeAgentId(workdir, taskId);
3038
3210
  const logSyncRef = { current: null };
3039
3211
  const outcome = await runCursorAgent(
@@ -3060,6 +3232,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
3060
3232
  // 本地 SDK sandbox 会拦截 custom-user-tools MCP,WebIDE 先关闭
3061
3233
  enableSandbox: false,
3062
3234
  taskId,
3235
+ terminalRpc: options?.terminalRpc,
3063
3236
  createRemoteLogSync: (agentId) => {
3064
3237
  saveWebIdeAgentId(workdir, taskId, agentId);
3065
3238
  logSyncRef.current = createThrottledWebIdeMessageLogSync(
@@ -3104,7 +3277,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
3104
3277
  id: messageId,
3105
3278
  content: fallback
3106
3279
  });
3107
- if (msg.action === "write-plan" && outcome.createPlan && outcome.createPlan.trim()) {
3280
+ if ((msg.action === "write-plan" || msg.action === "revise-plan") && outcome.createPlan && outcome.createPlan.trim()) {
3108
3281
  try {
3109
3282
  await api.cli.webideUpsertPlan({
3110
3283
  taskId,
@@ -3112,7 +3285,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
3112
3285
  });
3113
3286
  } catch (err) {
3114
3287
  console.warn(
3115
- "[apm] write-plan \u8865\u5199\u8BA1\u5212\u5931\u8D25:",
3288
+ `[apm] ${msg.action} \u8865\u5199\u8BA1\u5212\u5931\u8D25:`,
3116
3289
  err instanceof Error ? err.message : err
3117
3290
  );
3118
3291
  }
@@ -3154,19 +3327,68 @@ async function handleWebIdeInboundMessage(cfg, msg, signal) {
3154
3327
  }
3155
3328
  }
3156
3329
 
3330
+ // src/commands/connect/webide-terminal-rpc.ts
3331
+ import { parentPort } from "node:worker_threads";
3332
+ var nextId = 1;
3333
+ var pending = /* @__PURE__ */ new Map();
3334
+ function installTerminalRpcListener() {
3335
+ parentPort?.on("message", (raw) => {
3336
+ if (!raw || raw.type !== "terminal-rpc-result") return;
3337
+ const p = pending.get(raw.id);
3338
+ if (!p) return;
3339
+ pending.delete(raw.id);
3340
+ if (raw.ok) p.resolve(raw.result);
3341
+ else p.reject(new Error(raw.error || "terminal rpc failed"));
3342
+ });
3343
+ }
3344
+ function call(op, args) {
3345
+ const id = nextId++;
3346
+ return new Promise((resolve6, reject) => {
3347
+ if (!parentPort) {
3348
+ reject(new Error("parentPort \u4E0D\u53EF\u7528"));
3349
+ return;
3350
+ }
3351
+ pending.set(id, { resolve: resolve6, reject });
3352
+ parentPort.postMessage({
3353
+ type: "terminal-rpc",
3354
+ id,
3355
+ op,
3356
+ args
3357
+ });
3358
+ setTimeout(() => {
3359
+ if (pending.has(id)) {
3360
+ pending.delete(id);
3361
+ reject(new Error(`terminal rpc timeout op=${op}`));
3362
+ }
3363
+ }, 12e4);
3364
+ });
3365
+ }
3366
+ function createWorkerTerminalRpc() {
3367
+ return {
3368
+ start: (args) => call("start", args),
3369
+ stop: (args) => call("stop", args),
3370
+ stopAll: (args) => call("stopAll", args),
3371
+ list: (args) => call("list", args)
3372
+ };
3373
+ }
3374
+
3157
3375
  // src/commands/connect/webide-message-worker.ts
3158
3376
  var controllers = /* @__PURE__ */ new Map();
3377
+ installTerminalRpcListener();
3378
+ var terminalRpc = createWorkerTerminalRpc();
3159
3379
  async function runJob(cfg, msg) {
3160
3380
  const controller = new AbortController();
3161
3381
  controllers.set(msg.messageId, controller);
3162
3382
  try {
3163
- await handleWebIdeInboundMessage(cfg, msg, controller.signal);
3164
- parentPort?.postMessage({
3383
+ await handleWebIdeInboundMessage(cfg, msg, controller.signal, {
3384
+ terminalRpc
3385
+ });
3386
+ parentPort2?.postMessage({
3165
3387
  type: "done",
3166
3388
  messageId: msg.messageId
3167
3389
  });
3168
3390
  } catch (err) {
3169
- parentPort?.postMessage({
3391
+ parentPort2?.postMessage({
3170
3392
  type: "error",
3171
3393
  messageId: msg.messageId,
3172
3394
  error: err instanceof Error ? err.message : String(err)
@@ -3175,7 +3397,7 @@ async function runJob(cfg, msg) {
3175
3397
  controllers.delete(msg.messageId);
3176
3398
  }
3177
3399
  }
3178
- parentPort?.on("message", (raw) => {
3400
+ parentPort2?.on("message", (raw) => {
3179
3401
  if (raw.type === "cancel") {
3180
3402
  controllers.get(raw.messageId)?.abort();
3181
3403
  return;