@hangox/pm-cli 0.2.5 → 1.1.0

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,92 @@ 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 hoursRegex = /[((]([\d.]+)[))]/;
859
+ const hoursMatch = text.match(hoursRegex);
860
+ if (hoursMatch) {
861
+ const value = parseFloat(hoursMatch[1]);
862
+ return isNaN(value) ? void 0 : value;
863
+ }
864
+ return void 0;
865
+ }
866
+ function parseMarkdownLine(line) {
867
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length || 0;
868
+ const level = Math.floor(leadingSpaces / 2);
869
+ const cleanLine = line.trimStart().replace(/^[*-]\s*/, "").trim();
870
+ const estimatedHours = extractHours(cleanLine);
871
+ const subject = cleanLine.replace(/\s*[((][^))]*[))]/g, "").trim();
872
+ return { level, subject, estimatedHours };
873
+ }
874
+ function parseMarkdownToNodes(markdown) {
875
+ const lines = markdown.trim().split("\n").filter((line) => line.trim().length > 0).filter((line) => /^\s*[*-]/.test(line));
876
+ const rootNodes = [];
877
+ const nodeStack = [];
878
+ for (const line of lines) {
879
+ const { level, subject, estimatedHours } = parseMarkdownLine(line);
880
+ if (!subject) {
881
+ continue;
882
+ }
883
+ const node = {
884
+ subject,
885
+ estimatedHours,
886
+ children: []
887
+ };
888
+ while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1].level >= level) {
889
+ nodeStack.pop();
890
+ }
891
+ if (nodeStack.length === 0) {
892
+ rootNodes.push(node);
893
+ } else {
894
+ nodeStack[nodeStack.length - 1].node.children.push(node);
895
+ }
896
+ nodeStack.push({ level, node });
897
+ }
898
+ return rootNodes;
899
+ }
900
+ function countTasks(nodes) {
901
+ let count = 0;
902
+ for (const node of nodes) {
903
+ count += 1;
904
+ count += countTasks(node.children);
905
+ }
906
+ return count;
907
+ }
908
+ function sumEstimatedHours(nodes) {
909
+ let total = 0;
910
+ for (const node of nodes) {
911
+ if (node.estimatedHours) {
912
+ total += node.estimatedHours;
913
+ }
914
+ total += sumEstimatedHours(node.children);
915
+ }
916
+ return total;
917
+ }
918
+ function formatTaskTree(nodes, prefix = "", _isLast = true, showAssignee = false, assigneeName) {
919
+ const lines = [];
920
+ nodes.forEach((node, index) => {
921
+ const isLastNode = index === nodes.length - 1;
922
+ const connector = prefix === "" ? "* " : isLastNode ? "\u2514\u2500 " : "\u251C\u2500 ";
923
+ const hoursStr = node.estimatedHours !== void 0 ? ` (${node.estimatedHours}d)` : "";
924
+ const assigneeStr = showAssignee && assigneeName ? ` \u2192 ${assigneeName}` : "";
925
+ lines.push(`${prefix}${connector}${node.subject}${hoursStr}${assigneeStr}`);
926
+ if (node.children.length > 0) {
927
+ const childPrefix = prefix === "" ? " " : prefix + (isLastNode ? " " : "\u2502 ");
928
+ const childLines = formatTaskTree(node.children, childPrefix, isLastNode, showAssignee, assigneeName);
929
+ lines.push(childLines);
930
+ }
931
+ });
932
+ return lines.join("\n");
933
+ }
637
934
 
638
935
  // src/services/issue-service.ts
639
936
  function sleep(ms) {
@@ -903,6 +1200,7 @@ var IssueService = class {
903
1200
  skipExisting
904
1201
  });
905
1202
  const result = {
1203
+ totalTasks: 0,
906
1204
  totalCreated: 0,
907
1205
  totalSkipped: 0,
908
1206
  totalFailed: 0,
@@ -920,6 +1218,8 @@ var IssueService = class {
920
1218
  };
921
1219
  }
922
1220
  const sourceIssue = sourceResult.data;
1221
+ result.totalTasks = this.countAllChildren(sourceIssue);
1222
+ logger_default.info(`\u6E90\u7236\u5355\u5171\u6709 ${result.totalTasks} \u4E2A\u5B50\u4EFB\u52A1`);
923
1223
  logger_default.info("\u6B63\u5728\u83B7\u53D6\u76EE\u6807\u7236\u5355\u4FE1\u606F...");
924
1224
  const targetResult = await this.getIssue(token, host, project, targetParentId, false, false);
925
1225
  if (!targetResult.success || !targetResult.data) {
@@ -965,6 +1265,17 @@ var IssueService = class {
965
1265
  };
966
1266
  }
967
1267
  }
1268
+ /**
1269
+ * 计算所有子任务数量(递归)
1270
+ */
1271
+ countAllChildren(issue) {
1272
+ let count = 0;
1273
+ for (const child of issue.children || []) {
1274
+ count++;
1275
+ count += this.countAllChildren(child);
1276
+ }
1277
+ return count;
1278
+ }
968
1279
  /**
969
1280
  * 收集所有任务名称(递归)
970
1281
  */
@@ -994,6 +1305,8 @@ var IssueService = class {
994
1305
  subject: taskName,
995
1306
  reason: "\u76EE\u6807\u7236\u5355\u4E0B\u5DF2\u5B58\u5728\u540C\u540D\u4EFB\u52A1"
996
1307
  });
1308
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1309
+ console.error(`[${progress}/${result.totalTasks}] \u23ED\uFE0F \u8DF3\u8FC7: #${sourceId} ${taskName} (\u5DF2\u5B58\u5728)`);
997
1310
  continue;
998
1311
  }
999
1312
  const isLeafNode = !child.children || child.children.length === 0;
@@ -1083,6 +1396,8 @@ var IssueService = class {
1083
1396
  subject: taskName,
1084
1397
  parentId: targetParentId
1085
1398
  });
1399
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1400
+ console.error(`[${progress}/${result.totalTasks}] \u2705 \u540C\u6B65\u6210\u529F: #${newId} \u2190 #${sourceId} ${taskName}`);
1086
1401
  if (child.children && child.children.length > 0) {
1087
1402
  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
1403
  await this.syncChildrenRecursive(
@@ -1099,27 +1414,241 @@ var IssueService = class {
1099
1414
  );
1100
1415
  }
1101
1416
  } else {
1102
- logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, createResult.message || createResult.api_error_msg);
1417
+ const errorMsg = createResult.message || createResult.api_error_msg || "\u521B\u5EFA\u5931\u8D25";
1418
+ logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, errorMsg);
1103
1419
  result.totalFailed++;
1104
1420
  result.failed.push({
1105
1421
  sourceId,
1106
1422
  subject: taskName,
1107
- error: createResult.message || createResult.api_error_msg || "\u521B\u5EFA\u5931\u8D25"
1423
+ error: errorMsg
1108
1424
  });
1425
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1426
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u540C\u6B65\u5931\u8D25: #${sourceId} ${taskName} - ${errorMsg}`);
1109
1427
  }
1110
1428
  } catch (error) {
1429
+ const errorMsg = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
1111
1430
  logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u65F6\u53D1\u751F\u9519\u8BEF: ${taskName}`, error);
1112
1431
  result.totalFailed++;
1113
1432
  result.failed.push({
1114
1433
  sourceId,
1115
1434
  subject: taskName,
1116
- error: error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"
1435
+ error: errorMsg
1117
1436
  });
1437
+ const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
1438
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u540C\u6B65\u5931\u8D25: #${sourceId} ${taskName} - ${errorMsg}`);
1118
1439
  }
1119
1440
  await sleep(500);
1120
1441
  }
1121
1442
  }
1122
1443
  }
1444
+ /**
1445
+ * 从 Markdown 批量创建子单
1446
+ * @param parentId 父单 ID
1447
+ * @param markdown Markdown 文本
1448
+ * @param assignedToMail 指派人邮箱
1449
+ * @param options 选项
1450
+ */
1451
+ async batchCreateFromMarkdown(token, host, project, parentId, markdown, assignedToMail, options) {
1452
+ const { dryRun = false, interval = 5e3 } = options || {};
1453
+ logger_default.info("\u5F00\u59CB\u6279\u91CF\u521B\u5EFA\u5B50\u5355", {
1454
+ parentId,
1455
+ assignedToMail,
1456
+ dryRun,
1457
+ interval
1458
+ });
1459
+ const result = {
1460
+ totalCreated: 0,
1461
+ totalFailed: 0,
1462
+ totalTasks: 0,
1463
+ totalEstimatedHours: 0,
1464
+ created: [],
1465
+ failed: []
1466
+ };
1467
+ try {
1468
+ const taskNodes = parseMarkdownToNodes(markdown);
1469
+ if (taskNodes.length === 0) {
1470
+ return {
1471
+ success: false,
1472
+ message: "\u6CA1\u6709\u89E3\u6790\u5230\u4EFB\u4F55\u4EFB\u52A1\uFF0C\u8BF7\u68C0\u67E5 Markdown \u683C\u5F0F"
1473
+ };
1474
+ }
1475
+ result.totalTasks = countTasks(taskNodes);
1476
+ result.totalEstimatedHours = sumEstimatedHours(taskNodes);
1477
+ logger_default.info("Markdown \u89E3\u6790\u5B8C\u6210", {
1478
+ totalTasks: result.totalTasks,
1479
+ totalEstimatedHours: result.totalEstimatedHours
1480
+ });
1481
+ logger_default.info("\u6B63\u5728\u83B7\u53D6\u7236\u5355\u4FE1\u606F...");
1482
+ const parentResult = await this.getIssue(token, host, project, parentId, false, false);
1483
+ if (!parentResult.success || !parentResult.data) {
1484
+ return {
1485
+ success: false,
1486
+ message: `\u83B7\u53D6\u7236\u5355 #${parentId} \u5931\u8D25: ${parentResult.message || "\u672A\u77E5\u9519\u8BEF"}`
1487
+ };
1488
+ }
1489
+ const parentInfo = parentResult.data;
1490
+ logger_default.info("\u5F00\u59CB\u9012\u5F52\u521B\u5EFA\u5B50\u5355...");
1491
+ await this.batchCreateRecursive(
1492
+ token,
1493
+ host,
1494
+ project,
1495
+ taskNodes,
1496
+ parentId,
1497
+ parentInfo,
1498
+ assignedToMail,
1499
+ dryRun,
1500
+ interval,
1501
+ result
1502
+ );
1503
+ logger_default.info("\u6279\u91CF\u521B\u5EFA\u5B8C\u6210", {
1504
+ totalCreated: result.totalCreated,
1505
+ totalFailed: result.totalFailed
1506
+ });
1507
+ return { success: true, data: result };
1508
+ } catch (error) {
1509
+ logger_default.error("\u6279\u91CF\u521B\u5EFA\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF", error);
1510
+ return {
1511
+ success: false,
1512
+ message: `\u6279\u91CF\u521B\u5EFA\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF: ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}`
1513
+ };
1514
+ }
1515
+ }
1516
+ /**
1517
+ * 递归批量创建子单
1518
+ */
1519
+ async batchCreateRecursive(token, host, project, taskNodes, targetParentId, parentInfo, assignedToMail, dryRun, interval, result) {
1520
+ for (const node of taskNodes) {
1521
+ const taskName = node.subject;
1522
+ const isLeafNode = node.children.length === 0;
1523
+ const createParams = {
1524
+ token,
1525
+ host,
1526
+ project,
1527
+ parent_issue_id: targetParentId,
1528
+ subject: taskName,
1529
+ // 继承自父单
1530
+ tracker: parentInfo.tracker?.name,
1531
+ status: "\u65B0\u5EFA",
1532
+ // 指派人
1533
+ assigned_to_mail: assignedToMail,
1534
+ // 只有叶子节点才设置工时
1535
+ estimated_hours: isLeafNode ? node.estimatedHours : void 0
1536
+ };
1537
+ const parentWithVersion = parentInfo;
1538
+ if (parentWithVersion.fixed_version?.name) {
1539
+ createParams.version = parentWithVersion.fixed_version.name;
1540
+ }
1541
+ const parentWithCustomFields = parentInfo;
1542
+ if (parentWithCustomFields.custom_fields && parentWithCustomFields.custom_fields.length > 0) {
1543
+ const customFieldMap = {};
1544
+ const followsMails = [];
1545
+ for (const field of parentWithCustomFields.custom_fields) {
1546
+ if (field.value !== null && field.value !== void 0 && field.value !== "") {
1547
+ if (field.identify === "IssuesQCFollow") {
1548
+ const followsValue = field.value;
1549
+ if (Array.isArray(followsValue)) {
1550
+ for (const item of followsValue) {
1551
+ if (item.user?.mail) {
1552
+ followsMails.push(item.user.mail);
1553
+ }
1554
+ }
1555
+ }
1556
+ } else {
1557
+ customFieldMap[field.id] = field.value;
1558
+ }
1559
+ }
1560
+ }
1561
+ if (Object.keys(customFieldMap).length > 0) {
1562
+ createParams.custom_field = JSON.stringify(customFieldMap);
1563
+ }
1564
+ if (followsMails.length > 0) {
1565
+ createParams.follows = followsMails;
1566
+ }
1567
+ }
1568
+ logger_default.info(`\u6B63\u5728\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`, { targetParentId, isLeafNode, estimatedHours: node.estimatedHours });
1569
+ if (dryRun) {
1570
+ logger_default.info(`[\u6A21\u62DF] \u5C06\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`);
1571
+ result.totalCreated++;
1572
+ result.created.push({
1573
+ newId: 0,
1574
+ // 模拟模式没有真实 ID
1575
+ subject: taskName,
1576
+ parentId: targetParentId,
1577
+ estimatedHours: node.estimatedHours
1578
+ });
1579
+ if (node.children.length > 0) {
1580
+ await this.batchCreateRecursive(
1581
+ token,
1582
+ host,
1583
+ project,
1584
+ node.children,
1585
+ 0,
1586
+ // 模拟模式下没有真实的新 ID
1587
+ parentInfo,
1588
+ assignedToMail,
1589
+ dryRun,
1590
+ interval,
1591
+ result
1592
+ );
1593
+ }
1594
+ } else {
1595
+ try {
1596
+ const createResult = await this.createIssue(createParams);
1597
+ if (createResult.success && createResult.data) {
1598
+ const newId = createResult.data.id;
1599
+ logger_default.info(`\u2705 \u6210\u529F\u521B\u5EFA\u5B50\u4EFB\u52A1: ID=${newId}, \u6807\u9898=${taskName}`);
1600
+ result.totalCreated++;
1601
+ result.created.push({
1602
+ newId,
1603
+ subject: taskName,
1604
+ parentId: targetParentId,
1605
+ estimatedHours: node.estimatedHours
1606
+ });
1607
+ const progress = result.totalCreated + result.totalFailed;
1608
+ console.error(`[${progress}/${result.totalTasks}] \u2705 \u521B\u5EFA\u6210\u529F: #${newId} ${taskName}`);
1609
+ if (node.children.length > 0) {
1610
+ 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...`);
1611
+ await this.batchCreateRecursive(
1612
+ token,
1613
+ host,
1614
+ project,
1615
+ node.children,
1616
+ newId,
1617
+ parentInfo,
1618
+ assignedToMail,
1619
+ dryRun,
1620
+ interval,
1621
+ result
1622
+ );
1623
+ }
1624
+ } else {
1625
+ const errorMsg = createResult.message || createResult.api_error_msg || "\u521B\u5EFA\u5931\u8D25";
1626
+ logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, errorMsg);
1627
+ result.totalFailed++;
1628
+ result.failed.push({
1629
+ subject: taskName,
1630
+ parentId: targetParentId,
1631
+ error: errorMsg
1632
+ });
1633
+ const progress = result.totalCreated + result.totalFailed;
1634
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u521B\u5EFA\u5931\u8D25: ${taskName} - ${errorMsg}`);
1635
+ }
1636
+ } catch (error) {
1637
+ const errorMsg = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
1638
+ logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u65F6\u53D1\u751F\u9519\u8BEF: ${taskName}`, error);
1639
+ result.totalFailed++;
1640
+ result.failed.push({
1641
+ subject: taskName,
1642
+ parentId: targetParentId,
1643
+ error: errorMsg
1644
+ });
1645
+ const progress = result.totalCreated + result.totalFailed;
1646
+ console.error(`[${progress}/${result.totalTasks}] \u274C \u521B\u5EFA\u5931\u8D25: ${taskName} - ${errorMsg}`);
1647
+ }
1648
+ await sleep(interval);
1649
+ }
1650
+ }
1651
+ }
1123
1652
  };
1124
1653
  var issueService = new IssueService();
1125
1654
 
@@ -1149,8 +1678,8 @@ function parsePmLink(url) {
1149
1678
  }
1150
1679
 
1151
1680
  // src/utils/issue-exporter.ts
1152
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, statSync } from "fs";
1153
- import { join as join2 } from "path";
1681
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, statSync } from "fs";
1682
+ import { join as join3 } from "path";
1154
1683
 
1155
1684
  // src/utils/preset-extractor.ts
1156
1685
  var SUMMARY_CUSTOM_FIELD_IDS = [84];
@@ -1265,27 +1794,27 @@ function generateExportDir(command, identifier) {
1265
1794
  async function exportIssuePresets(issue, exportDir, options = {}) {
1266
1795
  const { depth = -1, pretty = false } = options;
1267
1796
  const indent = pretty ? 2 : 0;
1268
- mkdirSync2(exportDir, { recursive: true });
1269
- mkdirSync2(join2(exportDir, "standard"), { recursive: true });
1270
- mkdirSync2(join2(exportDir, "complete"), { recursive: true });
1797
+ mkdirSync3(exportDir, { recursive: true });
1798
+ mkdirSync3(join3(exportDir, "standard"), { recursive: true });
1799
+ mkdirSync3(join3(exportDir, "complete"), { recursive: true });
1271
1800
  const resultFiles = [];
1272
1801
  const summaryTree = generateSummaryTree(issue, 0, depth);
1273
- const summaryPath = join2(exportDir, "tree.summary.json");
1274
- writeFileSync3(summaryPath, JSON.stringify(summaryTree, null, indent), "utf-8");
1802
+ const summaryPath = join3(exportDir, "tree.summary.json");
1803
+ writeFileSync4(summaryPath, JSON.stringify(summaryTree, null, indent), "utf-8");
1275
1804
  const summarySize = statSync(summaryPath).size;
1276
1805
  resultFiles.push({ path: "tree.summary.json", size: summarySize, type: "tree" });
1277
1806
  const allIssues = flattenIssues(issue);
1278
1807
  for (const iss of allIssues) {
1279
1808
  const standardData = extractStandardFields(iss);
1280
- const standardPath = join2(exportDir, "standard", `${iss.id}.json`);
1281
- writeFileSync3(standardPath, JSON.stringify(standardData, null, indent), "utf-8");
1809
+ const standardPath = join3(exportDir, "standard", `${iss.id}.json`);
1810
+ writeFileSync4(standardPath, JSON.stringify(standardData, null, indent), "utf-8");
1282
1811
  const standardSize = statSync(standardPath).size;
1283
1812
  resultFiles.push({ path: `standard/${iss.id}.json`, size: standardSize, type: "standard" });
1284
1813
  }
1285
1814
  for (const iss of allIssues) {
1286
1815
  const completeData = extractCompleteFields(iss);
1287
- const completePath = join2(exportDir, "complete", `${iss.id}.json`);
1288
- writeFileSync3(completePath, JSON.stringify(completeData, null, indent), "utf-8");
1816
+ const completePath = join3(exportDir, "complete", `${iss.id}.json`);
1817
+ writeFileSync4(completePath, JSON.stringify(completeData, null, indent), "utf-8");
1289
1818
  const completeSize = statSync(completePath).size;
1290
1819
  resultFiles.push({ path: `complete/${iss.id}.json`, size: completeSize, type: "complete" });
1291
1820
  }
@@ -1936,6 +2465,117 @@ function createIssueCommand() {
1936
2465
  process.exit(1);
1937
2466
  }
1938
2467
  });
2468
+ 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) => {
2469
+ let parentId;
2470
+ let parentHost;
2471
+ if (options.parentUrl) {
2472
+ const linkInfo = parsePmLink(options.parentUrl);
2473
+ if (!linkInfo) {
2474
+ outputError("\u65E0\u6548\u7684\u7236\u5355 PM \u94FE\u63A5\u683C\u5F0F", options.pretty);
2475
+ process.exit(1);
2476
+ }
2477
+ parentId = parseInt(linkInfo.issueId, 10);
2478
+ parentHost = linkInfo.host;
2479
+ } else if (options.parentId) {
2480
+ const cleanId = options.parentId.replace(/^#/, "");
2481
+ parentId = parseInt(cleanId, 10);
2482
+ if (isNaN(parentId)) {
2483
+ outputError("\u65E0\u6548\u7684\u7236\u5355 ID", options.pretty);
2484
+ process.exit(1);
2485
+ }
2486
+ }
2487
+ if (!parentId) {
2488
+ outputError("\u8BF7\u63D0\u4F9B\u7236\u5355 ID\uFF08--parent-id\uFF09\u6216 PM \u94FE\u63A5\uFF08--parent-url\uFF09", options.pretty);
2489
+ process.exit(1);
2490
+ }
2491
+ let markdown;
2492
+ if (options.stdin) {
2493
+ const chunks = [];
2494
+ for await (const chunk of process.stdin) {
2495
+ chunks.push(chunk);
2496
+ }
2497
+ markdown = Buffer.concat(chunks).toString("utf-8");
2498
+ } else if (options.markdown) {
2499
+ try {
2500
+ markdown = readFileSync3(options.markdown, "utf-8");
2501
+ } catch {
2502
+ outputError(`\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${options.markdown}`, options.pretty);
2503
+ process.exit(1);
2504
+ }
2505
+ } else {
2506
+ outputError("\u8BF7\u63D0\u4F9B Markdown \u6587\u4EF6\u8DEF\u5F84\uFF08--markdown\uFF09\u6216\u4F7F\u7528\u6807\u51C6\u8F93\u5165\uFF08--stdin\uFF09", options.pretty);
2507
+ process.exit(1);
2508
+ }
2509
+ if (!markdown.trim()) {
2510
+ outputError("Markdown \u5185\u5BB9\u4E3A\u7A7A", options.pretty);
2511
+ process.exit(1);
2512
+ }
2513
+ const taskNodes = parseMarkdownToNodes(markdown);
2514
+ if (taskNodes.length === 0) {
2515
+ outputError("\u6CA1\u6709\u89E3\u6790\u5230\u4EFB\u4F55\u4EFB\u52A1\uFF0C\u8BF7\u68C0\u67E5 Markdown \u683C\u5F0F", options.pretty);
2516
+ process.exit(1);
2517
+ }
2518
+ const creds = resolveCredentials({
2519
+ ...options,
2520
+ host: parentHost || options.host
2521
+ });
2522
+ const validation = validateCredentials(creds, ["token", "host"]);
2523
+ if (!validation.valid) {
2524
+ outputError(`\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570: ${validation.missing.join(", ")}`, options.pretty);
2525
+ process.exit(1);
2526
+ }
2527
+ let assignedToMail = options.assignedToMail;
2528
+ if (!assignedToMail) {
2529
+ const config = readConfig(options.config);
2530
+ assignedToMail = config.default?.userMail;
2531
+ if (options.profile && config.profiles[options.profile]?.userMail) {
2532
+ assignedToMail = config.profiles[options.profile].userMail;
2533
+ }
2534
+ }
2535
+ if (!assignedToMail) {
2536
+ 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);
2537
+ process.exit(1);
2538
+ }
2539
+ const interval = parseInt(options.interval, 10);
2540
+ const dryRun = options.dryRun || false;
2541
+ const result = await issueService.batchCreateFromMarkdown(
2542
+ creds.token,
2543
+ creds.host,
2544
+ creds.project || "",
2545
+ parentId,
2546
+ markdown,
2547
+ assignedToMail,
2548
+ { dryRun, interval }
2549
+ );
2550
+ if (result.success && result.data) {
2551
+ const output = {
2552
+ success: true,
2553
+ dryRun,
2554
+ parentId,
2555
+ assignedToMail,
2556
+ summary: {
2557
+ totalTasks: result.data.totalTasks,
2558
+ totalEstimatedHours: result.data.totalEstimatedHours,
2559
+ totalCreated: result.data.totalCreated,
2560
+ totalFailed: result.data.totalFailed
2561
+ },
2562
+ // 树形结构预览
2563
+ taskTree: formatTaskTree(taskNodes, "", true, true, assignedToMail.split("@")[0]),
2564
+ created: result.data.created,
2565
+ failed: result.data.failed.length > 0 ? result.data.failed : void 0
2566
+ };
2567
+ smartOutput(output, {
2568
+ stdout: options.stdout,
2569
+ output: options.output,
2570
+ command: "issue-batch-create",
2571
+ identifier: `${parentId}`,
2572
+ pretty: options.pretty
2573
+ });
2574
+ } else {
2575
+ outputError(result.message || result.msg || result.api_error_msg || "\u6279\u91CF\u521B\u5EFA\u5931\u8D25", options.pretty);
2576
+ process.exit(1);
2577
+ }
2578
+ });
1939
2579
  return issueCmd;
1940
2580
  }
1941
2581
 
@@ -2006,6 +2646,17 @@ var TimeEntryService = class {
2006
2646
  var timeEntryService = new TimeEntryService();
2007
2647
 
2008
2648
  // src/commands/time/index.ts
2649
+ var WEEKDAY_NAMES = ["\u661F\u671F\u65E5", "\u661F\u671F\u4E00", "\u661F\u671F\u4E8C", "\u661F\u671F\u4E09", "\u661F\u671F\u56DB", "\u661F\u671F\u4E94", "\u661F\u671F\u516D"];
2650
+ function getTodayContext() {
2651
+ const now = /* @__PURE__ */ new Date();
2652
+ const year = now.getFullYear();
2653
+ const month = String(now.getMonth() + 1).padStart(2, "0");
2654
+ const day = String(now.getDate()).padStart(2, "0");
2655
+ return {
2656
+ today: `${year}-${month}-${day}`,
2657
+ todayWeekday: WEEKDAY_NAMES[now.getDay()]
2658
+ };
2659
+ }
2009
2660
  function getWeekDateRange(week) {
2010
2661
  const now = /* @__PURE__ */ new Date();
2011
2662
  let offset = 0;
@@ -2106,6 +2757,7 @@ function createTimeCommand() {
2106
2757
  }
2107
2758
  const totalHours = allTimeEntries.reduce((sum, e) => sum + (e.hours || 0), 0);
2108
2759
  outputSuccess({
2760
+ ...getTodayContext(),
2109
2761
  total_count: totalCount,
2110
2762
  total_hours: Math.round(totalHours * 100) / 100,
2111
2763
  project_summary: projectSummary,
@@ -2364,6 +3016,7 @@ function createTimeCommand() {
2364
3016
  const remainingHours = Math.max(0, targetHours - totalHours);
2365
3017
  totalHours = Math.round(totalHours * 100) / 100;
2366
3018
  const output = {
3019
+ ...getTodayContext(),
2367
3020
  period: periodLabel,
2368
3021
  userId: userId || "\u672A\u6307\u5B9A",
2369
3022
  totalHours,
@@ -2441,15 +3094,32 @@ function createProjectCommand() {
2441
3094
  }
2442
3095
 
2443
3096
  // src/index.ts
2444
- import { readFileSync as readFileSync2 } from "fs";
2445
- import { dirname as dirname2, join as join3 } from "path";
3097
+ import { readFileSync as readFileSync4 } from "fs";
3098
+ import { dirname as dirname2, join as join4 } from "path";
2446
3099
  import { fileURLToPath } from "url";
2447
3100
  var __filename = fileURLToPath(import.meta.url);
2448
3101
  var __dirname = dirname2(__filename);
2449
- var packageJson = JSON.parse(readFileSync2(join3(__dirname, "../package.json"), "utf-8"));
3102
+ var packageJson = JSON.parse(readFileSync4(join4(__dirname, "../package.json"), "utf-8"));
2450
3103
  var version = packageJson.version;
3104
+ initTelemetry(version);
3105
+ trackSessionStarted();
3106
+ function extractCommandNames(cmd) {
3107
+ const names = [];
3108
+ let current = cmd;
3109
+ while (current) {
3110
+ const name = current.name();
3111
+ if (name && name !== "pm-cli") {
3112
+ names.unshift(name);
3113
+ }
3114
+ current = current.parent;
3115
+ }
3116
+ return {
3117
+ command: names[0] || "unknown",
3118
+ subcommand: names.slice(1).join(":") || ""
3119
+ };
3120
+ }
2451
3121
  var program = new Command6();
2452
- 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) => {
3122
+ 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) => {
2453
3123
  const opts = thisCommand.opts();
2454
3124
  if (opts.debug) {
2455
3125
  logger_default.setLevel("debug");
@@ -2461,11 +3131,33 @@ program.name("pm-cli").description("\u7F51\u6613\u6613\u534F\u4F5C (PM NetEase)
2461
3131
  if (opts.mapping === false) {
2462
3132
  setKeyMappingEnabled(false);
2463
3133
  }
3134
+ const { command, subcommand } = extractCommandNames(actionCommand);
3135
+ const actionOpts = actionCommand.opts();
3136
+ startCommandTracking(command, subcommand, {
3137
+ has_profile: !!actionOpts.profile,
3138
+ has_url_input: !!actionOpts.url,
3139
+ depth: actionOpts.depth ? parseInt(actionOpts.depth, 10) : void 0,
3140
+ output_mode: actionOpts.stdout ? "stdout" : actionOpts.raw ? "raw" : "file"
3141
+ });
3142
+ }).hook("postAction", () => {
3143
+ endCommandTracking();
2464
3144
  });
2465
3145
  program.addCommand(createTestCommand());
2466
3146
  program.addCommand(createConfigCommand());
2467
3147
  program.addCommand(createIssueCommand());
2468
3148
  program.addCommand(createTimeCommand());
2469
3149
  program.addCommand(createProjectCommand());
2470
- program.parse();
3150
+ async function main() {
3151
+ try {
3152
+ await program.parseAsync();
3153
+ } catch (error) {
3154
+ failCommandTracking(error);
3155
+ throw error;
3156
+ } finally {
3157
+ await shutdownTelemetry();
3158
+ }
3159
+ }
3160
+ main().catch(() => {
3161
+ process.exit(1);
3162
+ });
2471
3163
  //# sourceMappingURL=index.js.map