ai-project-manage-cli 6.0.56 → 6.0.58

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/README.md CHANGED
@@ -27,8 +27,12 @@ apm init [--name=我的项目]
27
27
  ```bash
28
28
  apm pull <sessionId>
29
29
  apm sync-document <sessionId> --file PRD
30
+ apm sync-project-documents
31
+ apm sync-project-documents --push
30
32
  apm update-skills
31
33
  apm branch <sessionId>
32
34
  ```
33
35
 
36
+ `apm pull` 会自动将平台登记的**仓库项目文档**同步到 `.apm/project/`(含 `manifest.json`)。Agent 修改 `.apm/project/` 下文件后,`apm connect` 处理完消息会自动推回平台。
37
+
34
38
  详见仓库根目录 [docs/CLI.md](../../docs/CLI.md)。
package/dist/index.js CHANGED
@@ -457,6 +457,30 @@ var requestConfig = {
457
457
  createPullRequest: defineEndpoint({
458
458
  method: "POST",
459
459
  path: "/cli/pull-requests"
460
+ }),
461
+ getRepositoryProjectDocumentManifest: defineEndpoint({
462
+ method: "GET",
463
+ path: "/cli/repository-project-documents/manifest"
464
+ }),
465
+ listRepositoryProjectDocuments: defineEndpoint({
466
+ method: "GET",
467
+ path: "/cli/repository-project-documents"
468
+ }),
469
+ upsertRepositoryProjectDocument: defineEndpoint({
470
+ method: "PUT",
471
+ path: "/cli/repository-project-documents/upsert"
472
+ }),
473
+ removeRepositoryProjectDocument: defineEndpoint({
474
+ method: "DELETE",
475
+ path: "/cli/repository-project-documents"
476
+ }),
477
+ updateCoordinatorDeploymentStatus: defineEndpoint({
478
+ method: "PUT",
479
+ path: "/cli/coordinator-deployments/status"
480
+ }),
481
+ completeCoordinatorDeployment: defineEndpoint({
482
+ method: "PUT",
483
+ path: "/cli/coordinator-deployments/complete"
460
484
  })
461
485
  }
462
486
  };
@@ -1086,8 +1110,8 @@ async function runCleanBranches(options = {}) {
1086
1110
  }
1087
1111
 
1088
1112
  // src/commands/pull.ts
1089
- import { writeFileSync as writeFileSync8 } from "fs";
1090
- import { join as join8 } from "path";
1113
+ import { writeFileSync as writeFileSync9 } from "fs";
1114
+ import { join as join9 } from "path";
1091
1115
  import { stringify as yamlStringify } from "yaml";
1092
1116
 
1093
1117
  // src/session-messages-xml.ts
@@ -1383,6 +1407,218 @@ async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1383
1407
  return { written, skipped, removed, repositoryId };
1384
1408
  }
1385
1409
 
1410
+ // src/repository-project-documents-sync.ts
1411
+ import {
1412
+ existsSync as existsSync6,
1413
+ readdirSync as readdirSync3,
1414
+ readFileSync as readFileSync6,
1415
+ rmSync as rmSync3,
1416
+ writeFileSync as writeFileSync8
1417
+ } from "fs";
1418
+ import { createHash } from "crypto";
1419
+ import { dirname as dirname2, join as join8, relative, sep } from "path";
1420
+ var MANIFEST_FILE3 = "manifest.json";
1421
+ function projectDocumentsDir(apmRoot) {
1422
+ return join8(apmRoot ?? workspaceApmDir(), "project");
1423
+ }
1424
+ function projectDocumentLocalPath(apmRoot, documentPath) {
1425
+ const normalized = normalizeLocalDocumentPath(documentPath);
1426
+ return join8(projectDocumentsDir(apmRoot), ...normalized.split("/"));
1427
+ }
1428
+ function normalizeLocalDocumentPath(path10) {
1429
+ const trimmed = path10.trim().replace(/\\/g, "/");
1430
+ if (!trimmed || trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) {
1431
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path10}`);
1432
+ }
1433
+ const segments = trimmed.split("/").filter(Boolean);
1434
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
1435
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path10}`);
1436
+ }
1437
+ return segments.join("/");
1438
+ }
1439
+ function hashLocalFileContent(content) {
1440
+ return createHash("sha256").update(content, "utf8").digest("hex");
1441
+ }
1442
+ function readLocalManifest(apmRoot) {
1443
+ const manifestPath2 = join8(projectDocumentsDir(apmRoot), MANIFEST_FILE3);
1444
+ if (!existsSync6(manifestPath2)) {
1445
+ return null;
1446
+ }
1447
+ try {
1448
+ return JSON.parse(
1449
+ readFileSync6(manifestPath2, "utf8")
1450
+ );
1451
+ } catch {
1452
+ return null;
1453
+ }
1454
+ }
1455
+ function listLocalDocumentPaths(apmRoot) {
1456
+ const root = projectDocumentsDir(apmRoot);
1457
+ if (!existsSync6(root)) {
1458
+ return [];
1459
+ }
1460
+ const paths = [];
1461
+ const walk = (dir) => {
1462
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1463
+ const abs = join8(dir, entry.name);
1464
+ if (entry.isDirectory()) {
1465
+ walk(abs);
1466
+ continue;
1467
+ }
1468
+ if (entry.isFile() && entry.name === MANIFEST_FILE3) {
1469
+ continue;
1470
+ }
1471
+ const rel = relative(root, abs).split(sep).join("/");
1472
+ paths.push(rel);
1473
+ }
1474
+ };
1475
+ walk(root);
1476
+ return paths.sort();
1477
+ }
1478
+ function diffManifestPaths(remote, local) {
1479
+ const remoteMap = new Map(
1480
+ (remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1481
+ );
1482
+ const localMap = new Map(
1483
+ (local?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1484
+ );
1485
+ const download = [];
1486
+ for (const [path10, hash] of remoteMap) {
1487
+ if (localMap.get(path10) !== hash) {
1488
+ download.push(path10);
1489
+ }
1490
+ }
1491
+ const deleteLocal = [];
1492
+ for (const path10 of localMap.keys()) {
1493
+ if (!remoteMap.has(path10)) {
1494
+ deleteLocal.push(path10);
1495
+ }
1496
+ }
1497
+ return { download, deleteLocal };
1498
+ }
1499
+ async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
1500
+ const empty = {
1501
+ synced: false,
1502
+ repositoryId: null,
1503
+ downloaded: 0,
1504
+ deleted: 0
1505
+ };
1506
+ const cfg = await tryReadApmConfig();
1507
+ if (!cfg || !resolveApiKey(cfg)) {
1508
+ console.log(
1509
+ "[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"
1510
+ );
1511
+ return empty;
1512
+ }
1513
+ const api = createApmApiClient(cfg);
1514
+ const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
1515
+ api,
1516
+ workdirPath
1517
+ );
1518
+ if (!repositoryId) {
1519
+ console.log(
1520
+ `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
1521
+ ${diagnostic ?? ""}`
1522
+ );
1523
+ return empty;
1524
+ }
1525
+ const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1526
+ const projectDir = projectDocumentsDir(targetApmDir);
1527
+ await ensureDirExists(projectDir);
1528
+ const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
1529
+ if (!remoteManifest) {
1530
+ console.log(
1531
+ `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
1532
+ );
1533
+ return { ...empty, repositoryId };
1534
+ }
1535
+ const localManifest = readLocalManifest(targetApmDir);
1536
+ const { download, deleteLocal } = diffManifestPaths(
1537
+ remoteManifest,
1538
+ localManifest
1539
+ );
1540
+ let downloaded = 0;
1541
+ if (download.length > 0) {
1542
+ const { list } = await api.cli.listRepositoryProjectDocuments({
1543
+ repositoryId,
1544
+ paths: download.join(",")
1545
+ });
1546
+ for (const doc of list) {
1547
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
1548
+ await ensureDirExists(dirname2(absPath));
1549
+ writeFileSync8(absPath, doc.content, "utf8");
1550
+ downloaded += 1;
1551
+ }
1552
+ }
1553
+ let deleted = 0;
1554
+ for (const path10 of deleteLocal) {
1555
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path10));
1556
+ if (existsSync6(absPath)) {
1557
+ rmSync3(absPath, { force: true });
1558
+ deleted += 1;
1559
+ }
1560
+ }
1561
+ writeFileSync8(
1562
+ toFsPath(join8(projectDir, MANIFEST_FILE3)),
1563
+ `${JSON.stringify(remoteManifest, null, 2)}
1564
+ `,
1565
+ "utf8"
1566
+ );
1567
+ console.log(
1568
+ `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1569
+ );
1570
+ return {
1571
+ synced: true,
1572
+ repositoryId,
1573
+ downloaded,
1574
+ deleted
1575
+ };
1576
+ }
1577
+ async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1578
+ const api = createApmApiClient(cfg);
1579
+ const { repositoryId } = await resolveRepositoryIdForSync(api, workdirPath);
1580
+ if (!repositoryId) {
1581
+ return 0;
1582
+ }
1583
+ const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
1584
+ const localPaths = listLocalDocumentPaths(targetApmDir);
1585
+ if (localPaths.length === 0) {
1586
+ console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1587
+ return 0;
1588
+ }
1589
+ const remoteManifest = (await api.cli.getRepositoryProjectDocumentManifest({ repositoryId })).manifest ?? null;
1590
+ const remoteHashByPath = new Map(
1591
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1592
+ );
1593
+ const remoteDescriptionByPath = new Map(
1594
+ (remoteManifest?.documents ?? []).map((doc) => [
1595
+ doc.path,
1596
+ doc.description
1597
+ ])
1598
+ );
1599
+ let synced = 0;
1600
+ for (const path10 of localPaths) {
1601
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path10));
1602
+ const content = readFileSync6(absPath, "utf8");
1603
+ const contentHash = hashLocalFileContent(content);
1604
+ if (remoteHashByPath.get(path10) === contentHash) {
1605
+ continue;
1606
+ }
1607
+ await api.cli.upsertRepositoryProjectDocument({
1608
+ repositoryId,
1609
+ path: path10,
1610
+ content,
1611
+ description: remoteDescriptionByPath.get(path10) ?? void 0
1612
+ });
1613
+ synced += 1;
1614
+ console.log(`[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: ${path10}`);
1615
+ }
1616
+ if (synced === 0) {
1617
+ console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1618
+ }
1619
+ return synced;
1620
+ }
1621
+
1386
1622
  // src/commands/pull.ts
1387
1623
  async function runPull(sessionId, remoteWorkdir) {
1388
1624
  const trimmedId = sessionId.trim();
@@ -1406,20 +1642,20 @@ async function runPull(sessionId, remoteWorkdir) {
1406
1642
  const dir = sessionDir(trimmedId, apmRoot);
1407
1643
  const docsDir = sessionDocsDir(trimmedId, apmRoot);
1408
1644
  await ensureDirExists(docsDir);
1409
- writeFileSync8(
1645
+ writeFileSync9(
1410
1646
  sessionRulePath(trimmedId, apmRoot),
1411
1647
  detail.description ?? "",
1412
1648
  "utf8"
1413
1649
  );
1414
- writeFileSync8(
1650
+ writeFileSync9(
1415
1651
  sessionTaskPath(trimmedId, apmRoot),
1416
1652
  detail.task.description ?? "",
1417
1653
  "utf8"
1418
1654
  );
1419
- writeFileSync8(sessionTodoPath(trimmedId, apmRoot), detail.todo ?? "", "utf8");
1655
+ writeFileSync9(sessionTodoPath(trimmedId, apmRoot), detail.todo ?? "", "utf8");
1420
1656
  for (const doc of documents) {
1421
1657
  const fileName = documentLocalFileName(doc.name);
1422
- writeFileSync8(join8(docsDir, fileName), doc.content ?? "", "utf8");
1658
+ writeFileSync9(join9(docsDir, fileName), doc.content ?? "", "utf8");
1423
1659
  }
1424
1660
  const sessionYaml = yamlStringify(
1425
1661
  {
@@ -1436,19 +1672,21 @@ async function runPull(sessionId, remoteWorkdir) {
1436
1672
  },
1437
1673
  { lineWidth: 0 }
1438
1674
  );
1439
- writeFileSync8(
1675
+ writeFileSync9(
1440
1676
  sessionYamlPath(trimmedId, apmRoot),
1441
1677
  sessionYaml.endsWith("\n") ? sessionYaml : `${sessionYaml}
1442
1678
  `,
1443
1679
  "utf8"
1444
1680
  );
1445
- writeFileSync8(
1681
+ writeFileSync9(
1446
1682
  sessionMessagesXmlPath(trimmedId, apmRoot),
1447
1683
  formatSessionMessagesXml(trimmedId, messages),
1448
1684
  "utf8"
1449
1685
  );
1450
1686
  await syncSessionAttachments(cfg, trimmedId, attachments, apmRoot);
1451
1687
  await syncPlatformRules(cfg, trimmedId, workdir, apmRoot);
1688
+ await syncRemoteDeploymentConfig(workdir, apmRoot);
1689
+ await syncRepositoryProjectDocumentsPull(workdir, apmRoot);
1452
1690
  console.log(`[apm] \u5DF2\u540C\u6B65\u4F1A\u8BDD\u5DE5\u4F5C\u533A: ${dir}`);
1453
1691
  return dir;
1454
1692
  }
@@ -1457,15 +1695,15 @@ async function runPull(sessionId, remoteWorkdir) {
1457
1695
  import { spawnSync } from "child_process";
1458
1696
 
1459
1697
  // src/version.ts
1460
- import { readFileSync as readFileSync6 } from "fs";
1461
- import { dirname as dirname2, join as join9 } from "path";
1698
+ import { readFileSync as readFileSync7 } from "fs";
1699
+ import { dirname as dirname3, join as join10 } from "path";
1462
1700
  import { fileURLToPath as fileURLToPath2 } from "url";
1463
1701
  var CLI_PACKAGE_NAME = "ai-project-manage-cli";
1464
1702
  function readCliVersion() {
1465
1703
  try {
1466
- const dir = dirname2(fileURLToPath2(import.meta.url));
1467
- const pkgPath = join9(dir, "..", "package.json");
1468
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1704
+ const dir = dirname3(fileURLToPath2(import.meta.url));
1705
+ const pkgPath = join10(dir, "..", "package.json");
1706
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1469
1707
  return pkg.version ?? "0.0.0";
1470
1708
  } catch {
1471
1709
  return "0.0.0";
@@ -1538,12 +1776,12 @@ async function runUpdate() {
1538
1776
  }
1539
1777
 
1540
1778
  // src/commands/update-skills.ts
1541
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
1542
- import { join as join10 } from "path";
1779
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
1780
+ import { join as join11 } from "path";
1543
1781
  async function syncWorkspaceSkills(cfg, workdir) {
1544
1782
  const apmDir = workspaceApmDir(workdir);
1545
1783
  const fsApmDir = toFsPath(apmDir);
1546
- if (!existsSync6(fsApmDir)) {
1784
+ if (!existsSync7(fsApmDir)) {
1547
1785
  throw new Error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1548
1786
  }
1549
1787
  const apmStat = statSync3(fsApmDir);
@@ -1555,12 +1793,12 @@ async function syncWorkspaceSkills(cfg, workdir) {
1555
1793
  if (syncAgentsGuide(apmDir)) {
1556
1794
  console.log("[apm] \u5DF2\u540C\u6B65 APM \u6307\u5357: .apm/AGENTS.md");
1557
1795
  }
1558
- const rulesDir = join10(apmDir, "rules");
1796
+ const rulesDir = join11(apmDir, "rules");
1559
1797
  const ruleNames = syncBaseRules(rulesDir);
1560
1798
  for (const name of ruleNames) {
1561
1799
  console.log(`[apm] \u5DF2\u540C\u6B65\u57FA\u7840\u89C4\u5219: rules/${name}`);
1562
1800
  }
1563
- const skillsDir = join10(apmDir, "skills");
1801
+ const skillsDir = join11(apmDir, "skills");
1564
1802
  mkdirSync4(toFsPath(skillsDir), { recursive: true });
1565
1803
  const baseNames = syncBaseSkills(skillsDir);
1566
1804
  for (const name of baseNames) {
@@ -1587,7 +1825,7 @@ async function syncWorkspaceSkills(cfg, workdir) {
1587
1825
  }
1588
1826
  async function runUpdateSkills() {
1589
1827
  const apmDir = workspaceApmDir();
1590
- if (!existsSync6(apmDir)) {
1828
+ if (!existsSync7(apmDir)) {
1591
1829
  console.error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1592
1830
  process.exit(1);
1593
1831
  }
@@ -1600,11 +1838,11 @@ async function runUpdateSkills() {
1600
1838
  }
1601
1839
 
1602
1840
  // src/commands/sync-deploy-config.ts
1603
- import { existsSync as existsSync7, statSync as statSync4 } from "fs";
1841
+ import { existsSync as existsSync8, statSync as statSync4 } from "fs";
1604
1842
  async function runSyncDeployConfig() {
1605
1843
  const workdir = resolveWorkdirPath();
1606
1844
  const apmDir = workspaceApmDir(workdir);
1607
- if (!existsSync7(apmDir)) {
1845
+ if (!existsSync8(apmDir)) {
1608
1846
  console.error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1609
1847
  process.exit(1);
1610
1848
  }
@@ -1619,18 +1857,33 @@ async function runSyncDeployConfig() {
1619
1857
  }
1620
1858
  }
1621
1859
 
1860
+ // src/commands/sync-project-documents.ts
1861
+ async function runSyncProjectDocuments(options) {
1862
+ const pull = options?.pull ?? !options?.push;
1863
+ const push = options?.push ?? false;
1864
+ const workdir = resolveWorkdirPath();
1865
+ const apmRoot = workspaceApmDir(workdir);
1866
+ if (pull) {
1867
+ await syncRepositoryProjectDocumentsPull(workdir, apmRoot);
1868
+ }
1869
+ if (push) {
1870
+ const cfg = await ensureLoggedConfig();
1871
+ await syncRepositoryProjectDocumentsPush(cfg, workdir, apmRoot);
1872
+ }
1873
+ }
1874
+
1622
1875
  // src/commands/sync-document.ts
1623
- import { existsSync as existsSync9 } from "fs";
1876
+ import { existsSync as existsSync10 } from "fs";
1624
1877
  import { basename as basename3 } from "path";
1625
1878
 
1626
1879
  // src/commands/sync-session-documents.ts
1627
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
1628
- import { join as join11 } from "path";
1880
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
1881
+ import { join as join12 } from "path";
1629
1882
  function listLocalMarkdownFiles(docsDir) {
1630
- if (!existsSync8(docsDir)) {
1883
+ if (!existsSync9(docsDir)) {
1631
1884
  return [];
1632
1885
  }
1633
- return readdirSync3(docsDir).filter(
1886
+ return readdirSync4(docsDir).filter(
1634
1887
  (name) => name.toLowerCase().endsWith(".md")
1635
1888
  );
1636
1889
  }
@@ -1642,8 +1895,8 @@ function remoteDocumentByLocalName(remoteDocuments, localFileName) {
1642
1895
  });
1643
1896
  }
1644
1897
  async function upsertLocalDocumentFile(api, sessionId, docsDir, fileName) {
1645
- const absPath = join11(docsDir, fileName);
1646
- const content = readFileSync7(absPath, "utf8");
1898
+ const absPath = join12(docsDir, fileName);
1899
+ const content = readFileSync8(absPath, "utf8");
1647
1900
  const name = documentPlatformName(absPath);
1648
1901
  return api.cli.upsertDocument({
1649
1902
  sessionId,
@@ -1665,8 +1918,8 @@ async function syncSessionDocuments(cfg, sessionId, apmRoot, options) {
1665
1918
  const remoteDocuments = options?.remoteDocuments ?? await api.cli.listDocuments({ sessionId: trimmedSessionId });
1666
1919
  let synced = 0;
1667
1920
  for (const fileName of localFiles) {
1668
- const absPath = join11(docsDir, fileName);
1669
- const content = readFileSync7(absPath, "utf8");
1921
+ const absPath = join12(docsDir, fileName);
1922
+ const content = readFileSync8(absPath, "utf8");
1670
1923
  const remote = remoteDocumentByLocalName(remoteDocuments, fileName);
1671
1924
  if (remote && remote.content === content) {
1672
1925
  continue;
@@ -1699,7 +1952,7 @@ async function runSyncDocument(sessionId, options) {
1699
1952
  process.exit(1);
1700
1953
  }
1701
1954
  const absPath = resolveSessionDocumentPath(trimmedSessionId, fileArg);
1702
- if (!existsSync9(absPath)) {
1955
+ if (!existsSync10(absPath)) {
1703
1956
  const docsDir2 = sessionDocsDir(trimmedSessionId);
1704
1957
  console.error(
1705
1958
  `[apm] \u6587\u6863\u4E0D\u5B58\u5728: ${absPath}
@@ -1853,13 +2106,37 @@ function validateCancel(o) {
1853
2106
  data: { type: "cancel", messageId: o.messageId.trim() }
1854
2107
  };
1855
2108
  }
2109
+ function validateDeployPush(o) {
2110
+ if (o.type !== "deploy") {
2111
+ return { ok: false, reason: "\u671F\u671B deploy" };
2112
+ }
2113
+ if (!nonEmptyString(o.deploymentRunId)) {
2114
+ return { ok: false, reason: "deploy \u7F3A\u5C11 deploymentRunId" };
2115
+ }
2116
+ if (!nonEmptyString(o.workdir)) {
2117
+ return { ok: false, reason: "deploy \u7F3A\u5C11 workdir" };
2118
+ }
2119
+ const environment = o.environment;
2120
+ if (environment !== "test" && environment !== "online") {
2121
+ return { ok: false, reason: "deploy.environment \u65E0\u6548" };
2122
+ }
2123
+ return {
2124
+ ok: true,
2125
+ data: {
2126
+ type: "deploy",
2127
+ deploymentRunId: o.deploymentRunId.trim(),
2128
+ workdir: o.workdir.trim(),
2129
+ environment
2130
+ }
2131
+ };
2132
+ }
1856
2133
  function validateAgentWsMessage(value, kind) {
1857
2134
  if (typeof value !== "object" || value === null) {
1858
2135
  return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
1859
2136
  }
1860
2137
  const o = value;
1861
2138
  const type = o.type;
1862
- if (type !== "heartbeat" && type !== "message" && type !== "cancel") {
2139
+ if (type !== "heartbeat" && type !== "message" && type !== "cancel" && type !== "deploy") {
1863
2140
  return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
1864
2141
  }
1865
2142
  if (kind === "heartbeat" || type === "heartbeat") {
@@ -1868,9 +2145,125 @@ function validateAgentWsMessage(value, kind) {
1868
2145
  if (type === "cancel") {
1869
2146
  return validateCancel(o);
1870
2147
  }
2148
+ if (type === "deploy") {
2149
+ return validateDeployPush(o);
2150
+ }
1871
2151
  return validateMessagePush(o);
1872
2152
  }
1873
2153
 
2154
+ // src/commands/connect/deploy-run.ts
2155
+ import { spawn } from "node:child_process";
2156
+ import { readFileSync as readFileSync9 } from "node:fs";
2157
+ import { join as join13 } from "node:path";
2158
+ function readDeployConfig(workdir) {
2159
+ const configPath = join13(workspaceApmDir(workdir), "apm.config.json");
2160
+ try {
2161
+ const raw = readFileSync9(configPath, "utf8");
2162
+ const parsed = JSON.parse(raw);
2163
+ return parsed.deploy;
2164
+ } catch {
2165
+ return void 0;
2166
+ }
2167
+ }
2168
+ function resolveDeployCommand(workdir, environment) {
2169
+ const command = readDeployConfig(workdir)?.[environment];
2170
+ if (typeof command === "string" && command.trim()) {
2171
+ return command.trim();
2172
+ }
2173
+ return null;
2174
+ }
2175
+ function missingDeployCommandMessage(environment) {
2176
+ return `deploy.${environment} \u90E8\u7F72\u547D\u4EE4\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u914D\u7F6E`;
2177
+ }
2178
+ function runShellCommand(command, cwd, signal) {
2179
+ return new Promise((resolve5, reject) => {
2180
+ const child = spawn(command, {
2181
+ cwd,
2182
+ shell: true,
2183
+ env: process.env,
2184
+ windowsHide: true
2185
+ });
2186
+ let stdout = "";
2187
+ let stderr = "";
2188
+ const onAbort = () => {
2189
+ child.kill("SIGTERM");
2190
+ };
2191
+ if (signal.aborted) {
2192
+ onAbort();
2193
+ } else {
2194
+ signal.addEventListener("abort", onAbort, { once: true });
2195
+ }
2196
+ child.stdout.on("data", (chunk) => {
2197
+ stdout += String(chunk);
2198
+ });
2199
+ child.stderr.on("data", (chunk) => {
2200
+ stderr += String(chunk);
2201
+ });
2202
+ child.on("error", (error) => {
2203
+ signal.removeEventListener("abort", onAbort);
2204
+ reject(error);
2205
+ });
2206
+ child.on("close", (code) => {
2207
+ signal.removeEventListener("abort", onAbort);
2208
+ const log = [stdout, stderr].filter(Boolean).join("\n");
2209
+ if (code === 0) {
2210
+ resolve5({ log });
2211
+ return;
2212
+ }
2213
+ const error = new Error(
2214
+ `\u90E8\u7F72\u547D\u4EE4\u9000\u51FA\u7801 ${code ?? "unknown"}: ${command}`
2215
+ );
2216
+ error.log = log;
2217
+ reject(error);
2218
+ });
2219
+ });
2220
+ }
2221
+ async function handleInboundDeploy(cfg, msg, signal) {
2222
+ const api = createApmApiClient(cfg);
2223
+ const deploymentRunId = msg.deploymentRunId;
2224
+ if (signal.aborted) return;
2225
+ await api.cli.updateCoordinatorDeploymentStatus({
2226
+ id: deploymentRunId,
2227
+ status: "DEPLOYING"
2228
+ });
2229
+ const workdir = requireRemoteWorkdir(msg.workdir);
2230
+ const command = resolveDeployCommand(workdir, msg.environment);
2231
+ if (!command) {
2232
+ const error = missingDeployCommandMessage(msg.environment);
2233
+ console.error(`[apm] ${error}`);
2234
+ await api.cli.completeCoordinatorDeployment({
2235
+ id: deploymentRunId,
2236
+ status: "FAILED",
2237
+ log: error,
2238
+ error
2239
+ });
2240
+ return;
2241
+ }
2242
+ console.log(
2243
+ `[apm] deploy start id=${deploymentRunId} env=${msg.environment} cwd=${workdir}`
2244
+ );
2245
+ console.log(`[apm] deploy command: ${command}`);
2246
+ try {
2247
+ const { log } = await runShellCommand(command, workdir, signal);
2248
+ await api.cli.completeCoordinatorDeployment({
2249
+ id: deploymentRunId,
2250
+ status: "SUCCESS",
2251
+ log
2252
+ });
2253
+ console.log(`[apm] deploy success id=${deploymentRunId}`);
2254
+ } catch (error) {
2255
+ const detail = error instanceof Error ? error.message : String(error);
2256
+ const log = error && typeof error === "object" && "log" in error ? String(error.log ?? "") : "";
2257
+ await api.cli.completeCoordinatorDeployment({
2258
+ id: deploymentRunId,
2259
+ status: "FAILED",
2260
+ log,
2261
+ error: detail
2262
+ });
2263
+ console.error(`[apm] deploy failed id=${deploymentRunId}: ${detail}`);
2264
+ }
2265
+ }
2266
+
1874
2267
  // src/commands/connect/abort-signal-debug.ts
1875
2268
  import {
1876
2269
  getEventListeners,
@@ -2100,17 +2493,17 @@ ${JSON.stringify(event, null, 2)}
2100
2493
  }
2101
2494
 
2102
2495
  // src/commands/connect/agent-session-registry.ts
2103
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "node:fs";
2104
- import { dirname as dirname3, resolve as resolve3 } from "node:path";
2496
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
2497
+ import { dirname as dirname4, resolve as resolve3 } from "node:path";
2105
2498
  function registryPath(workdir, sessionId) {
2106
2499
  return resolve3(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
2107
2500
  }
2108
2501
  function readRegistry(path10) {
2109
- if (!existsSync10(path10)) {
2502
+ if (!existsSync11(path10)) {
2110
2503
  return {};
2111
2504
  }
2112
2505
  try {
2113
- const parsed = JSON.parse(readFileSync8(path10, "utf8"));
2506
+ const parsed = JSON.parse(readFileSync10(path10, "utf8"));
2114
2507
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2115
2508
  const result = {};
2116
2509
  for (const [key, value] of Object.entries(
@@ -2127,8 +2520,8 @@ function readRegistry(path10) {
2127
2520
  return {};
2128
2521
  }
2129
2522
  function writeRegistry(path10, registry) {
2130
- mkdirSync5(dirname3(path10), { recursive: true });
2131
- writeFileSync9(path10, `${JSON.stringify(registry, null, 2)}
2523
+ mkdirSync5(dirname4(path10), { recursive: true });
2524
+ writeFileSync10(path10, `${JSON.stringify(registry, null, 2)}
2132
2525
  `, "utf8");
2133
2526
  }
2134
2527
  function loadSessionAgentId(workdir, sessionId, user) {
@@ -2432,20 +2825,20 @@ async function runCursorAgent(cfg, ctx, options) {
2432
2825
  }
2433
2826
 
2434
2827
  // src/commands/connect/cli-version-sync.ts
2435
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
2436
- import { join as join12 } from "path";
2828
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
2829
+ import { join as join14 } from "path";
2437
2830
  var CLI_VERSION_FILE = ".cli-version.json";
2438
2831
  function manifestPath(apmDir) {
2439
- return join12(apmDir, CLI_VERSION_FILE);
2832
+ return join14(apmDir, CLI_VERSION_FILE);
2440
2833
  }
2441
2834
  function loadManifest3(apmDir) {
2442
2835
  const path10 = toFsPath(manifestPath(apmDir));
2443
- if (!existsSync11(path10)) {
2836
+ if (!existsSync12(path10)) {
2444
2837
  return null;
2445
2838
  }
2446
2839
  try {
2447
2840
  const parsed = JSON.parse(
2448
- readFileSync9(path10, "utf8")
2841
+ readFileSync11(path10, "utf8")
2449
2842
  );
2450
2843
  if (parsed?.version === 1 && typeof parsed.cliVersion === "string" && parsed.cliVersion.trim()) {
2451
2844
  return parsed;
@@ -2456,7 +2849,7 @@ function loadManifest3(apmDir) {
2456
2849
  }
2457
2850
  function saveManifest3(apmDir, cliVersion) {
2458
2851
  const manifest = { version: 1, cliVersion };
2459
- writeFileSync10(
2852
+ writeFileSync11(
2460
2853
  toFsPath(manifestPath(apmDir)),
2461
2854
  `${JSON.stringify(manifest, null, 2)}
2462
2855
  `,
@@ -2633,6 +3026,10 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
2633
3026
  "sync-documents",
2634
3027
  () => syncSessionDocuments(cfg, msg.sessionId, apmRoot)
2635
3028
  );
3029
+ await runStep(
3030
+ "sync-project-documents",
3031
+ () => syncRepositoryProjectDocumentsPush(cfg, workdir, apmRoot)
3032
+ );
2636
3033
  await runStep(
2637
3034
  "commit-files",
2638
3035
  () => commitWorkingTreeIfDirty(workdir, "chore(apm): commit working tree")
@@ -2770,6 +3167,27 @@ async function runConnect(options) {
2770
3167
  activeRuns.get(messageId)?.abort();
2771
3168
  return;
2772
3169
  }
3170
+ if (validated.data.type === "deploy") {
3171
+ const msg2 = validated.data;
3172
+ const perDeployController = new AbortController();
3173
+ const signal2 = AbortSignal.any([
3174
+ shutdownAbort.signal,
3175
+ perDeployController.signal
3176
+ ]);
3177
+ const task2 = (async () => {
3178
+ await runSlots.acquire();
3179
+ try {
3180
+ await handleInboundDeploy(cfg, msg2, signal2);
3181
+ } finally {
3182
+ runSlots.release();
3183
+ }
3184
+ })();
3185
+ activeTasks.add(task2);
3186
+ void task2.finally(() => {
3187
+ activeTasks.delete(task2);
3188
+ });
3189
+ return;
3190
+ }
2773
3191
  if (validated.data.type !== "message") {
2774
3192
  return;
2775
3193
  }
@@ -2852,19 +3270,19 @@ async function runCreatePr(options) {
2852
3270
  import path5 from "node:path";
2853
3271
 
2854
3272
  // src/commands/deploy/internal/apm-config.ts
2855
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
3273
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "node:fs";
2856
3274
  import { resolve as resolve4 } from "node:path";
2857
3275
  function loadApmConfig(options) {
2858
3276
  const p = resolve4(
2859
3277
  process.cwd(),
2860
3278
  options?.configPath ?? resolve4(workspaceApmDir(), "apm.config.json")
2861
3279
  );
2862
- if (!existsSync12(p)) {
3280
+ if (!existsSync13(p)) {
2863
3281
  console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
2864
3282
  process.exit(1);
2865
3283
  }
2866
3284
  try {
2867
- const raw = readFileSync10(p, "utf8");
3285
+ const raw = readFileSync12(p, "utf8");
2868
3286
  return JSON.parse(raw);
2869
3287
  } catch (e) {
2870
3288
  console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
@@ -2986,7 +3404,7 @@ import path4 from "node:path";
2986
3404
  import Docker from "dockerode";
2987
3405
 
2988
3406
  // src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
2989
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
3407
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
2990
3408
  import path from "node:path";
2991
3409
  function asOptionalTlsBuffer(value) {
2992
3410
  if (typeof value !== "string") {
@@ -2998,8 +3416,8 @@ function asOptionalTlsBuffer(value) {
2998
3416
  if (normalized === "") {
2999
3417
  return void 0;
3000
3418
  }
3001
- if (existsSync13(normalized)) {
3002
- return readFileSync11(normalized);
3419
+ if (existsSync14(normalized)) {
3420
+ return readFileSync13(normalized);
3003
3421
  }
3004
3422
  const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
3005
3423
  if (looksLikePath) {
@@ -3209,7 +3627,7 @@ var DockerodeClient = class {
3209
3627
  var createDockerodeClient = (config) => new DockerodeClient(config);
3210
3628
 
3211
3629
  // src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
3212
- import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync5 } from "node:fs";
3630
+ import { existsSync as existsSync15, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
3213
3631
  import path2 from "node:path";
3214
3632
  function stripSurroundingQuotes(value) {
3215
3633
  const t = value.trim();
@@ -3226,10 +3644,10 @@ function loadEnvFromFile(envFilePath) {
3226
3644
  return {};
3227
3645
  }
3228
3646
  const targetPath = path2.resolve(envFilePath);
3229
- if (!existsSync14(targetPath) || !statSync5(targetPath).isFile()) {
3647
+ if (!existsSync15(targetPath) || !statSync5(targetPath).isFile()) {
3230
3648
  return {};
3231
3649
  }
3232
- const raw = readFileSync12(targetPath, "utf-8");
3650
+ const raw = readFileSync14(targetPath, "utf-8");
3233
3651
  const result = {};
3234
3652
  for (const line of raw.split(/\r?\n/)) {
3235
3653
  const normalized = line.trim();
@@ -3400,12 +3818,12 @@ function dockerPushImage(params, cwd) {
3400
3818
  }
3401
3819
 
3402
3820
  // src/commands/deploy/internal/backend-deploy/resolve-dockerfile.ts
3403
- import { existsSync as existsSync15 } from "node:fs";
3821
+ import { existsSync as existsSync16 } from "node:fs";
3404
3822
  import path3 from "node:path";
3405
3823
  function resolveDockerBuildPaths(cwd) {
3406
3824
  const dockerfilePath = path3.join(cwd, "Dockerfile");
3407
3825
  Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
3408
- if (!existsSync15(dockerfilePath)) {
3826
+ if (!existsSync16(dockerfilePath)) {
3409
3827
  throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
3410
3828
  }
3411
3829
  Logger.info("\u2713 Dockerfile \u5B58\u5728");
@@ -4034,8 +4452,13 @@ function buildProgram() {
4034
4452
  ).action(async () => {
4035
4453
  await runSyncDeployConfig();
4036
4454
  });
4455
+ program.command("sync-project-documents").description(
4456
+ "\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/project/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
4457
+ ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/project/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/project/").action(async (opts) => {
4458
+ await runSyncProjectDocuments(opts);
4459
+ });
4037
4460
  program.command("pull").description(
4038
- "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF08session.yaml\u3001RULE.md\u3001TASK.md\u3001TODO.md\u3001docs\u3001attachments\uFF09"
4461
+ "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF0C\u5E76\u540C\u6B65\u90E8\u7F72\u914D\u7F6E\u4E0E\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u5230 .apm/project/"
4039
4462
  ).argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").action(async (sessionId) => {
4040
4463
  await runPull(sessionId);
4041
4464
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.56",
3
+ "version": "6.0.58",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -30,6 +30,11 @@
30
30
  - reply.md:当需要回复消息时需要读取这个文档,记住回复规则
31
31
  - write_doc.md:当需要写文档时需要读取这个文档,记住写文档的规则
32
32
 
33
+ - 仓库项目上下文(`.apm/project/`):
34
+ - 索引: `.apm/project/manifest.json` — 本仓库在平台登记的文档列表
35
+ - 文档文件: `.apm/project/{path}` — 与 manifest 中 path 对应;`apm pull` 自动同步,`apm connect` 消息结束后自动推回平台
36
+ - 任务涉及菜单名、路由、业务术语等时,**先 Read manifest 与相关文档**,禁止猜测路径
37
+
33
38
  - 本轮任务需要关注的文档指引(.apm/sessions/<会话 ID>/\*):
34
39
  - 本轮会话状态: `session.yaml`,从这里可以看到每个成员的信息,找到可以协助你一起解决问题的人,可以结合 `RULE.md`一起看
35
40
  - 群文档: `docs/xxxx.md`,当需要相关上下文可以在这里查找,按需阅读
@@ -1,3 +1,7 @@
1
1
  {
2
- "name": ""
3
- }
2
+ "name": "",
3
+ "deploy": {
4
+ "test": "npm run deploy:test && python .apm/deploy/deploy.py",
5
+ "online": "npm run deploy:online && python .apm/deploy/deploy.py"
6
+ }
7
+ }
@@ -25,6 +25,7 @@
25
25
 
26
26
  - 须标注**目标数据库**(库名/实例名、类型如 MySQL;同一变更涉及多库时分别标注)
27
27
  - 待执行的 SQL 语句须放在 ` ```sql ` 代码块中,按执行顺序逐条列出
28
+ - SQL 须写完整、可直接复制执行,**禁止**用 `...` 或「省略」占位代替表名、字段名、条件等任何片段
28
29
 
29
30
  ## 文档同步
30
31
 
@@ -52,9 +52,9 @@
52
52
 
53
53
  - **目标数据库**(库名/实例名、类型如 MySQL;同一变更涉及多库时分别标注)
54
54
  - 变更摘要(改了什么表/数据、为什么)
55
- - 完整 SQL 语句(按执行顺序排列;**每条须放在 ` ```sql ` 代码块中**,便于复制执行)
55
+ - 完整 SQL 语句(按执行顺序排列;**每条须放在 ` ```sql ` 代码块中**,便于复制执行;**禁止**用 `...` 或「省略」占位代替任何片段)
56
56
  - 执行环境说明(测试/生产是否一致、是否需人工执行)
57
- - 回滚方案(如适用;回滚 SQL 同样用 ` ```sql ` 代码块)
57
+ - 回滚方案(如适用;回滚 SQL 同样用 ` ```sql ` 代码块,须完整可执行,禁止省略)
58
58
 
59
59
  示例:
60
60
 
@@ -18,12 +18,12 @@
18
18
 
19
19
  ### 步骤 2:四项检查
20
20
 
21
- | 检查项 | 判定 |
22
- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
23
- | **白名单对账** | diff 中出现白名单之外的文件,且计划未更新说明 → **不通过** |
24
- | **需求相关性** | 存在与本需求无关的改动(顺手重构、改格式、动了无关逻辑)→ **不通过** |
25
- | **计划落实** | 计划「实现步骤」中的关键点在 diff 中找不到对应实现 → **不通过** |
26
- | **SQL 文档** | **仅后端**:diff 涉及 SQL 改动(DDL/DML、表结构、Mapper/XML 中 SQL 等),但 `docs/SQL.md` 缺失、未标注目标数据库、或与改动不一致 → **不通过** |
21
+ | 检查项 | 判定 |
22
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | **白名单对账** | diff 中出现白名单之外的文件,且计划未更新说明 → **不通过** |
24
+ | **需求相关性** | 存在与本需求无关的改动(顺手重构、改格式、动了无关逻辑)→ **不通过** |
25
+ | **计划落实** | 计划「实现步骤」中的关键点在 diff 中找不到对应实现 → **不通过** |
26
+ | **SQL 文档** | **仅后端**:diff 涉及 SQL 改动(DDL/DML、表结构、Mapper/XML 中 SQL 等),但 `docs/SQL.md` 缺失、未标注目标数据库、与改动不一致、或 SQL 代码块含 `...`/「省略」占位 → **不通过** |
27
27
 
28
28
  注意事项:
29
29