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 +90 -37
- package/package.json +1 -1
- package/template/skills/apm-refine/SKILL.md +39 -14
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
|
|
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[${
|
|
705
|
+
` <current><![CDATA[${escapeForCdata2(
|
|
672
706
|
d.currentState ?? ""
|
|
673
707
|
)}]]></current>`
|
|
674
708
|
);
|
|
675
709
|
lines.push(
|
|
676
|
-
` <expected><![CDATA[${
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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
|
|
1879
|
-
import { join as
|
|
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 =
|
|
1882
|
-
if (!
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
@@ -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. **正文 = 需求原文 +
|
|
27
|
-
2.
|
|
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:``
|
|
39
|
-
- **优先读本地附件**:`.apm/workitems/<需求ID
|
|
40
|
-
-
|
|
41
|
-
-
|
|
61
|
+
- 图片为标准 Markdown:``
|
|
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.
|
|
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
|
**仅**输出一张状态表,表格外无其他文字:
|