ai-project-manage-cli 4.0.10 → 4.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -590,6 +590,40 @@ async function runUploadArtifact(requirementId, workspaceDir) {
590
590
  import { writeFileSync as writeFileSync3 } from "fs";
591
591
  import { join as join5 } from "path";
592
592
  import { stringify as yamlStringify } from "yaml";
593
+
594
+ // src/pull-reviews-xml.ts
595
+ function escapeForCdata(text) {
596
+ return text.replace(/\]\]>/g, "]]]]><![CDATA[>");
597
+ }
598
+ function buildReviewsXml(reviews) {
599
+ const lines = ["<reviews>"];
600
+ for (const review of reviews) {
601
+ lines.push(
602
+ ` <review id="${xmlEscape(review.id)}" model="${xmlEscape(
603
+ review.model ?? ""
604
+ )}" stance="${xmlEscape(review.stance ?? "")}" memberRole="${xmlEscape(
605
+ review.memberRole
606
+ )}">`
607
+ );
608
+ for (const item of review.items) {
609
+ const kind = item.kind.toLowerCase();
610
+ lines.push(
611
+ ` <item id="${xmlEscape(item.id)}" start="${item.startLine}" end="${item.endLine}" kind="${xmlEscape(kind)}" status="open">`
612
+ );
613
+ lines.push(
614
+ ` <body><![CDATA[${escapeForCdata(item.body ?? "")}]]></body>`
615
+ );
616
+ const reply = item.reply?.trim() ?? "";
617
+ lines.push(` <reply><![CDATA[${escapeForCdata(reply)}]]></reply>`);
618
+ lines.push(" </item>");
619
+ }
620
+ lines.push(" </review>");
621
+ }
622
+ lines.push("</reviews>", "");
623
+ return lines.join("\n");
624
+ }
625
+
626
+ // src/commands/pull.ts
593
627
  var PULL_ARTIFACT_FILE_NAMES = ["api.md", "backend.md"];
594
628
  function normalizeArtifactPath(fileName) {
595
629
  return fileName.trim().replace(/\\/g, "/").replace(/^\/+/, "");
@@ -656,7 +690,7 @@ function unknownArrayToXml(rootName, itemName, items) {
656
690
  lines.push(`</${rootName}>`, "");
657
691
  return lines.join("\n");
658
692
  }
659
- function escapeForCdata(text) {
693
+ function escapeForCdata2(text) {
660
694
  return text.replace(/\]\]>/g, "]]]]><![CDATA[>");
661
695
  }
662
696
  function defectsToXml(defects) {
@@ -668,12 +702,12 @@ function defectsToXml(defects) {
668
702
  lines.push(` <defect id="${xmlEscape(d.id)}">`);
669
703
  lines.push(` <status>${xmlEscape(d.status)}</status>`);
670
704
  lines.push(
671
- ` <current><![CDATA[${escapeForCdata(
705
+ ` <current><![CDATA[${escapeForCdata2(
672
706
  d.currentState ?? ""
673
707
  )}]]></current>`
674
708
  );
675
709
  lines.push(
676
- ` <expected><![CDATA[${escapeForCdata(
710
+ ` <expected><![CDATA[${escapeForCdata2(
677
711
  d.expectedEffect ?? ""
678
712
  )}]]></expected>`
679
713
  );
@@ -705,25 +739,7 @@ async function runPull(requirementId, workspaceDir) {
705
739
  "utf8"
706
740
  );
707
741
  writeFileSync3(join5(WORKITEMS_DIR, "prd.md"), req2.content || "", "utf8");
708
- const reviews = data.reviews ?? [];
709
- const reviewsXml = [
710
- "<reviews>",
711
- ...reviews.map((r) => {
712
- return [
713
- ` <review id="${xmlEscape(r.id)}">`,
714
- ` <model>${xmlEscape(r.model ?? "")}</model>`,
715
- ` <content>`,
716
- `${xmlEscape(r.content ?? "")}`,
717
- ` </content>`,
718
- ` <reply>`,
719
- `${xmlEscape(r.reply ?? "")}`,
720
- ` </reply>`,
721
- " </review>"
722
- ].join("\n");
723
- }),
724
- "</reviews>",
725
- ""
726
- ].join("\n");
742
+ const reviewsXml = buildReviewsXml(data.reviews ?? []);
727
743
  writeFileSync3(join5(WORKITEMS_DIR, "reviews.xml"), reviewsXml, "utf8");
728
744
  const defectsXml = defectsToXml(data.defects ?? []);
729
745
  writeFileSync3(join5(WORKITEMS_DIR, "defect.xml"), defectsXml, "utf8");
@@ -1190,6 +1206,38 @@ async function runUpdate() {
1190
1206
  }
1191
1207
  }
1192
1208
 
1209
+ // src/commands/update-skills.ts
1210
+ import { cpSync as cpSync2, existsSync as existsSync4, rmSync, statSync as statSync3 } from "fs";
1211
+ import { join as join10 } from "path";
1212
+ var TEMPLATE_SKILLS_DIR = join10(CLI_TEMPLATE_DIR, "skills");
1213
+ async function runUpdateSkills() {
1214
+ const apmDir = WORKSPACE_APM_DIR;
1215
+ if (!existsSync4(apmDir)) {
1216
+ console.error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1217
+ process.exit(1);
1218
+ }
1219
+ const apmStat = statSync3(apmDir);
1220
+ if (!apmStat.isDirectory()) {
1221
+ throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${apmDir}`);
1222
+ }
1223
+ let templateStat;
1224
+ try {
1225
+ templateStat = statSync3(TEMPLATE_SKILLS_DIR);
1226
+ } catch {
1227
+ throw new Error(`[apm] \u5185\u7F6E\u6280\u80FD\u6A21\u677F\u4E0D\u5B58\u5728: ${TEMPLATE_SKILLS_DIR}`);
1228
+ }
1229
+ if (!templateStat.isDirectory()) {
1230
+ throw new Error(`[apm] \u5185\u7F6E\u6280\u80FD\u6A21\u677F\u4E0D\u662F\u76EE\u5F55: ${TEMPLATE_SKILLS_DIR}`);
1231
+ }
1232
+ const skillsDir = join10(apmDir, "skills");
1233
+ if (existsSync4(skillsDir)) {
1234
+ rmSync(skillsDir, { recursive: true, force: true });
1235
+ }
1236
+ await ensureDirExists(apmDir);
1237
+ cpSync2(TEMPLATE_SKILLS_DIR, skillsDir, { recursive: true });
1238
+ console.log("[apm] \u5DF2\u66F4\u65B0 .apm/skills");
1239
+ }
1240
+
1193
1241
  // src/commands/update-dev-status.ts
1194
1242
  async function runUpdateDevStatus(requirementId, status) {
1195
1243
  const cfg = await ensureLoggedConfig();
@@ -1216,14 +1264,14 @@ async function runUpdateStatus(requirementId, status) {
1216
1264
  import path5 from "node:path";
1217
1265
 
1218
1266
  // src/commands/deploy/internal/apm-config.ts
1219
- import { existsSync as existsSync4, readFileSync as readFileSync7 } from "node:fs";
1267
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
1220
1268
  import { resolve as resolve4 } from "node:path";
1221
1269
  function loadApmConfig(options) {
1222
1270
  const p = resolve4(
1223
1271
  process.cwd(),
1224
1272
  options?.configPath ?? resolve4(WORKSPACE_APM_DIR, "apm.config.json")
1225
1273
  );
1226
- if (!existsSync4(p)) {
1274
+ if (!existsSync5(p)) {
1227
1275
  console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
1228
1276
  process.exit(1);
1229
1277
  }
@@ -1327,7 +1375,7 @@ import path4 from "node:path";
1327
1375
  import Docker from "dockerode";
1328
1376
 
1329
1377
  // src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
1330
- import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
1378
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
1331
1379
  import path from "node:path";
1332
1380
  function asOptionalTlsBuffer(value) {
1333
1381
  if (typeof value !== "string") {
@@ -1339,7 +1387,7 @@ function asOptionalTlsBuffer(value) {
1339
1387
  if (normalized === "") {
1340
1388
  return void 0;
1341
1389
  }
1342
- if (existsSync5(normalized)) {
1390
+ if (existsSync6(normalized)) {
1343
1391
  return readFileSync8(normalized);
1344
1392
  }
1345
1393
  const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
@@ -1550,7 +1598,7 @@ var DockerodeClient = class {
1550
1598
  var createDockerodeClient = (config) => new DockerodeClient(config);
1551
1599
 
1552
1600
  // src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
1553
- import { existsSync as existsSync6, readFileSync as readFileSync9, statSync as statSync3 } from "node:fs";
1601
+ import { existsSync as existsSync7, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
1554
1602
  import path2 from "node:path";
1555
1603
  function stripSurroundingQuotes(value) {
1556
1604
  const t = value.trim();
@@ -1567,7 +1615,7 @@ function loadEnvFromFile(envFilePath) {
1567
1615
  return {};
1568
1616
  }
1569
1617
  const targetPath = path2.resolve(envFilePath);
1570
- if (!existsSync6(targetPath) || !statSync3(targetPath).isFile()) {
1618
+ if (!existsSync7(targetPath) || !statSync4(targetPath).isFile()) {
1571
1619
  return {};
1572
1620
  }
1573
1621
  const raw = readFileSync9(targetPath, "utf-8");
@@ -1741,12 +1789,12 @@ function dockerPushImage(params, cwd) {
1741
1789
  }
1742
1790
 
1743
1791
  // src/commands/deploy/internal/backend-deploy/resolve-dockerfile.ts
1744
- import { existsSync as existsSync7 } from "node:fs";
1792
+ import { existsSync as existsSync8 } from "node:fs";
1745
1793
  import path3 from "node:path";
1746
1794
  function resolveDockerBuildPaths(cwd) {
1747
1795
  const dockerfilePath = path3.join(cwd, "Dockerfile");
1748
1796
  Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
1749
- if (!existsSync7(dockerfilePath)) {
1797
+ if (!existsSync8(dockerfilePath)) {
1750
1798
  throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
1751
1799
  }
1752
1800
  Logger.info("\u2713 Dockerfile \u5B58\u5728");
@@ -1875,11 +1923,11 @@ import { copyFile, readdir as readdir2, stat } from "node:fs/promises";
1875
1923
  import path7 from "node:path";
1876
1924
 
1877
1925
  // src/commands/deploy/internal/load-apm-dotenv.ts
1878
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "node:fs";
1879
- import { join as join10 } from "node:path";
1926
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
1927
+ import { join as join11 } from "node:path";
1880
1928
  function loadApmDotEnvIfPresent() {
1881
- const p = join10(WORKSPACE_APM_DIR, ".env");
1882
- if (!existsSync8(p)) {
1929
+ const p = join11(WORKSPACE_APM_DIR, ".env");
1930
+ if (!existsSync9(p)) {
1883
1931
  return;
1884
1932
  }
1885
1933
  let text;
@@ -1909,14 +1957,14 @@ function loadApmDotEnvIfPresent() {
1909
1957
  }
1910
1958
 
1911
1959
  // src/commands/deploy/internal/minio.ts
1912
- import { statSync as statSync4 } from "node:fs";
1960
+ import { statSync as statSync5 } from "node:fs";
1913
1961
  import { readdir } from "node:fs/promises";
1914
1962
  import path6 from "node:path";
1915
1963
  import * as Minio from "minio";
1916
1964
  var DEFAULT_MAX_FILE_SIZE_MB = 50;
1917
1965
  async function isDirectoryPath(dir) {
1918
1966
  try {
1919
- const st = statSync4(dir);
1967
+ const st = statSync5(dir);
1920
1968
  return st.isDirectory();
1921
1969
  } catch {
1922
1970
  return false;
@@ -1946,7 +1994,7 @@ async function collectFiles(root) {
1946
1994
  if (e.isDirectory()) {
1947
1995
  await walk(abs, rel);
1948
1996
  } else if (e.isFile()) {
1949
- const st = statSync4(abs);
1997
+ const st = statSync5(abs);
1950
1998
  out.push({
1951
1999
  absPath: abs,
1952
2000
  relativePath: rel.replace(/\\/g, "/"),
@@ -2264,6 +2312,11 @@ function buildProgram() {
2264
2312
  ).action(async () => {
2265
2313
  await runUpdate();
2266
2314
  });
2315
+ program.command("update-skills").description(
2316
+ "\u5220\u9664\u5DE5\u4F5C\u533A .apm/skills \u540E\uFF0C\u4ECE\u5F53\u524D CLI \u5185\u7F6E\u6A21\u677F\u91CD\u65B0\u590D\u5236\u6280\u80FD\u76EE\u5F55"
2317
+ ).action(async () => {
2318
+ await runUpdateSkills();
2319
+ });
2267
2320
  program.command("pull").description(
2268
2321
  "GET /api/cli/requirements/pull\uFF0C\u540C\u6B65\u6570\u636E\u4E0E\u9644\u4EF6\u5230 .apm/workitems/<\u9700\u6C42ID>"
2269
2322
  ).argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "4.0.10",
3
+ "version": "4.0.12",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -21,12 +21,35 @@ description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、
21
21
 
22
22
  正文骨架、需求点写法、示例与落盘自检见 **[apm-refine-template.md](./apm-refine-template.md)**。
23
23
 
24
+ ## `reviews.xml` 格式(v2)
25
+
26
+ `apm pull` 生成的 `reviews.xml` 为**行级条目**,仅含 **OPEN** 状态、且 **有 `reply` 的条目**才需要合并进正文(无 `reply` 或空 `reply` 的条目本次不处理)。
27
+
28
+ ```xml
29
+ <reviews>
30
+ <review id="..." model="Auto" stance="frontend" memberRole="FE">
31
+ <item id="..." start="44" end="55" kind="clarify" status="open">
32
+ <body><![CDATA[评审正文…]]></body>
33
+ <reply><![CDATA[产品已拍板口径…]]></reply>
34
+ </item>
35
+ </review>
36
+ </reviews>
37
+ ```
38
+
39
+ | 属性 / 节点 | 含义 |
40
+ | --------------- | --------------------------------------------------------------------------------------- |
41
+ | `start` / `end` | 锚定 `prd.md` 行号(1-based,闭区间),合并时优先据此定位段落 |
42
+ | `kind` | `clarify` / `difficulty` / `business` / `coordination`;**仅处理含非空 `reply` 的条目** |
43
+ | `body` | 评审意见(辅助理解,不作为正文来源) |
44
+ | `reply` | 产品回复,**并入正文**的权威补充 |
45
+
24
46
  ## 合并原则
25
47
 
26
- 1. **正文 = 需求原文 + 已拍板补充**:合并 `prd.md` `reviews.xml` `reply` 的明确口径;未说到的保持原文或不写,不自行扩需求。
27
- 2. **评审辅助理解 `reply`**;与补充冲突时以补充为准;未回应的评审本次不处理。
28
- 3. **同一议题只写一处**;合并后篇幅短于或接近原文。
29
- 4. **图片**:原文图片语法原样保留;理解图片后把规则写入对应需求点的文字描述。
48
+ 1. **正文 = 需求原文 + 已拍板补充**:将各 `<item>` 中非空 `reply` 并入 `prd.md` 对应行区间(或该行所在需求点);未回复的评审本次不处理。
49
+ 2. **定位**:优先按 `start`–`end` 行号找到段落;行号区间与章节标题不一致时,以**业务语义**归入最近的需求点,**不要**机械插入到错误章节。
50
+ 3. **冲突**:`reply` 与原文冲突时以 `reply` 为准;`coordination` 类条目通常无 `reply`,润色时忽略。
51
+ 4. **同一议题只写一处**;合并后篇幅短于或接近原文。
52
+ 5. **图片**:原文图片语法原样保留;理解图片后把规则写入对应需求点的文字描述。
30
53
 
31
54
  ## 执行步骤
32
55
 
@@ -35,22 +58,22 @@ description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、
35
58
  1. **Read** `.apm/workitems/<需求ID>/prd.md`
36
59
  2. **Read** `.apm/workitems/<需求ID>/reviews.xml`(不存在则视为无评审)
37
60
  3. **理解 PRD 中的图片**(有则执行):
38
- - 图片为标准 Markdown:`![描述](展示URL "相对路径")` — 圆括号内为展示 URL,**引号内**为相对路径(如 `attachments/图1.png`)
39
- - **优先读本地附件**:`.apm/workitems/<需求ID>/<相对路径>`;存在则用该文件理解图片内容
40
- - **本地不存在时**:再用圆括号内的展示 URL 下载图片后理解
41
- - 将理解到的界面/字段/交互规则写入对应需求点;回写时保留原 `![…](… "…")` 语法不变
61
+ - 图片为标准 Markdown:`![描述](展示URL "相对路径")`
62
+ - **优先读本地附件**:`.apm/workitems/<需求ID>/<相对路径>`
63
+ - **本地不存在时**:再用展示 URL 下载后理解
64
+ - 回写时保留原 `![…](… "…")` 语法不变
42
65
 
43
66
  `prd.md` 不可读 → 终止,步骤 5 注明原因。
44
67
 
45
68
  ### 步骤 2:润色
46
69
 
47
- **不向用户追问。** `reply` 视为补充信息。
70
+ **不向用户追问。** 非空 `reply` 视为已拍板补充。
48
71
 
49
- 1. `reply` 中已拍板内容并入正文对应需求点。
50
- 2. **Read** **[apm-refine-template.md](./apm-refine-template.md)**,按骨架、写法表与示例组织全文。
51
- 3. 写完后按模板 **「落盘前自检」** 核对,不满足则再改一版。
52
- 4. **待确认**:仅当补充信息原文含「待定」「再议」等时追加该章,每条一行归纳用户原意。
53
- 5. **局部替换**:只改涉及段落时,保留其余章节,重写为完整段落。
72
+ 1. 遍历 `reviews.xml` 中每个 `<item>`:若 `<reply>` 非空,将口径并入 `prd.md` 对应段落(参考 `start`/`end`)。
73
+ 2. **Read** **[apm-refine-template.md](./apm-refine-template.md)**,按骨架组织全文。
74
+ 3. 写完后按模板 **「落盘前自检」** 核对。
75
+ 4. **待确认**:仅当补充信息原文含「待定」「再议」等时追加该章。
76
+ 5. **局部替换**:只改涉及段落时,保留其余章节。
54
77
 
55
78
  ### 步骤 3:回写
56
79
 
@@ -60,6 +83,8 @@ description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、
60
83
 
61
84
  仓库根目录执行:`apm refine <需求ID>`
62
85
 
86
+ (平台会将旧正文追加到 `contentHistory`,并将全部 OPEN 评审标为已解决;侧栏不再展示这些条目。)
87
+
63
88
  ### 步骤 5:回复用户
64
89
 
65
90
  **仅**输出一张状态表,表格外无其他文字: