ai-project-manage-cli 6.0.59 → 6.0.60

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.
Files changed (2) hide show
  1. package/dist/index.js +280 -273
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -80,8 +80,8 @@ function buildAgentWsUrl(httpBase, apiKey) {
80
80
  }
81
81
 
82
82
  // src/commands/init.ts
83
- import { join as join4 } from "path";
84
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
83
+ import { join as join5 } from "path";
84
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
85
85
 
86
86
  // src/command-utils.ts
87
87
  import {
@@ -589,6 +589,218 @@ ${diagnostic ?? ""}
589
589
  return { synced: true, repositoryId, configName: config.name };
590
590
  }
591
591
 
592
+ // src/repository-project-documents-sync.ts
593
+ import {
594
+ existsSync as existsSync2,
595
+ readdirSync as readdirSync2,
596
+ readFileSync as readFileSync3,
597
+ rmSync,
598
+ writeFileSync as writeFileSync4
599
+ } from "fs";
600
+ import { createHash } from "crypto";
601
+ import { dirname as dirname2, join as join4, relative, sep } from "path";
602
+ var MANIFEST_FILE = "manifest.json";
603
+ function projectDocumentsDir(apmRoot) {
604
+ return join4(apmRoot ?? workspaceApmDir(), "project");
605
+ }
606
+ function projectDocumentLocalPath(apmRoot, documentPath) {
607
+ const normalized = normalizeLocalDocumentPath(documentPath);
608
+ return join4(projectDocumentsDir(apmRoot), ...normalized.split("/"));
609
+ }
610
+ function normalizeLocalDocumentPath(path10) {
611
+ const trimmed = path10.trim().replace(/\\/g, "/");
612
+ if (!trimmed || trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) {
613
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path10}`);
614
+ }
615
+ const segments = trimmed.split("/").filter(Boolean);
616
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
617
+ throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path10}`);
618
+ }
619
+ return segments.join("/");
620
+ }
621
+ function hashLocalFileContent(content) {
622
+ return createHash("sha256").update(content, "utf8").digest("hex");
623
+ }
624
+ function readLocalManifest(apmRoot) {
625
+ const manifestPath2 = join4(projectDocumentsDir(apmRoot), MANIFEST_FILE);
626
+ if (!existsSync2(manifestPath2)) {
627
+ return null;
628
+ }
629
+ try {
630
+ return JSON.parse(
631
+ readFileSync3(manifestPath2, "utf8")
632
+ );
633
+ } catch {
634
+ return null;
635
+ }
636
+ }
637
+ function listLocalDocumentPaths(apmRoot) {
638
+ const root = projectDocumentsDir(apmRoot);
639
+ if (!existsSync2(root)) {
640
+ return [];
641
+ }
642
+ const paths = [];
643
+ const walk = (dir) => {
644
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
645
+ const abs = join4(dir, entry.name);
646
+ if (entry.isDirectory()) {
647
+ walk(abs);
648
+ continue;
649
+ }
650
+ if (entry.isFile() && entry.name === MANIFEST_FILE) {
651
+ continue;
652
+ }
653
+ const rel = relative(root, abs).split(sep).join("/");
654
+ paths.push(rel);
655
+ }
656
+ };
657
+ walk(root);
658
+ return paths.sort();
659
+ }
660
+ function diffManifestPaths(remote, local) {
661
+ const remoteMap = new Map(
662
+ (remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
663
+ );
664
+ const localMap = new Map(
665
+ (local?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
666
+ );
667
+ const download = [];
668
+ for (const [path10, hash] of remoteMap) {
669
+ if (localMap.get(path10) !== hash) {
670
+ download.push(path10);
671
+ }
672
+ }
673
+ const deleteLocal = [];
674
+ for (const path10 of localMap.keys()) {
675
+ if (!remoteMap.has(path10)) {
676
+ deleteLocal.push(path10);
677
+ }
678
+ }
679
+ return { download, deleteLocal };
680
+ }
681
+ async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
682
+ const empty = {
683
+ synced: false,
684
+ repositoryId: null,
685
+ downloaded: 0,
686
+ deleted: 0
687
+ };
688
+ const cfg = await tryReadApmConfig();
689
+ if (!cfg || !resolveApiKey(cfg)) {
690
+ console.log(
691
+ "[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"
692
+ );
693
+ return empty;
694
+ }
695
+ const api = createApmApiClient(cfg);
696
+ const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
697
+ api,
698
+ workdirPath
699
+ );
700
+ if (!repositoryId) {
701
+ console.log(
702
+ `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
703
+ ${diagnostic ?? ""}`
704
+ );
705
+ return empty;
706
+ }
707
+ const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
708
+ const projectDir = projectDocumentsDir(targetApmDir);
709
+ await ensureDirExists(projectDir);
710
+ const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
711
+ if (!remoteManifest) {
712
+ console.log(
713
+ `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
714
+ );
715
+ return { ...empty, repositoryId };
716
+ }
717
+ const localManifest = readLocalManifest(targetApmDir);
718
+ const { download, deleteLocal } = diffManifestPaths(
719
+ remoteManifest,
720
+ localManifest
721
+ );
722
+ let downloaded = 0;
723
+ if (download.length > 0) {
724
+ const { list } = await api.cli.listRepositoryProjectDocuments({
725
+ repositoryId,
726
+ paths: download.join(",")
727
+ });
728
+ for (const doc of list) {
729
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
730
+ await ensureDirExists(dirname2(absPath));
731
+ writeFileSync4(absPath, doc.content, "utf8");
732
+ downloaded += 1;
733
+ }
734
+ }
735
+ let deleted = 0;
736
+ for (const path10 of deleteLocal) {
737
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path10));
738
+ if (existsSync2(absPath)) {
739
+ rmSync(absPath, { force: true });
740
+ deleted += 1;
741
+ }
742
+ }
743
+ writeFileSync4(
744
+ toFsPath(join4(projectDir, MANIFEST_FILE)),
745
+ `${JSON.stringify(remoteManifest, null, 2)}
746
+ `,
747
+ "utf8"
748
+ );
749
+ console.log(
750
+ `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
751
+ );
752
+ return {
753
+ synced: true,
754
+ repositoryId,
755
+ downloaded,
756
+ deleted
757
+ };
758
+ }
759
+ async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
760
+ const api = createApmApiClient(cfg);
761
+ const { repositoryId } = await resolveRepositoryIdForSync(api, workdirPath);
762
+ if (!repositoryId) {
763
+ return 0;
764
+ }
765
+ const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
766
+ const localPaths = listLocalDocumentPaths(targetApmDir);
767
+ if (localPaths.length === 0) {
768
+ console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
769
+ return 0;
770
+ }
771
+ const remoteManifest = (await api.cli.getRepositoryProjectDocumentManifest({ repositoryId })).manifest ?? null;
772
+ const remoteHashByPath = new Map(
773
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
774
+ );
775
+ const remoteDescriptionByPath = new Map(
776
+ (remoteManifest?.documents ?? []).map((doc) => [
777
+ doc.path,
778
+ doc.description
779
+ ])
780
+ );
781
+ let synced = 0;
782
+ for (const path10 of localPaths) {
783
+ const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path10));
784
+ const content = readFileSync3(absPath, "utf8");
785
+ const contentHash = hashLocalFileContent(content);
786
+ if (remoteHashByPath.get(path10) === contentHash) {
787
+ continue;
788
+ }
789
+ await api.cli.upsertRepositoryProjectDocument({
790
+ repositoryId,
791
+ path: path10,
792
+ content,
793
+ description: remoteDescriptionByPath.get(path10) ?? void 0
794
+ });
795
+ synced += 1;
796
+ console.log(`[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: ${path10}`);
797
+ }
798
+ if (synced === 0) {
799
+ console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
800
+ }
801
+ return synced;
802
+ }
803
+
592
804
  // src/git-utils.ts
593
805
  import { execFile as execFile2 } from "child_process";
594
806
  import { promisify as promisify2 } from "util";
@@ -666,13 +878,14 @@ async function ensureWorkspaceInitialized(workdir, options) {
666
878
  const apmDir = workspaceApmDir(workdir);
667
879
  await copyTemplateFiles(apmDir, workdir);
668
880
  const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
881
+ await syncRepositoryProjectDocumentsPull(workdir, apmDir);
669
882
  const trimmedName = options?.name?.trim();
670
883
  if (trimmedName) {
671
- const apmConfigPath = toFsPath(join4(apmDir, "apm.config.json"));
672
- const config = readFileSync3(apmConfigPath, "utf8");
884
+ const apmConfigPath = toFsPath(join5(apmDir, "apm.config.json"));
885
+ const config = readFileSync4(apmConfigPath, "utf8");
673
886
  const configJson = JSON.parse(config);
674
887
  configJson.name = trimmedName;
675
- writeFileSync4(
888
+ writeFileSync5(
676
889
  apmConfigPath,
677
890
  `${JSON.stringify(configJson, null, 2)}
678
891
  `,
@@ -703,7 +916,7 @@ async function runInit(name) {
703
916
  }
704
917
 
705
918
  // src/commands/login.ts
706
- import { existsSync as existsSync2 } from "fs";
919
+ import { existsSync as existsSync3 } from "fs";
707
920
  import { ApiError } from "listpage-http";
708
921
  async function runLogin(opts) {
709
922
  const baseUrl = (opts.server?.trim() || process.env.AI_PM_SERVER?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
@@ -758,7 +971,7 @@ async function runLogin(opts) {
758
971
  );
759
972
  const workdir = resolveWorkdirPath();
760
973
  const apmDir = workspaceApmDir(workdir);
761
- if (existsSync2(apmDir)) {
974
+ if (existsSync3(apmDir)) {
762
975
  await syncRemoteDeploymentConfig(workdir, apmDir);
763
976
  }
764
977
  }
@@ -1161,9 +1374,9 @@ function formatSessionMessagesXml(sessionId, messages) {
1161
1374
  }
1162
1375
 
1163
1376
  // src/commands/sync-session-attachments.ts
1164
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1165
- import { join as join5 } from "path";
1166
- var MANIFEST_FILE = ".sync-manifest.json";
1377
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
1378
+ import { join as join6 } from "path";
1379
+ var MANIFEST_FILE2 = ".sync-manifest.json";
1167
1380
  async function downloadAttachment(cfg, attachmentId) {
1168
1381
  const base = cfg.baseUrl.trim().replace(/\/+$/, "");
1169
1382
  const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({ attachmentId })}`;
@@ -1176,13 +1389,13 @@ async function downloadAttachment(cfg, attachmentId) {
1176
1389
  return Buffer.from(await res.arrayBuffer());
1177
1390
  }
1178
1391
  function loadManifest(dir) {
1179
- const path10 = join5(dir, MANIFEST_FILE);
1180
- if (!existsSync3(path10)) {
1392
+ const path10 = join6(dir, MANIFEST_FILE2);
1393
+ if (!existsSync4(path10)) {
1181
1394
  return { version: 1, attachments: {} };
1182
1395
  }
1183
1396
  try {
1184
1397
  const parsed = JSON.parse(
1185
- readFileSync4(path10, "utf8")
1398
+ readFileSync5(path10, "utf8")
1186
1399
  );
1187
1400
  if (parsed?.version === 1 && parsed.attachments && typeof parsed.attachments === "object") {
1188
1401
  return parsed;
@@ -1192,21 +1405,21 @@ function loadManifest(dir) {
1192
1405
  return { version: 1, attachments: {} };
1193
1406
  }
1194
1407
  function saveManifest(dir, manifest) {
1195
- writeFileSync5(
1196
- join5(dir, MANIFEST_FILE),
1408
+ writeFileSync6(
1409
+ join6(dir, MANIFEST_FILE2),
1197
1410
  `${JSON.stringify(manifest, null, 2)}
1198
1411
  `,
1199
1412
  "utf8"
1200
1413
  );
1201
1414
  }
1202
1415
  function isAttachmentUpToDate(entry, item, dest) {
1203
- if (!entry || !existsSync3(dest)) return false;
1416
+ if (!entry || !existsSync4(dest)) return false;
1204
1417
  if (entry.name !== item.name) return false;
1205
1418
  const createdAt = item.createdAt ?? "";
1206
1419
  return entry.createdAt === createdAt;
1207
1420
  }
1208
1421
  async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
1209
- const dir = join5(sessionDir(sessionId, apmRoot), SESSION_ATTACHMENTS_SUBDIR);
1422
+ const dir = join6(sessionDir(sessionId, apmRoot), SESSION_ATTACHMENTS_SUBDIR);
1210
1423
  await ensureDirExists(dir);
1211
1424
  if (attachments.length === 0) {
1212
1425
  saveManifest(dir, { version: 1, attachments: {} });
@@ -1215,7 +1428,7 @@ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
1215
1428
  const manifest = loadManifest(dir);
1216
1429
  const nextManifest = { version: 1, attachments: {} };
1217
1430
  for (const item of attachments) {
1218
- const dest = join5(dir, item.name);
1431
+ const dest = join6(dir, item.name);
1219
1432
  const entry = manifest.attachments[item.id];
1220
1433
  const createdAt = item.createdAt ?? "";
1221
1434
  if (isAttachmentUpToDate(entry, item, dest)) {
@@ -1226,7 +1439,7 @@ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
1226
1439
  continue;
1227
1440
  }
1228
1441
  const buffer = await downloadAttachment(cfg, item.id);
1229
- writeFileSync5(dest, buffer);
1442
+ writeFileSync6(dest, buffer);
1230
1443
  nextManifest.attachments[item.id] = {
1231
1444
  name: item.name,
1232
1445
  createdAt
@@ -1237,46 +1450,46 @@ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
1237
1450
  }
1238
1451
 
1239
1452
  // src/rules-sync.ts
1240
- import { basename as basename2, extname as extname2, join as join7 } from "path";
1241
- import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
1453
+ import { basename as basename2, extname as extname2, join as join8 } from "path";
1454
+ import { existsSync as existsSync6, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
1242
1455
 
1243
1456
  // src/skills-sync.ts
1244
1457
  import {
1245
1458
  copyFileSync as copyFileSync2,
1246
1459
  cpSync,
1247
- existsSync as existsSync4,
1460
+ existsSync as existsSync5,
1248
1461
  mkdirSync as mkdirSync3,
1249
- readdirSync as readdirSync2,
1250
- rmSync,
1462
+ readdirSync as readdirSync3,
1463
+ rmSync as rmSync2,
1251
1464
  statSync as statSync2,
1252
- writeFileSync as writeFileSync6
1465
+ writeFileSync as writeFileSync7
1253
1466
  } from "fs";
1254
- import { join as join6 } from "path";
1255
- var AGENTS_TEMPLATE_PATH = join6(CLI_TEMPLATE_DIR, "AGENTS.md");
1256
- var BASE_SKILLS_TEMPLATE_DIR = join6(CLI_TEMPLATE_DIR, "skills");
1257
- var BASE_RULES_TEMPLATE_DIR = join6(CLI_TEMPLATE_DIR, "rules");
1467
+ import { join as join7 } from "path";
1468
+ var AGENTS_TEMPLATE_PATH = join7(CLI_TEMPLATE_DIR, "AGENTS.md");
1469
+ var BASE_SKILLS_TEMPLATE_DIR = join7(CLI_TEMPLATE_DIR, "skills");
1470
+ var BASE_RULES_TEMPLATE_DIR = join7(CLI_TEMPLATE_DIR, "rules");
1258
1471
  function sanitizeSkillDirName(name) {
1259
1472
  const trimmed = name.trim();
1260
1473
  if (!trimmed) return "skill";
1261
1474
  return trimmed.replace(/[/\\:*?"<>|]/g, "_");
1262
1475
  }
1263
1476
  function listBaseSkillDirNames() {
1264
- if (!existsSync4(BASE_SKILLS_TEMPLATE_DIR)) return [];
1265
- return readdirSync2(BASE_SKILLS_TEMPLATE_DIR).filter((name) => {
1266
- const path10 = join6(BASE_SKILLS_TEMPLATE_DIR, name);
1477
+ if (!existsSync5(BASE_SKILLS_TEMPLATE_DIR)) return [];
1478
+ return readdirSync3(BASE_SKILLS_TEMPLATE_DIR).filter((name) => {
1479
+ const path10 = join7(BASE_SKILLS_TEMPLATE_DIR, name);
1267
1480
  return statSync2(path10).isDirectory();
1268
1481
  });
1269
1482
  }
1270
1483
  function syncAgentsGuide(apmDir) {
1271
- if (!existsSync4(AGENTS_TEMPLATE_PATH)) return false;
1484
+ if (!existsSync5(AGENTS_TEMPLATE_PATH)) return false;
1272
1485
  mkdirSync3(apmDir, { recursive: true });
1273
- copyFileSync2(AGENTS_TEMPLATE_PATH, join6(apmDir, "AGENTS.md"));
1486
+ copyFileSync2(AGENTS_TEMPLATE_PATH, join7(apmDir, "AGENTS.md"));
1274
1487
  return true;
1275
1488
  }
1276
1489
  function listBaseRuleFileNames() {
1277
- if (!existsSync4(BASE_RULES_TEMPLATE_DIR)) return [];
1278
- return readdirSync2(BASE_RULES_TEMPLATE_DIR).filter((name) => {
1279
- const path10 = join6(BASE_RULES_TEMPLATE_DIR, name);
1490
+ if (!existsSync5(BASE_RULES_TEMPLATE_DIR)) return [];
1491
+ return readdirSync3(BASE_RULES_TEMPLATE_DIR).filter((name) => {
1492
+ const path10 = join7(BASE_RULES_TEMPLATE_DIR, name);
1280
1493
  return statSync2(path10).isFile();
1281
1494
  });
1282
1495
  }
@@ -1284,8 +1497,8 @@ function syncBaseRules(rulesDir) {
1284
1497
  mkdirSync3(rulesDir, { recursive: true });
1285
1498
  const names = listBaseRuleFileNames();
1286
1499
  for (const name of names) {
1287
- const src = join6(BASE_RULES_TEMPLATE_DIR, name);
1288
- const dest = join6(rulesDir, name);
1500
+ const src = join7(BASE_RULES_TEMPLATE_DIR, name);
1501
+ const dest = join7(rulesDir, name);
1289
1502
  copyFileSync2(src, dest);
1290
1503
  }
1291
1504
  return names;
@@ -1294,8 +1507,8 @@ function syncBaseSkills(skillsDir) {
1294
1507
  mkdirSync3(skillsDir, { recursive: true });
1295
1508
  const names = listBaseSkillDirNames();
1296
1509
  for (const name of names) {
1297
- const src = join6(BASE_SKILLS_TEMPLATE_DIR, name);
1298
- const dest = join6(skillsDir, name);
1510
+ const src = join7(BASE_SKILLS_TEMPLATE_DIR, name);
1511
+ const dest = join7(skillsDir, name);
1299
1512
  cpSync(src, dest, { recursive: true, force: true });
1300
1513
  }
1301
1514
  return names;
@@ -1312,26 +1525,26 @@ function syncSupplementarySkills(skillsDir, list) {
1312
1525
  skipped.push(dirName);
1313
1526
  continue;
1314
1527
  }
1315
- const skillDir = join6(skillsDir, dirName);
1528
+ const skillDir = join7(skillsDir, dirName);
1316
1529
  mkdirSync3(skillDir, { recursive: true });
1317
- writeFileSync6(join6(skillDir, "SKILL.md"), skill.content ?? "", "utf8");
1530
+ writeFileSync7(join7(skillDir, "SKILL.md"), skill.content ?? "", "utf8");
1318
1531
  written.push(dirName);
1319
1532
  }
1320
1533
  const removed = [];
1321
- if (!existsSync4(skillsDir)) return { written, skipped, removed };
1322
- for (const entry of readdirSync2(skillsDir)) {
1323
- const full = join6(skillsDir, entry);
1534
+ if (!existsSync5(skillsDir)) return { written, skipped, removed };
1535
+ for (const entry of readdirSync3(skillsDir)) {
1536
+ const full = join7(skillsDir, entry);
1324
1537
  if (!statSync2(full).isDirectory()) continue;
1325
1538
  if (baseNames.has(entry)) continue;
1326
1539
  if (apiDirNames.has(entry)) continue;
1327
- rmSync(full, { recursive: true, force: true });
1540
+ rmSync2(full, { recursive: true, force: true });
1328
1541
  removed.push(entry);
1329
1542
  }
1330
1543
  return { written, skipped, removed };
1331
1544
  }
1332
1545
 
1333
1546
  // src/rules-sync.ts
1334
- var MANIFEST_FILE2 = ".rules-sync-manifest.json";
1547
+ var MANIFEST_FILE3 = ".rules-sync-manifest.json";
1335
1548
  function ruleLocalFileName(ruleName) {
1336
1549
  const trimmed = ruleName.trim();
1337
1550
  if (!trimmed) return "rule.md";
@@ -1340,13 +1553,13 @@ function ruleLocalFileName(ruleName) {
1340
1553
  return `${sanitized}.md`;
1341
1554
  }
1342
1555
  function loadManifest2(rulesDir) {
1343
- const path10 = join7(rulesDir, MANIFEST_FILE2);
1344
- if (!existsSync5(toFsPath(path10))) {
1556
+ const path10 = join8(rulesDir, MANIFEST_FILE3);
1557
+ if (!existsSync6(toFsPath(path10))) {
1345
1558
  return { version: 1, rules: {} };
1346
1559
  }
1347
1560
  try {
1348
1561
  const parsed = JSON.parse(
1349
- readFileSync5(toFsPath(path10), "utf8")
1562
+ readFileSync6(toFsPath(path10), "utf8")
1350
1563
  );
1351
1564
  if (parsed?.version === 1 && parsed.rules && typeof parsed.rules === "object") {
1352
1565
  return parsed;
@@ -1356,8 +1569,8 @@ function loadManifest2(rulesDir) {
1356
1569
  return { version: 1, rules: {} };
1357
1570
  }
1358
1571
  function saveManifest2(rulesDir, manifest) {
1359
- writeFileSync7(
1360
- toFsPath(join7(rulesDir, MANIFEST_FILE2)),
1572
+ writeFileSync8(
1573
+ toFsPath(join8(rulesDir, MANIFEST_FILE3)),
1361
1574
  `${JSON.stringify(manifest, null, 2)}
1362
1575
  `,
1363
1576
  "utf8"
@@ -1367,18 +1580,18 @@ function isBaseRuleFileName(fileName) {
1367
1580
  return listBaseRuleFileNames().includes(basename2(fileName));
1368
1581
  }
1369
1582
  function isRuleUpToDate(entry, rule, dest) {
1370
- if (!entry || !existsSync5(toFsPath(dest))) return false;
1583
+ if (!entry || !existsSync6(toFsPath(dest))) return false;
1371
1584
  if (entry.fileName !== ruleLocalFileName(rule.name)) return false;
1372
1585
  const updatedAt = rule.updatedAt ?? "";
1373
1586
  if (entry.updatedAt !== updatedAt) return false;
1374
- const localContent = readFileSync5(toFsPath(dest), "utf8");
1587
+ const localContent = readFileSync6(toFsPath(dest), "utf8");
1375
1588
  return localContent === (rule.content ?? "");
1376
1589
  }
1377
1590
  async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1378
1591
  const api = createApmApiClient(cfg);
1379
1592
  const baseline = await api.cli.branchBaseline({ sessionId, workdirPath });
1380
1593
  const repositoryId = baseline.repositoryId;
1381
- const rulesDir = join7(apmRoot ?? workspaceApmDir(workdirPath), "rules");
1594
+ const rulesDir = join8(apmRoot ?? workspaceApmDir(workdirPath), "rules");
1382
1595
  await ensureDirExists(rulesDir);
1383
1596
  if (!repositoryId) {
1384
1597
  console.log(
@@ -1395,7 +1608,7 @@ async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1395
1608
  for (const rule of list) {
1396
1609
  remoteIds.add(rule.id);
1397
1610
  const fileName = ruleLocalFileName(rule.name);
1398
- const dest = join7(rulesDir, fileName);
1611
+ const dest = join8(rulesDir, fileName);
1399
1612
  const entry = manifest.rules[rule.id];
1400
1613
  const updatedAt = rule.updatedAt ?? "";
1401
1614
  if (isRuleUpToDate(entry, rule, dest)) {
@@ -1404,7 +1617,7 @@ async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1404
1617
  console.log(`[apm] \u89C4\u5219\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: rules/${fileName}`);
1405
1618
  continue;
1406
1619
  }
1407
- writeFileSync7(toFsPath(dest), rule.content ?? "", "utf8");
1620
+ writeFileSync8(toFsPath(dest), rule.content ?? "", "utf8");
1408
1621
  nextManifest.rules[rule.id] = { fileName, updatedAt };
1409
1622
  written.push(fileName);
1410
1623
  console.log(`[apm] \u5DF2\u540C\u6B65\u5E73\u53F0\u89C4\u5219: rules/${fileName}`);
@@ -1413,9 +1626,9 @@ async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1413
1626
  for (const [ruleId, entry] of Object.entries(manifest.rules)) {
1414
1627
  if (remoteIds.has(ruleId)) continue;
1415
1628
  if (isBaseRuleFileName(entry.fileName)) continue;
1416
- const dest = join7(rulesDir, entry.fileName);
1417
- if (existsSync5(toFsPath(dest))) {
1418
- rmSync2(toFsPath(dest), { force: true });
1629
+ const dest = join8(rulesDir, entry.fileName);
1630
+ if (existsSync6(toFsPath(dest))) {
1631
+ rmSync3(toFsPath(dest), { force: true });
1419
1632
  }
1420
1633
  removed.push(entry.fileName);
1421
1634
  console.log(`[apm] \u5DF2\u79FB\u9664\u5DF2\u4E0B\u7EBF\u7684\u5E73\u53F0\u89C4\u5219: rules/${entry.fileName}`);
@@ -1424,218 +1637,6 @@ async function syncPlatformRules(cfg, sessionId, workdirPath, apmRoot) {
1424
1637
  return { written, skipped, removed, repositoryId };
1425
1638
  }
1426
1639
 
1427
- // src/repository-project-documents-sync.ts
1428
- import {
1429
- existsSync as existsSync6,
1430
- readdirSync as readdirSync3,
1431
- readFileSync as readFileSync6,
1432
- rmSync as rmSync3,
1433
- writeFileSync as writeFileSync8
1434
- } from "fs";
1435
- import { createHash } from "crypto";
1436
- import { dirname as dirname2, join as join8, relative, sep } from "path";
1437
- var MANIFEST_FILE3 = "manifest.json";
1438
- function projectDocumentsDir(apmRoot) {
1439
- return join8(apmRoot ?? workspaceApmDir(), "project");
1440
- }
1441
- function projectDocumentLocalPath(apmRoot, documentPath) {
1442
- const normalized = normalizeLocalDocumentPath(documentPath);
1443
- return join8(projectDocumentsDir(apmRoot), ...normalized.split("/"));
1444
- }
1445
- function normalizeLocalDocumentPath(path10) {
1446
- const trimmed = path10.trim().replace(/\\/g, "/");
1447
- if (!trimmed || trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) {
1448
- throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path10}`);
1449
- }
1450
- const segments = trimmed.split("/").filter(Boolean);
1451
- if (segments.some((segment) => segment === ".." || segment === ".")) {
1452
- throw new Error(`\u975E\u6CD5\u6587\u6863\u8DEF\u5F84: ${path10}`);
1453
- }
1454
- return segments.join("/");
1455
- }
1456
- function hashLocalFileContent(content) {
1457
- return createHash("sha256").update(content, "utf8").digest("hex");
1458
- }
1459
- function readLocalManifest(apmRoot) {
1460
- const manifestPath2 = join8(projectDocumentsDir(apmRoot), MANIFEST_FILE3);
1461
- if (!existsSync6(manifestPath2)) {
1462
- return null;
1463
- }
1464
- try {
1465
- return JSON.parse(
1466
- readFileSync6(manifestPath2, "utf8")
1467
- );
1468
- } catch {
1469
- return null;
1470
- }
1471
- }
1472
- function listLocalDocumentPaths(apmRoot) {
1473
- const root = projectDocumentsDir(apmRoot);
1474
- if (!existsSync6(root)) {
1475
- return [];
1476
- }
1477
- const paths = [];
1478
- const walk = (dir) => {
1479
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1480
- const abs = join8(dir, entry.name);
1481
- if (entry.isDirectory()) {
1482
- walk(abs);
1483
- continue;
1484
- }
1485
- if (entry.isFile() && entry.name === MANIFEST_FILE3) {
1486
- continue;
1487
- }
1488
- const rel = relative(root, abs).split(sep).join("/");
1489
- paths.push(rel);
1490
- }
1491
- };
1492
- walk(root);
1493
- return paths.sort();
1494
- }
1495
- function diffManifestPaths(remote, local) {
1496
- const remoteMap = new Map(
1497
- (remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1498
- );
1499
- const localMap = new Map(
1500
- (local?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1501
- );
1502
- const download = [];
1503
- for (const [path10, hash] of remoteMap) {
1504
- if (localMap.get(path10) !== hash) {
1505
- download.push(path10);
1506
- }
1507
- }
1508
- const deleteLocal = [];
1509
- for (const path10 of localMap.keys()) {
1510
- if (!remoteMap.has(path10)) {
1511
- deleteLocal.push(path10);
1512
- }
1513
- }
1514
- return { download, deleteLocal };
1515
- }
1516
- async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
1517
- const empty = {
1518
- synced: false,
1519
- repositoryId: null,
1520
- downloaded: 0,
1521
- deleted: 0
1522
- };
1523
- const cfg = await tryReadApmConfig();
1524
- if (!cfg || !resolveApiKey(cfg)) {
1525
- console.log(
1526
- "[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"
1527
- );
1528
- return empty;
1529
- }
1530
- const api = createApmApiClient(cfg);
1531
- const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
1532
- api,
1533
- workdirPath
1534
- );
1535
- if (!repositoryId) {
1536
- console.log(
1537
- `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
1538
- ${diagnostic ?? ""}`
1539
- );
1540
- return empty;
1541
- }
1542
- const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1543
- const projectDir = projectDocumentsDir(targetApmDir);
1544
- await ensureDirExists(projectDir);
1545
- const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
1546
- if (!remoteManifest) {
1547
- console.log(
1548
- `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
1549
- );
1550
- return { ...empty, repositoryId };
1551
- }
1552
- const localManifest = readLocalManifest(targetApmDir);
1553
- const { download, deleteLocal } = diffManifestPaths(
1554
- remoteManifest,
1555
- localManifest
1556
- );
1557
- let downloaded = 0;
1558
- if (download.length > 0) {
1559
- const { list } = await api.cli.listRepositoryProjectDocuments({
1560
- repositoryId,
1561
- paths: download.join(",")
1562
- });
1563
- for (const doc of list) {
1564
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
1565
- await ensureDirExists(dirname2(absPath));
1566
- writeFileSync8(absPath, doc.content, "utf8");
1567
- downloaded += 1;
1568
- }
1569
- }
1570
- let deleted = 0;
1571
- for (const path10 of deleteLocal) {
1572
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path10));
1573
- if (existsSync6(absPath)) {
1574
- rmSync3(absPath, { force: true });
1575
- deleted += 1;
1576
- }
1577
- }
1578
- writeFileSync8(
1579
- toFsPath(join8(projectDir, MANIFEST_FILE3)),
1580
- `${JSON.stringify(remoteManifest, null, 2)}
1581
- `,
1582
- "utf8"
1583
- );
1584
- console.log(
1585
- `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1586
- );
1587
- return {
1588
- synced: true,
1589
- repositoryId,
1590
- downloaded,
1591
- deleted
1592
- };
1593
- }
1594
- async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1595
- const api = createApmApiClient(cfg);
1596
- const { repositoryId } = await resolveRepositoryIdForSync(api, workdirPath);
1597
- if (!repositoryId) {
1598
- return 0;
1599
- }
1600
- const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
1601
- const localPaths = listLocalDocumentPaths(targetApmDir);
1602
- if (localPaths.length === 0) {
1603
- console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1604
- return 0;
1605
- }
1606
- const remoteManifest = (await api.cli.getRepositoryProjectDocumentManifest({ repositoryId })).manifest ?? null;
1607
- const remoteHashByPath = new Map(
1608
- (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1609
- );
1610
- const remoteDescriptionByPath = new Map(
1611
- (remoteManifest?.documents ?? []).map((doc) => [
1612
- doc.path,
1613
- doc.description
1614
- ])
1615
- );
1616
- let synced = 0;
1617
- for (const path10 of localPaths) {
1618
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path10));
1619
- const content = readFileSync6(absPath, "utf8");
1620
- const contentHash = hashLocalFileContent(content);
1621
- if (remoteHashByPath.get(path10) === contentHash) {
1622
- continue;
1623
- }
1624
- await api.cli.upsertRepositoryProjectDocument({
1625
- repositoryId,
1626
- path: path10,
1627
- content,
1628
- description: remoteDescriptionByPath.get(path10) ?? void 0
1629
- });
1630
- synced += 1;
1631
- console.log(`[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: ${path10}`);
1632
- }
1633
- if (synced === 0) {
1634
- console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1635
- }
1636
- return synced;
1637
- }
1638
-
1639
1640
  // src/commands/pull.ts
1640
1641
  async function runPull(sessionId, remoteWorkdir) {
1641
1642
  const trimmedId = sessionId.trim();
@@ -3028,6 +3029,12 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
3028
3029
  console.log(`[apm] step=update-skills skipped workdir=${workdir}`);
3029
3030
  }
3030
3031
  if (signal.aborted) return;
3032
+ if (!pullRan) {
3033
+ await runStep(
3034
+ "sync-project-documents-pull",
3035
+ () => syncRepositoryProjectDocumentsPull(workdir, apmRoot)
3036
+ );
3037
+ }
3031
3038
  await runStep(
3032
3039
  "cursor-agent",
3033
3040
  () => runCursorAgent(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.59",
3
+ "version": "6.0.60",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,