@hangox/pm-cli 0.2.6 → 1.1.1

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
@@ -562,10 +562,217 @@ function createTestCommand() {
562
562
 
563
563
  // src/commands/config/index.ts
564
564
  import { Command as Command2 } from "commander";
565
+
566
+ // src/telemetry/index.ts
567
+ import * as os3 from "os";
568
+
569
+ // src/telemetry/client.ts
570
+ import { PostHog } from "posthog-node";
571
+ import * as os2 from "os";
572
+ import * as crypto2 from "crypto";
573
+ import * as fs2 from "fs";
574
+ import * as path2 from "path";
575
+
576
+ // src/telemetry/sanitizer.ts
577
+ import * as crypto from "crypto";
578
+ function hashString(str) {
579
+ return crypto.createHash("sha256").update(str).digest("hex").slice(0, 16);
580
+ }
581
+ function classifyError(error) {
582
+ if (error instanceof Error) {
583
+ const message = error.message.toLowerCase();
584
+ const name = error.name.toLowerCase();
585
+ if (message.includes("econnrefused") || message.includes("enotfound") || message.includes("etimedout") || message.includes("network") || message.includes("fetch failed") || name.includes("fetch")) {
586
+ return "network";
587
+ }
588
+ if (message.includes("401") || message.includes("403") || message.includes("unauthorized") || message.includes("forbidden") || message.includes("token") || message.includes("authentication")) {
589
+ return "auth";
590
+ }
591
+ if (message.includes("validation") || message.includes("invalid") || message.includes("required") || message.includes("missing")) {
592
+ return "validation";
593
+ }
594
+ if (message.includes("400") || message.includes("404") || message.includes("500") || message.includes("502") || message.includes("503")) {
595
+ return "api";
596
+ }
597
+ }
598
+ return "unknown";
599
+ }
600
+
601
+ // src/telemetry/client.ts
602
+ var POSTHOG_KEY = "phc_GrxYD6adJvJyDkSSUwWhhD4hThh3EOSJ4Q7xKkNSfdl";
603
+ var POSTHOG_HOST = "https://us.i.posthog.com";
604
+ var CONFIG_DIR = path2.join(os2.homedir(), ".config", "pm-cli");
605
+ var TELEMETRY_ID_FILE = path2.join(CONFIG_DIR, ".telemetry-id");
606
+ var client = null;
607
+ var cliVersion = "unknown";
608
+ function getOrCreateDeviceId() {
609
+ try {
610
+ if (fs2.existsSync(TELEMETRY_ID_FILE)) {
611
+ return fs2.readFileSync(TELEMETRY_ID_FILE, "utf-8").trim();
612
+ }
613
+ if (!fs2.existsSync(CONFIG_DIR)) {
614
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
615
+ }
616
+ const id = `device_${crypto2.randomUUID()}`;
617
+ fs2.writeFileSync(TELEMETRY_ID_FILE, id);
618
+ return id;
619
+ } catch {
620
+ return `temp_${crypto2.randomUUID()}`;
621
+ }
622
+ }
623
+ function getDistinctId() {
624
+ const userMail = getConfigValue("userMail");
625
+ if (userMail) {
626
+ return `user_${hashString(userMail)}`;
627
+ }
628
+ return getOrCreateDeviceId();
629
+ }
630
+ function initTelemetry(version2) {
631
+ cliVersion = version2;
632
+ try {
633
+ client = new PostHog(POSTHOG_KEY, {
634
+ host: POSTHOG_HOST,
635
+ flushAt: 1,
636
+ // CLI 单次执行,立即发送
637
+ flushInterval: 0
638
+ // 禁用定时刷新
639
+ });
640
+ logger_default.debug("\u9065\u6D4B\u5DF2\u521D\u59CB\u5316");
641
+ } catch (error) {
642
+ logger_default.debug("\u9065\u6D4B\u521D\u59CB\u5316\u5931\u8D25:", error);
643
+ client = null;
644
+ }
645
+ }
646
+ async function shutdownTelemetry() {
647
+ if (client) {
648
+ try {
649
+ await client.shutdown();
650
+ logger_default.debug("\u9065\u6D4B\u5DF2\u5173\u95ED");
651
+ } catch (error) {
652
+ logger_default.debug("\u9065\u6D4B\u5173\u95ED\u5931\u8D25:", error);
653
+ }
654
+ }
655
+ }
656
+ function getCommonProperties() {
657
+ return {
658
+ cli_version: cliVersion,
659
+ node_version: process.version,
660
+ os: process.platform,
661
+ os_version: os2.release(),
662
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
663
+ };
664
+ }
665
+ function capture(event, properties) {
666
+ if (!client) {
667
+ return;
668
+ }
669
+ try {
670
+ client.capture({
671
+ distinctId: getDistinctId(),
672
+ event,
673
+ properties: {
674
+ ...getCommonProperties(),
675
+ ...properties
676
+ }
677
+ });
678
+ logger_default.debug(`\u9065\u6D4B\u4E8B\u4EF6: ${event}`, properties);
679
+ } catch (error) {
680
+ logger_default.debug("\u9065\u6D4B\u4E8B\u4EF6\u53D1\u9001\u5931\u8D25:", error);
681
+ }
682
+ }
683
+
684
+ // src/telemetry/events.ts
685
+ var EventNames = {
686
+ SESSION_STARTED: "cli_session_started",
687
+ COMMAND_EXECUTED: "command_executed",
688
+ ERROR_OCCURRED: "error_occurred",
689
+ CONFIG_CHANGED: "config_changed",
690
+ MILESTONE_REACHED: "milestone_reached",
691
+ PERFORMANCE_METRIC: "performance_metric"
692
+ };
693
+
694
+ // src/telemetry/index.ts
695
+ var currentContext = null;
696
+ function startCommandTracking(command, subcommand, extraProps) {
697
+ currentContext = {
698
+ command,
699
+ subcommand,
700
+ startTime: Date.now(),
701
+ extraProps
702
+ };
703
+ }
704
+ function endCommandTracking(extraProps) {
705
+ if (!currentContext) return;
706
+ const duration = Date.now() - currentContext.startTime;
707
+ const properties = {
708
+ command: currentContext.command,
709
+ subcommand: currentContext.subcommand,
710
+ success: true,
711
+ duration_ms: duration,
712
+ ...currentContext.extraProps,
713
+ ...extraProps
714
+ };
715
+ capture(EventNames.COMMAND_EXECUTED, properties);
716
+ currentContext = null;
717
+ }
718
+ function failCommandTracking(error, extraProps) {
719
+ if (!currentContext) return;
720
+ const duration = Date.now() - currentContext.startTime;
721
+ const errorType = classifyError(error);
722
+ const properties = {
723
+ command: currentContext.command,
724
+ subcommand: currentContext.subcommand,
725
+ success: false,
726
+ duration_ms: duration,
727
+ error_type: errorType,
728
+ ...currentContext.extraProps,
729
+ ...extraProps
730
+ };
731
+ capture(EventNames.COMMAND_EXECUTED, properties);
732
+ currentContext = null;
733
+ }
734
+ function isCI() {
735
+ return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.JENKINS_URL || process.env.TRAVIS);
736
+ }
737
+ function getTerminal() {
738
+ return process.env.TERM_PROGRAM || process.env.TERMINAL || "unknown";
739
+ }
740
+ function getShell() {
741
+ const shell = process.env.SHELL || "";
742
+ const shellName = shell.split("/").pop() || "unknown";
743
+ return shellName;
744
+ }
745
+ function trackSessionStarted() {
746
+ const properties = {
747
+ cli_version: "",
748
+ // 由 client 自动填充
749
+ node_version: process.version,
750
+ os: process.platform,
751
+ os_version: os3.release(),
752
+ shell: getShell(),
753
+ is_ci: isCI(),
754
+ terminal: getTerminal()
755
+ };
756
+ capture(EventNames.SESSION_STARTED, properties);
757
+ }
758
+ function trackConfigChanged(key, action, isProfile = false, profileCount) {
759
+ const sensitiveKeys = ["token", "default-token"];
760
+ const safeKey = sensitiveKeys.includes(key.toLowerCase()) ? "[redacted]" : key;
761
+ const properties = {
762
+ key: safeKey,
763
+ action,
764
+ is_profile: isProfile,
765
+ profile_count: profileCount
766
+ };
767
+ capture(EventNames.CONFIG_CHANGED, properties);
768
+ }
769
+
770
+ // src/commands/config/index.ts
565
771
  function createConfigCommand() {
566
772
  const configCmd = new Command2("config").description("\u914D\u7F6E\u7BA1\u7406");
567
773
  configCmd.command("set <key> <value>").description("\u8BBE\u7F6E\u914D\u7F6E\u9879").option("--config <path>", "\u81EA\u5B9A\u4E49\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--pretty", "\u683C\u5F0F\u5316\u8F93\u51FA JSON\uFF08\u9ED8\u8BA4\u538B\u7F29\uFF09").action((key, value, options) => {
568
774
  setConfigValue(key, value, options.config);
775
+ trackConfigChanged(key, "set", false);
569
776
  outputSuccess({
570
777
  message: `\u914D\u7F6E\u9879 ${key} \u5DF2\u8BBE\u7F6E`,
571
778
  key,
@@ -601,6 +808,8 @@ function createConfigCommand() {
601
808
  process.exit(1);
602
809
  }
603
810
  setProfile(name, profile, options.config);
811
+ const profileCount = listProfiles(options.config).length;
812
+ trackConfigChanged(name, "set", true, profileCount);
604
813
  outputSuccess({
605
814
  message: `Profile ${name} \u5DF2\u6DFB\u52A0`,
606
815
  name,
@@ -611,6 +820,8 @@ function createConfigCommand() {
611
820
  profileCmd.command("remove <name>").description("\u5220\u9664 profile").option("--config <path>", "\u81EA\u5B9A\u4E49\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--pretty", "\u683C\u5F0F\u5316\u8F93\u51FA JSON\uFF08\u9ED8\u8BA4\u538B\u7F29\uFF09").action((name, options) => {
612
821
  const removed = removeProfile(name, options.config);
613
822
  if (removed) {
823
+ const profileCount = listProfiles(options.config).length;
824
+ trackConfigChanged(name, "delete", true, profileCount);
614
825
  outputSuccess({ message: `Profile ${name} \u5DF2\u5220\u9664`, name }, options.pretty);
615
826
  } else {
616
827
  outputError(`Profile ${name} \u4E0D\u5B58\u5728`, options.pretty);
@@ -634,6 +845,98 @@ function createConfigCommand() {
634
845
 
635
846
  // src/commands/issue/index.ts
636
847
  import { Command as Command3 } from "commander";
848
+ import { readFileSync as readFileSync3 } from "fs";
849
+
850
+ // src/utils/markdown-parser.ts
851
+ function extractHours(text) {
852
+ const daysRegex = /[((][^))]*?([\d.]+)d[^))]*[))]/;
853
+ const daysMatch = text.match(daysRegex);
854
+ if (daysMatch) {
855
+ const value = parseFloat(daysMatch[1]);
856
+ return isNaN(value) ? void 0 : value;
857
+ }
858
+ const chineseDaysRegex = /[((]([\d.]+)天[))]/;
859
+ const chineseDaysMatch = text.match(chineseDaysRegex);
860
+ if (chineseDaysMatch) {
861
+ const value = parseFloat(chineseDaysMatch[1]);
862
+ return isNaN(value) ? void 0 : value;
863
+ }
864
+ const hoursRegex = /[((]([\d.]+)[))]/;
865
+ const hoursMatch = text.match(hoursRegex);
866
+ if (hoursMatch) {
867
+ const value = parseFloat(hoursMatch[1]);
868
+ return isNaN(value) ? void 0 : value;
869
+ }
870
+ return void 0;
871
+ }
872
+ function parseMarkdownLine(line) {
873
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length || 0;
874
+ const level = Math.floor(leadingSpaces / 2);
875
+ const cleanLine = line.trimStart().replace(/^[*-]\s*/, "").trim();
876
+ const estimatedHours = extractHours(cleanLine);
877
+ const subject = cleanLine.replace(/\s*[((][^))]*[))]/g, "").trim();
878
+ return { level, subject, estimatedHours };
879
+ }
880
+ function parseMarkdownToNodes(markdown) {
881
+ const lines = markdown.trim().split("\n").filter((line) => line.trim().length > 0).filter((line) => /^\s*[*-]/.test(line));
882
+ const rootNodes = [];
883
+ const nodeStack = [];
884
+ for (const line of lines) {
885
+ const { level, subject, estimatedHours } = parseMarkdownLine(line);
886
+ if (!subject) {
887
+ continue;
888
+ }
889
+ const node = {
890
+ subject,
891
+ estimatedHours,
892
+ children: []
893
+ };
894
+ while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1].level >= level) {
895
+ nodeStack.pop();
896
+ }
897
+ if (nodeStack.length === 0) {
898
+ rootNodes.push(node);
899
+ } else {
900
+ nodeStack[nodeStack.length - 1].node.children.push(node);
901
+ }
902
+ nodeStack.push({ level, node });
903
+ }
904
+ return rootNodes;
905
+ }
906
+ function countTasks(nodes) {
907
+ let count = 0;
908
+ for (const node of nodes) {
909
+ count += 1;
910
+ count += countTasks(node.children);
911
+ }
912
+ return count;
913
+ }
914
+ function sumEstimatedHours(nodes) {
915
+ let total = 0;
916
+ for (const node of nodes) {
917
+ if (node.estimatedHours) {
918
+ total += node.estimatedHours;
919
+ }
920
+ total += sumEstimatedHours(node.children);
921
+ }
922
+ return total;
923
+ }
924
+ function formatTaskTree(nodes, prefix = "", _isLast = true, showAssignee = false, assigneeName) {
925
+ const lines = [];
926
+ nodes.forEach((node, index) => {
927
+ const isLastNode = index === nodes.length - 1;
928
+ const connector = prefix === "" ? "* " : isLastNode ? "\u2514\u2500 " : "\u251C\u2500 ";
929
+ const hoursStr = node.estimatedHours !== void 0 ? ` (${node.estimatedHours}d)` : "";
930
+ const assigneeStr = showAssignee && assigneeName ? ` \u2192 ${assigneeName}` : "";
931
+ lines.push(`${prefix}${connector}${node.subject}${hoursStr}${assigneeStr}`);
932
+ if (node.children.length > 0) {
933
+ const childPrefix = prefix === "" ? " " : prefix + (isLastNode ? " " : "\u2502 ");
934
+ const childLines = formatTaskTree(node.children, childPrefix, isLastNode, showAssignee, assigneeName);
935
+ lines.push(childLines);
936
+ }
937
+ });
938
+ return lines.join("\n");
939
+ }
637
940
 
638
941
  // src/services/issue-service.ts
639
942
  function sleep(ms) {
@@ -903,6 +1206,7 @@ var IssueService = class {
903
1206
  skipExisting
904
1207
  });
905
1208
  const result = {
1209
+ totalTasks: 0,
906
1210
  totalCreated: 0,
907
1211
  totalSkipped: 0,
908
1212
  totalFailed: 0,
@@ -920,6 +1224,8 @@ var IssueService = class {
920
1224
  };
921
1225
  }
922
1226
  const sourceIssue = sourceResult.data;
1227
+ result.totalTasks = this.countAllChildren(sourceIssue);
1228
+ logger_default.info(`\u6E90\u7236\u5355\u5171\u6709 ${result.totalTasks} \u4E2A\u5B50\u4EFB\u52A1`);
923
1229
  logger_default.info("\u6B63\u5728\u83B7\u53D6\u76EE\u6807\u7236\u5355\u4FE1\u606F...");
924
1230
  const targetResult = await this.getIssue(token, host, project, targetParentId, false, false);
925
1231
  if (!targetResult.success || !targetResult.data) {
@@ -965,6 +1271,17 @@ var IssueService = class {
965
1271
  };
966
1272
  }
967
1273
  }
1274
+ /**
1275
+ * 计算所有子任务数量(递归)
1276
+ */
1277
+ countAllChildren(issue) {
1278
+ let count = 0;
1279
+ for (const child of issue.children || []) {
1280
+ count++;
1281
+ count += this.countAllChildren(child);
1282
+ }
1283
+ return count;
1284
+ }
968
1285
  /**
969
1286
  * 收集所有任务名称(递归)
970
1287
  */
@@ -994,6 +1311,8 @@ var IssueService = class {
994
1311
  subject: taskName,
995
1312
  reason: "\u76EE\u6807\u7236\u5355\u4E0B\u5DF2\u5B58\u5728\u540C\u540D\u4EFB\u52A1"
996
1313
  });
1314
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1315
+ console.error(`[${progress}/${result.totalTasks}] \u23ED\uFE0F \u8DF3\u8FC7: #${sourceId} ${taskName} (\u5DF2\u5B58\u5728)`);
997
1316
  continue;
998
1317
  }
999
1318
  const isLeafNode = !child.children || child.children.length === 0;
@@ -1083,6 +1402,8 @@ var IssueService = class {
1083
1402
  subject: taskName,
1084
1403
  parentId: targetParentId
1085
1404
  });
1405
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1406
+ console.error(`[${progress}/${result.totalTasks}] \u2705 \u540C\u6B65\u6210\u529F: #${newId} \u2190 #${sourceId} ${taskName}`);
1086
1407
  if (child.children && child.children.length > 0) {
1087
1408
  logger_default.info(`\u{1F4C1} \u53D1\u73B0\u5B50\u4EFB\u52A1 ${newId} \u6709 ${child.children.length} \u4E2A\u5B50\u4EFB\u52A1\uFF0C\u7EE7\u7EED\u9012\u5F52\u521B\u5EFA...`);
1088
1409
  await this.syncChildrenRecursive(
@@ -1099,27 +1420,241 @@ var IssueService = class {
1099
1420
  );
1100
1421
  }
1101
1422
  } else {
1102
- logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, createResult.message || createResult.api_error_msg);
1423
+ const errorMsg = createResult.message || createResult.api_error_msg || "\u521B\u5EFA\u5931\u8D25";
1424
+ logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, errorMsg);
1103
1425
  result.totalFailed++;
1104
1426
  result.failed.push({
1105
1427
  sourceId,
1106
1428
  subject: taskName,
1107
- error: createResult.message || createResult.api_error_msg || "\u521B\u5EFA\u5931\u8D25"
1429
+ error: errorMsg
1108
1430
  });
1431
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1432
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u540C\u6B65\u5931\u8D25: #${sourceId} ${taskName} - ${errorMsg}`);
1109
1433
  }
1110
1434
  } catch (error) {
1435
+ const errorMsg = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
1111
1436
  logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u65F6\u53D1\u751F\u9519\u8BEF: ${taskName}`, error);
1112
1437
  result.totalFailed++;
1113
1438
  result.failed.push({
1114
1439
  sourceId,
1115
1440
  subject: taskName,
1116
- error: error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"
1441
+ error: errorMsg
1117
1442
  });
1443
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1444
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u540C\u6B65\u5931\u8D25: #${sourceId} ${taskName} - ${errorMsg}`);
1118
1445
  }
1119
1446
  await sleep(500);
1120
1447
  }
1121
1448
  }
1122
1449
  }
1450
+ /**
1451
+ * 从 Markdown 批量创建子单
1452
+ * @param parentId 父单 ID
1453
+ * @param markdown Markdown 文本
1454
+ * @param assignedToMail 指派人邮箱
1455
+ * @param options 选项
1456
+ */
1457
+ async batchCreateFromMarkdown(token, host, project, parentId, markdown, assignedToMail, options) {
1458
+ const { dryRun = false, interval = 5e3 } = options || {};
1459
+ logger_default.info("\u5F00\u59CB\u6279\u91CF\u521B\u5EFA\u5B50\u5355", {
1460
+ parentId,
1461
+ assignedToMail,
1462
+ dryRun,
1463
+ interval
1464
+ });
1465
+ const result = {
1466
+ totalCreated: 0,
1467
+ totalFailed: 0,
1468
+ totalTasks: 0,
1469
+ totalEstimatedHours: 0,
1470
+ created: [],
1471
+ failed: []
1472
+ };
1473
+ try {
1474
+ const taskNodes = parseMarkdownToNodes(markdown);
1475
+ if (taskNodes.length === 0) {
1476
+ return {
1477
+ success: false,
1478
+ message: "\u6CA1\u6709\u89E3\u6790\u5230\u4EFB\u4F55\u4EFB\u52A1\uFF0C\u8BF7\u68C0\u67E5 Markdown \u683C\u5F0F"
1479
+ };
1480
+ }
1481
+ result.totalTasks = countTasks(taskNodes);
1482
+ result.totalEstimatedHours = sumEstimatedHours(taskNodes);
1483
+ logger_default.info("Markdown \u89E3\u6790\u5B8C\u6210", {
1484
+ totalTasks: result.totalTasks,
1485
+ totalEstimatedHours: result.totalEstimatedHours
1486
+ });
1487
+ logger_default.info("\u6B63\u5728\u83B7\u53D6\u7236\u5355\u4FE1\u606F...");
1488
+ const parentResult = await this.getIssue(token, host, project, parentId, false, false);
1489
+ if (!parentResult.success || !parentResult.data) {
1490
+ return {
1491
+ success: false,
1492
+ message: `\u83B7\u53D6\u7236\u5355 #${parentId} \u5931\u8D25: ${parentResult.message || "\u672A\u77E5\u9519\u8BEF"}`
1493
+ };
1494
+ }
1495
+ const parentInfo = parentResult.data;
1496
+ logger_default.info("\u5F00\u59CB\u9012\u5F52\u521B\u5EFA\u5B50\u5355...");
1497
+ await this.batchCreateRecursive(
1498
+ token,
1499
+ host,
1500
+ project,
1501
+ taskNodes,
1502
+ parentId,
1503
+ parentInfo,
1504
+ assignedToMail,
1505
+ dryRun,
1506
+ interval,
1507
+ result
1508
+ );
1509
+ logger_default.info("\u6279\u91CF\u521B\u5EFA\u5B8C\u6210", {
1510
+ totalCreated: result.totalCreated,
1511
+ totalFailed: result.totalFailed
1512
+ });
1513
+ return { success: true, data: result };
1514
+ } catch (error) {
1515
+ logger_default.error("\u6279\u91CF\u521B\u5EFA\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF", error);
1516
+ return {
1517
+ success: false,
1518
+ message: `\u6279\u91CF\u521B\u5EFA\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF: ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}`
1519
+ };
1520
+ }
1521
+ }
1522
+ /**
1523
+ * 递归批量创建子单
1524
+ */
1525
+ async batchCreateRecursive(token, host, project, taskNodes, targetParentId, parentInfo, assignedToMail, dryRun, interval, result) {
1526
+ for (const node of taskNodes) {
1527
+ const taskName = node.subject;
1528
+ const isLeafNode = node.children.length === 0;
1529
+ const createParams = {
1530
+ token,
1531
+ host,
1532
+ project,
1533
+ parent_issue_id: targetParentId,
1534
+ subject: taskName,
1535
+ // 继承自父单
1536
+ tracker: parentInfo.tracker?.name,
1537
+ status: "\u65B0\u5EFA",
1538
+ // 指派人
1539
+ assigned_to_mail: assignedToMail,
1540
+ // 只有叶子节点才设置工时
1541
+ estimated_hours: isLeafNode ? node.estimatedHours : void 0
1542
+ };
1543
+ const parentWithVersion = parentInfo;
1544
+ if (parentWithVersion.fixed_version?.name) {
1545
+ createParams.version = parentWithVersion.fixed_version.name;
1546
+ }
1547
+ const parentWithCustomFields = parentInfo;
1548
+ if (parentWithCustomFields.custom_fields && parentWithCustomFields.custom_fields.length > 0) {
1549
+ const customFieldMap = {};
1550
+ const followsMails = [];
1551
+ for (const field of parentWithCustomFields.custom_fields) {
1552
+ if (field.value !== null && field.value !== void 0 && field.value !== "") {
1553
+ if (field.identify === "IssuesQCFollow") {
1554
+ const followsValue = field.value;
1555
+ if (Array.isArray(followsValue)) {
1556
+ for (const item of followsValue) {
1557
+ if (item.user?.mail) {
1558
+ followsMails.push(item.user.mail);
1559
+ }
1560
+ }
1561
+ }
1562
+ } else {
1563
+ customFieldMap[field.id] = field.value;
1564
+ }
1565
+ }
1566
+ }
1567
+ if (Object.keys(customFieldMap).length > 0) {
1568
+ createParams.custom_field = JSON.stringify(customFieldMap);
1569
+ }
1570
+ if (followsMails.length > 0) {
1571
+ createParams.follows = followsMails;
1572
+ }
1573
+ }
1574
+ logger_default.info(`\u6B63\u5728\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`, { targetParentId, isLeafNode, estimatedHours: node.estimatedHours });
1575
+ if (dryRun) {
1576
+ logger_default.info(`[\u6A21\u62DF] \u5C06\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`);
1577
+ result.totalCreated++;
1578
+ result.created.push({
1579
+ newId: 0,
1580
+ // 模拟模式没有真实 ID
1581
+ subject: taskName,
1582
+ parentId: targetParentId,
1583
+ estimatedHours: node.estimatedHours
1584
+ });
1585
+ if (node.children.length > 0) {
1586
+ await this.batchCreateRecursive(
1587
+ token,
1588
+ host,
1589
+ project,
1590
+ node.children,
1591
+ 0,
1592
+ // 模拟模式下没有真实的新 ID
1593
+ parentInfo,
1594
+ assignedToMail,
1595
+ dryRun,
1596
+ interval,
1597
+ result
1598
+ );
1599
+ }
1600
+ } else {
1601
+ try {
1602
+ const createResult = await this.createIssue(createParams);
1603
+ if (createResult.success && createResult.data) {
1604
+ const newId = createResult.data.id;
1605
+ logger_default.info(`\u2705 \u6210\u529F\u521B\u5EFA\u5B50\u4EFB\u52A1: ID=${newId}, \u6807\u9898=${taskName}`);
1606
+ result.totalCreated++;
1607
+ result.created.push({
1608
+ newId,
1609
+ subject: taskName,
1610
+ parentId: targetParentId,
1611
+ estimatedHours: node.estimatedHours
1612
+ });
1613
+ const progress = result.totalCreated + result.totalFailed;
1614
+ console.error(`[${progress}/${result.totalTasks}] \u2705 \u521B\u5EFA\u6210\u529F: #${newId} ${taskName}`);
1615
+ if (node.children.length > 0) {
1616
+ logger_default.info(`\u{1F4C1} \u53D1\u73B0\u5B50\u4EFB\u52A1 ${newId} \u6709 ${node.children.length} \u4E2A\u5B50\u4EFB\u52A1\uFF0C\u7EE7\u7EED\u9012\u5F52\u521B\u5EFA...`);
1617
+ await this.batchCreateRecursive(
1618
+ token,
1619
+ host,
1620
+ project,
1621
+ node.children,
1622
+ newId,
1623
+ parentInfo,
1624
+ assignedToMail,
1625
+ dryRun,
1626
+ interval,
1627
+ result
1628
+ );
1629
+ }
1630
+ } else {
1631
+ const errorMsg = createResult.message || createResult.api_error_msg || "\u521B\u5EFA\u5931\u8D25";
1632
+ logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, errorMsg);
1633
+ result.totalFailed++;
1634
+ result.failed.push({
1635
+ subject: taskName,
1636
+ parentId: targetParentId,
1637
+ error: errorMsg
1638
+ });
1639
+ const progress = result.totalCreated + result.totalFailed;
1640
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u521B\u5EFA\u5931\u8D25: ${taskName} - ${errorMsg}`);
1641
+ }
1642
+ } catch (error) {
1643
+ const errorMsg = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
1644
+ logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u65F6\u53D1\u751F\u9519\u8BEF: ${taskName}`, error);
1645
+ result.totalFailed++;
1646
+ result.failed.push({
1647
+ subject: taskName,
1648
+ parentId: targetParentId,
1649
+ error: errorMsg
1650
+ });
1651
+ const progress = result.totalCreated + result.totalFailed;
1652
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u521B\u5EFA\u5931\u8D25: ${taskName} - ${errorMsg}`);
1653
+ }
1654
+ await sleep(interval);
1655
+ }
1656
+ }
1657
+ }
1123
1658
  };
1124
1659
  var issueService = new IssueService();
1125
1660
 
@@ -1149,8 +1684,8 @@ function parsePmLink(url) {
1149
1684
  }
1150
1685
 
1151
1686
  // src/utils/issue-exporter.ts
1152
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, statSync } from "fs";
1153
- import { join as join2 } from "path";
1687
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, statSync } from "fs";
1688
+ import { join as join3 } from "path";
1154
1689
 
1155
1690
  // src/utils/preset-extractor.ts
1156
1691
  var SUMMARY_CUSTOM_FIELD_IDS = [84];
@@ -1265,27 +1800,27 @@ function generateExportDir(command, identifier) {
1265
1800
  async function exportIssuePresets(issue, exportDir, options = {}) {
1266
1801
  const { depth = -1, pretty = false } = options;
1267
1802
  const indent = pretty ? 2 : 0;
1268
- mkdirSync2(exportDir, { recursive: true });
1269
- mkdirSync2(join2(exportDir, "standard"), { recursive: true });
1270
- mkdirSync2(join2(exportDir, "complete"), { recursive: true });
1803
+ mkdirSync3(exportDir, { recursive: true });
1804
+ mkdirSync3(join3(exportDir, "standard"), { recursive: true });
1805
+ mkdirSync3(join3(exportDir, "complete"), { recursive: true });
1271
1806
  const resultFiles = [];
1272
1807
  const summaryTree = generateSummaryTree(issue, 0, depth);
1273
- const summaryPath = join2(exportDir, "tree.summary.json");
1274
- writeFileSync3(summaryPath, JSON.stringify(summaryTree, null, indent), "utf-8");
1808
+ const summaryPath = join3(exportDir, "tree.summary.json");
1809
+ writeFileSync4(summaryPath, JSON.stringify(summaryTree, null, indent), "utf-8");
1275
1810
  const summarySize = statSync(summaryPath).size;
1276
1811
  resultFiles.push({ path: "tree.summary.json", size: summarySize, type: "tree" });
1277
1812
  const allIssues = flattenIssues(issue);
1278
1813
  for (const iss of allIssues) {
1279
1814
  const standardData = extractStandardFields(iss);
1280
- const standardPath = join2(exportDir, "standard", `${iss.id}.json`);
1281
- writeFileSync3(standardPath, JSON.stringify(standardData, null, indent), "utf-8");
1815
+ const standardPath = join3(exportDir, "standard", `${iss.id}.json`);
1816
+ writeFileSync4(standardPath, JSON.stringify(standardData, null, indent), "utf-8");
1282
1817
  const standardSize = statSync(standardPath).size;
1283
1818
  resultFiles.push({ path: `standard/${iss.id}.json`, size: standardSize, type: "standard" });
1284
1819
  }
1285
1820
  for (const iss of allIssues) {
1286
1821
  const completeData = extractCompleteFields(iss);
1287
- const completePath = join2(exportDir, "complete", `${iss.id}.json`);
1288
- writeFileSync3(completePath, JSON.stringify(completeData, null, indent), "utf-8");
1822
+ const completePath = join3(exportDir, "complete", `${iss.id}.json`);
1823
+ writeFileSync4(completePath, JSON.stringify(completeData, null, indent), "utf-8");
1289
1824
  const completeSize = statSync(completePath).size;
1290
1825
  resultFiles.push({ path: `complete/${iss.id}.json`, size: completeSize, type: "complete" });
1291
1826
  }
@@ -1936,6 +2471,117 @@ function createIssueCommand() {
1936
2471
  process.exit(1);
1937
2472
  }
1938
2473
  });
2474
+ issueCmd.command("batch-create").description("\u4ECE Markdown \u6279\u91CF\u521B\u5EFA\u5B50\u5355").option("--parent-id <id>", "\u7236\u5355 ID").option("--parent-url <url>", "\u7236\u5355 PM \u94FE\u63A5").option("--markdown <file>", "Markdown \u6587\u4EF6\u8DEF\u5F84").option("--stdin", "\u4ECE\u6807\u51C6\u8F93\u5165\u8BFB\u53D6 Markdown").option("--assigned-to-mail <email>", "\u6307\u6D3E\u4EBA\u90AE\u7BB1\uFF08\u9ED8\u8BA4\u4F7F\u7528\u914D\u7F6E\u7684 userMail\uFF09").option("--interval <ms>", "\u521B\u5EFA\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09", "5000").option("--dry-run", "\u6A21\u62DF\u8FD0\u884C\uFF0C\u4E0D\u5B9E\u9645\u521B\u5EFA").option("-o, --output <path>", "\u8F93\u51FA JSON \u5230\u6307\u5B9A\u6587\u4EF6").option("--stdout", "\u5F3A\u5236\u8F93\u51FA\u5230\u63A7\u5236\u53F0\u800C\u975E\u6587\u4EF6").option("--token <token>", "API Token").option("--host <host>", "PM \u4E3B\u673A\u5730\u5740").option("--project <project>", "\u9879\u76EE\u540D\u79F0").option("--profile <name>", "\u4F7F\u7528\u914D\u7F6E profile").option("--config <path>", "\u81EA\u5B9A\u4E49\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--pretty", "\u683C\u5F0F\u5316\u8F93\u51FA JSON\uFF08\u9ED8\u8BA4\u538B\u7F29\uFF09").action(async (options) => {
2475
+ let parentId;
2476
+ let parentHost;
2477
+ if (options.parentUrl) {
2478
+ const linkInfo = parsePmLink(options.parentUrl);
2479
+ if (!linkInfo) {
2480
+ outputError("\u65E0\u6548\u7684\u7236\u5355 PM \u94FE\u63A5\u683C\u5F0F", options.pretty);
2481
+ process.exit(1);
2482
+ }
2483
+ parentId = parseInt(linkInfo.issueId, 10);
2484
+ parentHost = linkInfo.host;
2485
+ } else if (options.parentId) {
2486
+ const cleanId = options.parentId.replace(/^#/, "");
2487
+ parentId = parseInt(cleanId, 10);
2488
+ if (isNaN(parentId)) {
2489
+ outputError("\u65E0\u6548\u7684\u7236\u5355 ID", options.pretty);
2490
+ process.exit(1);
2491
+ }
2492
+ }
2493
+ if (!parentId) {
2494
+ outputError("\u8BF7\u63D0\u4F9B\u7236\u5355 ID\uFF08--parent-id\uFF09\u6216 PM \u94FE\u63A5\uFF08--parent-url\uFF09", options.pretty);
2495
+ process.exit(1);
2496
+ }
2497
+ let markdown;
2498
+ if (options.stdin) {
2499
+ const chunks = [];
2500
+ for await (const chunk of process.stdin) {
2501
+ chunks.push(chunk);
2502
+ }
2503
+ markdown = Buffer.concat(chunks).toString("utf-8");
2504
+ } else if (options.markdown) {
2505
+ try {
2506
+ markdown = readFileSync3(options.markdown, "utf-8");
2507
+ } catch {
2508
+ outputError(`\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${options.markdown}`, options.pretty);
2509
+ process.exit(1);
2510
+ }
2511
+ } else {
2512
+ outputError("\u8BF7\u63D0\u4F9B Markdown \u6587\u4EF6\u8DEF\u5F84\uFF08--markdown\uFF09\u6216\u4F7F\u7528\u6807\u51C6\u8F93\u5165\uFF08--stdin\uFF09", options.pretty);
2513
+ process.exit(1);
2514
+ }
2515
+ if (!markdown.trim()) {
2516
+ outputError("Markdown \u5185\u5BB9\u4E3A\u7A7A", options.pretty);
2517
+ process.exit(1);
2518
+ }
2519
+ const taskNodes = parseMarkdownToNodes(markdown);
2520
+ if (taskNodes.length === 0) {
2521
+ outputError("\u6CA1\u6709\u89E3\u6790\u5230\u4EFB\u4F55\u4EFB\u52A1\uFF0C\u8BF7\u68C0\u67E5 Markdown \u683C\u5F0F", options.pretty);
2522
+ process.exit(1);
2523
+ }
2524
+ const creds = resolveCredentials({
2525
+ ...options,
2526
+ host: parentHost || options.host
2527
+ });
2528
+ const validation = validateCredentials(creds, ["token", "host"]);
2529
+ if (!validation.valid) {
2530
+ outputError(`\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570: ${validation.missing.join(", ")}`, options.pretty);
2531
+ process.exit(1);
2532
+ }
2533
+ let assignedToMail = options.assignedToMail;
2534
+ if (!assignedToMail) {
2535
+ const config = readConfig(options.config);
2536
+ assignedToMail = config.default?.userMail;
2537
+ if (options.profile && config.profiles[options.profile]?.userMail) {
2538
+ assignedToMail = config.profiles[options.profile].userMail;
2539
+ }
2540
+ }
2541
+ if (!assignedToMail) {
2542
+ outputError("\u8BF7\u63D0\u4F9B\u6307\u6D3E\u4EBA\u90AE\u7BB1\uFF08--assigned-to-mail\uFF09\u6216\u914D\u7F6E userMail\uFF08pm-cli config set user-mail xxx\uFF09", options.pretty);
2543
+ process.exit(1);
2544
+ }
2545
+ const interval = parseInt(options.interval, 10);
2546
+ const dryRun = options.dryRun || false;
2547
+ const result = await issueService.batchCreateFromMarkdown(
2548
+ creds.token,
2549
+ creds.host,
2550
+ creds.project || "",
2551
+ parentId,
2552
+ markdown,
2553
+ assignedToMail,
2554
+ { dryRun, interval }
2555
+ );
2556
+ if (result.success && result.data) {
2557
+ const output = {
2558
+ success: true,
2559
+ dryRun,
2560
+ parentId,
2561
+ assignedToMail,
2562
+ summary: {
2563
+ totalTasks: result.data.totalTasks,
2564
+ totalEstimatedHours: result.data.totalEstimatedHours,
2565
+ totalCreated: result.data.totalCreated,
2566
+ totalFailed: result.data.totalFailed
2567
+ },
2568
+ // 树形结构预览
2569
+ taskTree: formatTaskTree(taskNodes, "", true, true, assignedToMail.split("@")[0]),
2570
+ created: result.data.created,
2571
+ failed: result.data.failed.length > 0 ? result.data.failed : void 0
2572
+ };
2573
+ smartOutput(output, {
2574
+ stdout: options.stdout,
2575
+ output: options.output,
2576
+ command: "issue-batch-create",
2577
+ identifier: `${parentId}`,
2578
+ pretty: options.pretty
2579
+ });
2580
+ } else {
2581
+ outputError(result.message || result.msg || result.api_error_msg || "\u6279\u91CF\u521B\u5EFA\u5931\u8D25", options.pretty);
2582
+ process.exit(1);
2583
+ }
2584
+ });
1939
2585
  return issueCmd;
1940
2586
  }
1941
2587
 
@@ -2330,6 +2976,9 @@ function createTimeCommand() {
2330
2976
  const configUserId = getConfigValue("userId", options.config);
2331
2977
  if (configUserId) {
2332
2978
  userId = parseInt(configUserId, 10);
2979
+ } else {
2980
+ outputError("\u672A\u914D\u7F6E\u7528\u6237 ID\uFF0C\u8BF7\u5148\u8BBE\u7F6E: pm-cli config set user-id <\u60A8\u7684\u7528\u6237ID>", options.pretty);
2981
+ process.exit(1);
2333
2982
  }
2334
2983
  }
2335
2984
  logger_default.info(`\u67E5\u8BE2\u5DE5\u65F6\u7EDF\u8BA1: ${periodLabel}`);
@@ -2454,15 +3103,32 @@ function createProjectCommand() {
2454
3103
  }
2455
3104
 
2456
3105
  // src/index.ts
2457
- import { readFileSync as readFileSync2 } from "fs";
2458
- import { dirname as dirname2, join as join3 } from "path";
3106
+ import { readFileSync as readFileSync4 } from "fs";
3107
+ import { dirname as dirname2, join as join4 } from "path";
2459
3108
  import { fileURLToPath } from "url";
2460
3109
  var __filename = fileURLToPath(import.meta.url);
2461
3110
  var __dirname = dirname2(__filename);
2462
- var packageJson = JSON.parse(readFileSync2(join3(__dirname, "../package.json"), "utf-8"));
3111
+ var packageJson = JSON.parse(readFileSync4(join4(__dirname, "../package.json"), "utf-8"));
2463
3112
  var version = packageJson.version;
3113
+ initTelemetry(version);
3114
+ trackSessionStarted();
3115
+ function extractCommandNames(cmd) {
3116
+ const names = [];
3117
+ let current = cmd;
3118
+ while (current) {
3119
+ const name = current.name();
3120
+ if (name && name !== "pm-cli") {
3121
+ names.unshift(name);
3122
+ }
3123
+ current = current.parent;
3124
+ }
3125
+ return {
3126
+ command: names[0] || "unknown",
3127
+ subcommand: names.slice(1).join(":") || ""
3128
+ };
3129
+ }
2464
3130
  var program = new Command6();
2465
- program.name("pm-cli").description("\u7F51\u6613\u6613\u534F\u4F5C (PM NetEase) \u547D\u4EE4\u884C\u5DE5\u5177").version(version).option("--verbose", "\u8BE6\u7EC6\u8F93\u51FA").option("--debug", "\u8C03\u8BD5\u6A21\u5F0F").option("--no-mapping", "\u4E0D\u8F93\u51FA key \u6620\u5C04\u8868\uFF08\u9ED8\u8BA4\u8F93\u51FA\uFF09").hook("preAction", (thisCommand) => {
3131
+ program.name("pm-cli").description("\u7F51\u6613\u6613\u534F\u4F5C (PM NetEase) \u547D\u4EE4\u884C\u5DE5\u5177").version(version).option("--verbose", "\u8BE6\u7EC6\u8F93\u51FA").option("--debug", "\u8C03\u8BD5\u6A21\u5F0F").option("--no-mapping", "\u4E0D\u8F93\u51FA key \u6620\u5C04\u8868\uFF08\u9ED8\u8BA4\u8F93\u51FA\uFF09").hook("preAction", (thisCommand, actionCommand) => {
2466
3132
  const opts = thisCommand.opts();
2467
3133
  if (opts.debug) {
2468
3134
  logger_default.setLevel("debug");
@@ -2474,11 +3140,33 @@ program.name("pm-cli").description("\u7F51\u6613\u6613\u534F\u4F5C (PM NetEase)
2474
3140
  if (opts.mapping === false) {
2475
3141
  setKeyMappingEnabled(false);
2476
3142
  }
3143
+ const { command, subcommand } = extractCommandNames(actionCommand);
3144
+ const actionOpts = actionCommand.opts();
3145
+ startCommandTracking(command, subcommand, {
3146
+ has_profile: !!actionOpts.profile,
3147
+ has_url_input: !!actionOpts.url,
3148
+ depth: actionOpts.depth ? parseInt(actionOpts.depth, 10) : void 0,
3149
+ output_mode: actionOpts.stdout ? "stdout" : actionOpts.raw ? "raw" : "file"
3150
+ });
3151
+ }).hook("postAction", () => {
3152
+ endCommandTracking();
2477
3153
  });
2478
3154
  program.addCommand(createTestCommand());
2479
3155
  program.addCommand(createConfigCommand());
2480
3156
  program.addCommand(createIssueCommand());
2481
3157
  program.addCommand(createTimeCommand());
2482
3158
  program.addCommand(createProjectCommand());
2483
- program.parse();
3159
+ async function main() {
3160
+ try {
3161
+ await program.parseAsync();
3162
+ } catch (error) {
3163
+ failCommandTracking(error);
3164
+ throw error;
3165
+ } finally {
3166
+ await shutdownTelemetry();
3167
+ }
3168
+ }
3169
+ main().catch(() => {
3170
+ process.exit(1);
3171
+ });
2484
3172
  //# sourceMappingURL=index.js.map