ai-project-manage-cli 3.0.4 → 3.0.6

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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync5 } from "fs";
5
- import { dirname as dirname2, join as join6 } from "path";
4
+ import { readFileSync as readFileSync10 } from "fs";
5
+ import { dirname as dirname2, join as join8 } from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
8
8
 
@@ -115,6 +115,24 @@ var requestConfig = {
115
115
  method: "POST",
116
116
  path: "/cli/requirements/update-dev-status"
117
117
  })
118
+ },
119
+ requirementArtifact: {
120
+ list: defineEndpoint({
121
+ method: "GET",
122
+ path: "/requirement-artifacts/list"
123
+ }),
124
+ create: defineEndpoint({
125
+ method: "POST",
126
+ path: "/requirement-artifacts/create"
127
+ }),
128
+ update: defineEndpoint({
129
+ method: "POST",
130
+ path: "/requirement-artifacts/update"
131
+ }),
132
+ delete: defineEndpoint({
133
+ method: "POST",
134
+ path: "/requirement-artifacts/delete"
135
+ })
118
136
  }
119
137
  };
120
138
 
@@ -136,6 +154,9 @@ import { fileURLToPath } from "url";
136
154
  var __dirname = dirname(fileURLToPath(import.meta.url));
137
155
  var CLI_TEMPLATE_DIR = resolve(__dirname, "../template");
138
156
  var WORKSPACE_APM_DIR = resolve(process.cwd(), ".apm");
157
+ function requirementWorkitemsDir(requirementId) {
158
+ return join2(WORKSPACE_APM_DIR, "workitems", requirementId);
159
+ }
139
160
  async function ensureLoggedConfig() {
140
161
  const cfg = await ensureApmConfig();
141
162
  if (!cfg.token) {
@@ -499,15 +520,15 @@ async function runPull(requirementId) {
499
520
  const cfg = await ensureLoggedConfig();
500
521
  const api = createApmApiClient(cfg);
501
522
  const data = await api.cliRequirements.pull({ requirementId });
502
- const WORKITEMS_DIR = join4(WORKSPACE_APM_DIR, "workitems", requirementId);
523
+ const WORKITEMS_DIR = requirementWorkitemsDir(requirementId);
503
524
  await ensureDirExists(WORKITEMS_DIR);
504
- const req = data.requirement;
525
+ const req2 = data.requirement;
505
526
  const statusYaml = yamlStringify(
506
527
  {
507
- id: req.id,
508
- status: req.status,
509
- title: req.title,
510
- env: req.envName || "",
528
+ id: req2.id,
529
+ status: req2.status,
530
+ title: req2.title,
531
+ env: req2.envName || "",
511
532
  tasks: tasksForStatusYaml(data.tasks ?? [])
512
533
  },
513
534
  { lineWidth: 0 }
@@ -518,7 +539,7 @@ async function runPull(requirementId) {
518
539
  `,
519
540
  "utf8"
520
541
  );
521
- writeFileSync3(join4(WORKITEMS_DIR, "prd.md"), req.content || "", "utf8");
542
+ writeFileSync3(join4(WORKITEMS_DIR, "prd.md"), req2.content || "", "utf8");
522
543
  const reviews = data.reviews ?? [];
523
544
  const reviewsXml = [
524
545
  "<reviews>",
@@ -584,12 +605,1116 @@ async function runUpdateStatus(requirementId, status) {
584
605
  console.log(JSON.stringify(data, null, 2));
585
606
  }
586
607
 
608
+ // src/commands/upload-artifact.ts
609
+ import { existsSync as existsSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
610
+ import { join as join6, relative, sep } from "path";
611
+ var EXCLUDED_RELATIVE_PATHS = /* @__PURE__ */ new Set([
612
+ "defect.xml",
613
+ "prd.md",
614
+ "requirement-status.yaml",
615
+ "reviews.xml",
616
+ "testcase.xml"
617
+ ]);
618
+ function toPosixRelative(root, absoluteFile) {
619
+ return relative(root, absoluteFile).split(sep).join("/");
620
+ }
621
+ function artifactTagFromRelPath(relPosix) {
622
+ const parts = relPosix.split("/");
623
+ const fileName = parts[parts.length - 1] ?? relPosix;
624
+ if (parts.length > 1) {
625
+ return parts[0] ?? fileName;
626
+ }
627
+ const dot = fileName.lastIndexOf(".");
628
+ return dot > 0 ? fileName.slice(0, dot) : fileName;
629
+ }
630
+ function* walkMarkdownFiles(dir) {
631
+ const names = readdirSync2(dir);
632
+ for (const name of names) {
633
+ if (name.startsWith(".")) continue;
634
+ const full = join6(dir, name);
635
+ const st = statSync2(full);
636
+ if (st.isDirectory()) {
637
+ yield* walkMarkdownFiles(full);
638
+ continue;
639
+ }
640
+ if (!st.isFile()) continue;
641
+ if (!name.toLowerCase().endsWith(".md")) continue;
642
+ yield full;
643
+ }
644
+ }
645
+ async function deleteAllArtifactsForRequirement(api, requirementId) {
646
+ const pageSize = 100;
647
+ let page = 1;
648
+ const rows = [];
649
+ while (true) {
650
+ const batch = await api.requirementArtifact.list({
651
+ requirementId,
652
+ page,
653
+ pageSize
654
+ });
655
+ rows.push(...batch.items);
656
+ if (batch.total === 0 || rows.length >= batch.total) break;
657
+ page += 1;
658
+ }
659
+ for (const row of rows) {
660
+ await api.requirementArtifact.delete({ artifactId: row.id });
661
+ }
662
+ return rows.length;
663
+ }
664
+ async function runUploadArtifact(requirementId) {
665
+ const cfg = await ensureLoggedConfig();
666
+ const api = createApmApiClient(cfg);
667
+ const root = requirementWorkitemsDir(requirementId);
668
+ if (!existsSync2(root)) {
669
+ console.error(
670
+ `[apm] \u76EE\u5F55\u4E0D\u5B58\u5728: ${root}
671
+ \u8BF7\u5148\u6267\u884C: apm pull ${requirementId}`
672
+ );
673
+ process.exit(1);
674
+ }
675
+ const deleted = await deleteAllArtifactsForRequirement(api, requirementId);
676
+ console.log(`[apm] \u5DF2\u6E05\u7A7A\u9700\u6C42\u4EA7\u7269\u6587\u6863 ${deleted} \u6761`);
677
+ const paths = [...walkMarkdownFiles(root)];
678
+ let created = 0;
679
+ let skipped = 0;
680
+ for (const abs of paths) {
681
+ const relPosix = toPosixRelative(root, abs);
682
+ if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
683
+ skipped += 1;
684
+ console.log(`[apm] \u8DF3\u8FC7\uFF08\u6392\u9664\u5217\u8868\uFF09: ${relPosix}`);
685
+ continue;
686
+ }
687
+ const content = readFileSync5(abs, "utf8");
688
+ const tag = artifactTagFromRelPath(relPosix);
689
+ await api.requirementArtifact.create({
690
+ requirementId,
691
+ tag,
692
+ fileName: relPosix,
693
+ content
694
+ });
695
+ created += 1;
696
+ console.log(`[apm] \u5DF2\u4E0A\u4F20\u4EA7\u7269: ${relPosix} (tag=${tag})`);
697
+ }
698
+ console.log(
699
+ `[apm] \u5B8C\u6210\uFF1A\u5220\u9664 ${deleted}\uFF0C\u65B0\u5EFA ${created}\uFF0C\u8DF3\u8FC7\uFF08\u6392\u9664\uFF09 ${skipped}\uFF0C\u5171\u626B\u63CF ${paths.length} \u4E2A Markdown \u6587\u4EF6`
700
+ );
701
+ }
702
+
703
+ // src/commands/deploy/backend.ts
704
+ import path5 from "node:path";
705
+
706
+ // src/commands/deploy/lib/apm-config.ts
707
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
708
+ import { resolve as resolve3 } from "node:path";
709
+ function loadApmConfig(options) {
710
+ const p = resolve3(
711
+ process.cwd(),
712
+ options?.configPath ?? resolve3(WORKSPACE_APM_DIR, "apm.config.json")
713
+ );
714
+ if (!existsSync3(p)) {
715
+ console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
716
+ process.exit(1);
717
+ }
718
+ try {
719
+ const raw = readFileSync6(p, "utf8");
720
+ return JSON.parse(raw);
721
+ } catch (e) {
722
+ console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
723
+ process.exit(1);
724
+ }
725
+ }
726
+ function req(v, field) {
727
+ if (v === void 0 || v === null || typeof v === "string" && !v.trim()) {
728
+ console.error(`apm.config.json \u4E2D backendDeploy.${field} \u4E0D\u80FD\u4E3A\u7A7A`);
729
+ process.exit(1);
730
+ }
731
+ return v;
732
+ }
733
+ function reqBackendPositiveInt(v, field) {
734
+ const n = Number(v);
735
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
736
+ console.error(
737
+ `apm.config.json \u4E2D backendDeploy.${field} \u987B\u4E3A\u6B63\u6574\u6570`
738
+ );
739
+ process.exit(1);
740
+ }
741
+ return n;
742
+ }
743
+ function resolveBackendDeployFromApmConfig(cfg) {
744
+ const b = cfg.backendDeploy ?? {};
745
+ const protoRaw = req(b.remoteProtocol, "remoteProtocol").trim().toLowerCase();
746
+ if (protoRaw !== "http" && protoRaw !== "https") {
747
+ console.error(
748
+ "apm.config.json \u4E2D backendDeploy.remoteProtocol \u53EA\u80FD\u4E3A http \u6216 https"
749
+ );
750
+ process.exit(1);
751
+ }
752
+ const remoteProtocol = protoRaw;
753
+ if (!Array.isArray(b.containerPortsMappings)) {
754
+ console.error(
755
+ "apm.config.json \u4E2D backendDeploy.containerPortsMappings \u987B\u4E3A\u975E\u7A7A\u6570\u7EC4"
756
+ );
757
+ process.exit(1);
758
+ }
759
+ const mappings = b.containerPortsMappings.map((x) => String(x).trim()).filter(Boolean);
760
+ if (mappings.length === 0) {
761
+ console.error(
762
+ "apm.config.json \u4E2D backendDeploy.containerPortsMappings \u987B\u81F3\u5C11\u5305\u542B\u4E00\u9879\u7AEF\u53E3\u6620\u5C04"
763
+ );
764
+ process.exit(1);
765
+ }
766
+ return {
767
+ name: req(b.name, "name").trim(),
768
+ registryHost: req(b.registryHost, "registryHost").trim(),
769
+ registryNamespace: req(b.registryNamespace, "registryNamespace").trim(),
770
+ registryUser: req(b.registryUser, "registryUser").trim(),
771
+ registryPassword: req(b.registryPassword, "registryPassword").trim(),
772
+ remoteHost: req(b.remoteHost, "remoteHost").trim(),
773
+ remotePort: reqBackendPositiveInt(b.remotePort, "remotePort"),
774
+ remoteProtocol,
775
+ caPath: b.caPath?.trim(),
776
+ certPath: b.certPath?.trim(),
777
+ keyPath: b.keyPath?.trim(),
778
+ envFilePath: typeof b.envFilePath === "string" ? b.envFilePath.trim() : "",
779
+ containerPortsMappings: mappings,
780
+ dockerNetwork: b.dockerNetwork?.trim() || void 0
781
+ };
782
+ }
783
+ function reqFe(v, field) {
784
+ if (v === void 0 || v === null || typeof v === "string" && !v.trim()) {
785
+ console.error(`apm.config.json \u4E2D frontendDeploy.${field} \u4E0D\u80FD\u4E3A\u7A7A`);
786
+ process.exit(1);
787
+ }
788
+ return v;
789
+ }
790
+ function resolveFrontendDeployFromApmConfig(cfg) {
791
+ const f = cfg.frontendDeploy ?? {};
792
+ const port = Number(f.port);
793
+ return {
794
+ endpoint: reqFe(f.endpoint, "endpoint").trim(),
795
+ port: Number.isFinite(port) && port > 0 ? port : 9e3,
796
+ useSsl: Boolean(f.useSsl),
797
+ accessKey: reqFe(f.accessKey, "accessKey").trim(),
798
+ secretKey: reqFe(f.secretKey, "secretKey").trim(),
799
+ bucket: reqFe(f.bucket, "bucket").trim()
800
+ };
801
+ }
802
+
803
+ // src/commands/deploy/lib/backend-deploy/backend-deploy-workflow.ts
804
+ import path4 from "node:path";
805
+
806
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/client.ts
807
+ import Docker from "dockerode";
808
+
809
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/connection-options.ts
810
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "node:fs";
811
+ import path from "node:path";
812
+ function asOptionalTlsBuffer(value) {
813
+ if (typeof value !== "string") {
814
+ console.log("tls filepath not exist");
815
+ return void 0;
816
+ }
817
+ console.log("tls filepath", path.join(process.cwd(), value));
818
+ const normalized = value.trim();
819
+ if (normalized === "") {
820
+ return void 0;
821
+ }
822
+ if (existsSync4(normalized)) {
823
+ return readFileSync7(normalized);
824
+ }
825
+ const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
826
+ if (looksLikePath) {
827
+ throw new Error(`TLS \u8BC1\u4E66\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${normalized}`);
828
+ }
829
+ return Buffer.from(normalized);
830
+ }
831
+ function toDockerConnectionOptions(config) {
832
+ const protocol = (config.protocol ?? "https").toLowerCase();
833
+ const isHttps = protocol === "https";
834
+ const options = {
835
+ host: config.host ?? "127.0.0.1",
836
+ port: config.port ?? 2376,
837
+ protocol
838
+ };
839
+ const socketPath = (config.socketPath ?? "").trim();
840
+ if (socketPath !== "") {
841
+ options.socketPath = socketPath;
842
+ }
843
+ if (isHttps) {
844
+ const tlsOptions = {
845
+ ca: asOptionalTlsBuffer(config.ca),
846
+ cert: asOptionalTlsBuffer(config.cert),
847
+ key: asOptionalTlsBuffer(config.key)
848
+ };
849
+ if (tlsOptions.ca) {
850
+ options.ca = tlsOptions.ca;
851
+ }
852
+ if (tlsOptions.cert) {
853
+ options.cert = tlsOptions.cert;
854
+ }
855
+ if (tlsOptions.key) {
856
+ options.key = tlsOptions.key;
857
+ }
858
+ options.checkServerIdentity = () => void 0;
859
+ }
860
+ return options;
861
+ }
862
+
863
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/container-inspect-format.ts
864
+ function formatPortMappings(inspectInfo) {
865
+ const ports = inspectInfo?.NetworkSettings?.Ports;
866
+ if (!ports || typeof ports !== "object") {
867
+ return void 0;
868
+ }
869
+ const mappings = [];
870
+ for (const [containerPort, hostBindings] of Object.entries(ports)) {
871
+ if (!hostBindings || hostBindings.length === 0) {
872
+ mappings.push(`${containerPort} -> <not-published>`);
873
+ continue;
874
+ }
875
+ for (const binding of hostBindings) {
876
+ const hostIp = binding.HostIp || "0.0.0.0";
877
+ const hostPort = binding.HostPort || "<unknown>";
878
+ mappings.push(`${containerPort} -> ${hostIp}:${hostPort}`);
879
+ }
880
+ }
881
+ return mappings.length > 0 ? mappings : void 0;
882
+ }
883
+
884
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/container-options.ts
885
+ function toContainerCreateOptions(input) {
886
+ const hostConfig = {};
887
+ let hasHostConfig = false;
888
+ if (input.portBindings) {
889
+ hostConfig.PortBindings = input.portBindings;
890
+ hasHostConfig = true;
891
+ }
892
+ if (input.binds) {
893
+ hostConfig.Binds = input.binds;
894
+ hasHostConfig = true;
895
+ }
896
+ if (typeof input.nanoCpus === "number") {
897
+ hostConfig.NanoCpus = input.nanoCpus;
898
+ hasHostConfig = true;
899
+ }
900
+ if (typeof input.memory === "number") {
901
+ hostConfig.Memory = Number(input.memory) * 1024 * 1024;
902
+ hasHostConfig = true;
903
+ }
904
+ const payload = {
905
+ name: input.name,
906
+ Image: input.image,
907
+ Env: input.env
908
+ };
909
+ if (input.exposedPorts) {
910
+ payload.ExposedPorts = input.exposedPorts;
911
+ }
912
+ if (hasHostConfig) {
913
+ payload.HostConfig = hostConfig;
914
+ }
915
+ const net = input.dockerNetwork?.trim();
916
+ if (net) {
917
+ payload.NetworkingConfig = {
918
+ EndpointsConfig: {
919
+ [net]: {}
920
+ }
921
+ };
922
+ }
923
+ return payload;
924
+ }
925
+
926
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/client.ts
927
+ var DockerodeClient = class {
928
+ client;
929
+ constructor(config) {
930
+ const options = toDockerConnectionOptions(config);
931
+ this.client = new Docker(options);
932
+ }
933
+ async ping() {
934
+ await this.client.ping();
935
+ }
936
+ async imageExists(image) {
937
+ try {
938
+ await this.client.getImage(image).inspect();
939
+ return true;
940
+ } catch {
941
+ return false;
942
+ }
943
+ }
944
+ async removeImage(image) {
945
+ await this.client.getImage(image).remove({ force: true });
946
+ }
947
+ async pullImage(image, auth) {
948
+ const stream = await new Promise((resolve4, reject) => {
949
+ const pullOptions = auth ? { authconfig: auth } : void 0;
950
+ this.client.pull(image, pullOptions, (err, output) => {
951
+ if (err || !output) {
952
+ reject(err ?? new Error("docker pull \u8FD4\u56DE\u7A7A\u8F93\u51FA"));
953
+ return;
954
+ }
955
+ resolve4(output);
956
+ });
957
+ });
958
+ await new Promise((resolve4, reject) => {
959
+ this.client.modem.followProgress(
960
+ stream,
961
+ (err) => {
962
+ if (err) {
963
+ reject(err);
964
+ return;
965
+ }
966
+ resolve4();
967
+ },
968
+ () => void 0
969
+ );
970
+ });
971
+ }
972
+ async findContainerIdByName(name) {
973
+ const containers = await this.client.listContainers({ all: true });
974
+ const matched = containers.find(
975
+ (item) => (item.Names ?? []).some((n) => n === `/${name}`)
976
+ );
977
+ return matched?.Id;
978
+ }
979
+ async stopContainer(id) {
980
+ const container = this.client.getContainer(id);
981
+ try {
982
+ await container.stop();
983
+ } catch {
984
+ }
985
+ }
986
+ async removeContainer(id) {
987
+ const container = this.client.getContainer(id);
988
+ await container.remove({ force: true });
989
+ }
990
+ async createContainer(input) {
991
+ const payload = toContainerCreateOptions(input);
992
+ const container = await this.client.createContainer(payload);
993
+ return container.id;
994
+ }
995
+ async startContainer(id) {
996
+ const container = this.client.getContainer(id);
997
+ await container.start();
998
+ }
999
+ async inspectContainer(id) {
1000
+ const container = this.client.getContainer(id);
1001
+ const inspectInfo = await container.inspect();
1002
+ const state = inspectInfo?.State;
1003
+ const portMappings = formatPortMappings(inspectInfo);
1004
+ return {
1005
+ running: state?.Running,
1006
+ status: state?.Status,
1007
+ exitCode: state?.ExitCode,
1008
+ error: state?.Error,
1009
+ startedAt: state?.StartedAt,
1010
+ finishedAt: state?.FinishedAt,
1011
+ health: state?.Health?.Status,
1012
+ portMappings
1013
+ };
1014
+ }
1015
+ async getContainerLogs(id, tail = 100) {
1016
+ const container = this.client.getContainer(id);
1017
+ const logs = await container.logs({
1018
+ stdout: true,
1019
+ stderr: true,
1020
+ timestamps: true,
1021
+ follow: false,
1022
+ tail
1023
+ });
1024
+ if (typeof logs === "string") {
1025
+ return logs;
1026
+ }
1027
+ return logs.toString("utf8");
1028
+ }
1029
+ };
1030
+ var createDockerodeClient = (config) => new DockerodeClient(config);
1031
+
1032
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/env.ts
1033
+ import { existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
1034
+ import path2 from "node:path";
1035
+ function loadEnvFromFile(envFilePath) {
1036
+ if (!envFilePath) {
1037
+ return {};
1038
+ }
1039
+ const targetPath = path2.resolve(envFilePath);
1040
+ if (!existsSync5(targetPath) || !statSync3(targetPath).isFile()) {
1041
+ return {};
1042
+ }
1043
+ const raw = readFileSync8(targetPath, "utf-8");
1044
+ const result = {};
1045
+ for (const line of raw.split(/\r?\n/)) {
1046
+ const normalized = line.trim();
1047
+ if (normalized === "" || normalized.startsWith("#")) {
1048
+ continue;
1049
+ }
1050
+ const eqIndex = normalized.indexOf("=");
1051
+ if (eqIndex <= 0) {
1052
+ continue;
1053
+ }
1054
+ const key = normalized.slice(0, eqIndex).trim();
1055
+ const value = normalized.slice(eqIndex + 1);
1056
+ if (key !== "") {
1057
+ result[key] = value;
1058
+ }
1059
+ }
1060
+ return result;
1061
+ }
1062
+ function toEnvArray(env) {
1063
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`);
1064
+ }
1065
+
1066
+ // src/commands/deploy/lib/backend-deploy/dockerode-client/ports.ts
1067
+ function parsePorts(ports) {
1068
+ if (!ports || ports.length === 0) {
1069
+ return {};
1070
+ }
1071
+ const exposedPorts = {};
1072
+ const portBindings = {};
1073
+ for (const rawPort of ports) {
1074
+ const normalized = rawPort.trim();
1075
+ if (normalized === "") {
1076
+ continue;
1077
+ }
1078
+ const [hostAndContainer, protocolPart] = normalized.split("/");
1079
+ const protocol = protocolPart?.trim() === "udp" ? "udp" : "tcp";
1080
+ const segments = hostAndContainer.split(":").map((part) => part.trim());
1081
+ if (segments.length !== 2 || !segments[0] || !segments[1]) {
1082
+ throw new Error(`container.ports \u914D\u7F6E\u65E0\u6548: ${rawPort}`);
1083
+ }
1084
+ const hostPort = segments[0];
1085
+ const containerPort = segments[1];
1086
+ const key = `${containerPort}/${protocol}`;
1087
+ exposedPorts[key] = {};
1088
+ portBindings[key] = [{ HostPort: hostPort }];
1089
+ }
1090
+ return {
1091
+ exposedPorts: Object.keys(exposedPorts).length > 0 ? exposedPorts : void 0,
1092
+ portBindings: Object.keys(portBindings).length > 0 ? portBindings : void 0
1093
+ };
1094
+ }
1095
+
1096
+ // src/commands/deploy/lib/backend-deploy/image-tag.ts
1097
+ var DEPLOY_IMAGE_TAG = /^(?!\.|-)[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
1098
+ function assertDeployImageTag(tag) {
1099
+ const t = tag.trim();
1100
+ if (!DEPLOY_IMAGE_TAG.test(t)) {
1101
+ throw new Error(
1102
+ `${t} \u987B\u4E3A\u5408\u6CD5\u955C\u50CF tag\uFF08\u5B57\u6BCD\u6570\u5B57\u53CA ._-\uFF0C\u4E0D\u4EE5 . \u6216 - \u5F00\u5934\uFF0C\u6700\u957F 128 \u5B57\u7B26\uFF09\uFF0C\u5F53\u524D\uFF1A${JSON.stringify(tag)}`
1103
+ );
1104
+ }
1105
+ }
1106
+
1107
+ // src/commands/deploy/lib/backend-deploy/local-docker-build.ts
1108
+ import { platform } from "node:os";
1109
+
1110
+ // src/commands/deploy/lib/backend-deploy/command-runner.ts
1111
+ import { execSync } from "child_process";
1112
+
1113
+ // src/commands/deploy/lib/backend-deploy/logger.ts
1114
+ var Logger = class {
1115
+ static info(message) {
1116
+ console.log(`\x1B[36m[INFO]\x1B[0m ${message}`);
1117
+ }
1118
+ static success(message) {
1119
+ console.log(`\x1B[32m[SUCCESS]\x1B[0m ${message}`);
1120
+ }
1121
+ static warn(message) {
1122
+ console.log(`\x1B[33m[WARN]\x1B[0m ${message}`);
1123
+ }
1124
+ static error(message) {
1125
+ console.log(`\x1B[31m[ERROR]\x1B[0m ${message}`);
1126
+ }
1127
+ };
1128
+
1129
+ // src/commands/deploy/lib/backend-deploy/command-runner.ts
1130
+ var CommandRunner = class {
1131
+ /**
1132
+ * 执行命令
1133
+ */
1134
+ static exec(command, cwd) {
1135
+ try {
1136
+ Logger.info(`\u6267\u884C\u547D\u4EE4: ${command}`);
1137
+ const result = execSync(command, {
1138
+ cwd,
1139
+ encoding: "utf8",
1140
+ stdio: "pipe"
1141
+ });
1142
+ return result.toString().trim();
1143
+ } catch (error) {
1144
+ Logger.error(`\u547D\u4EE4\u6267\u884C\u5931\u8D25: ${command}`);
1145
+ Logger.error(error.message);
1146
+ throw error;
1147
+ }
1148
+ }
1149
+ /**
1150
+ * 执行命令并显示输出
1151
+ */
1152
+ static execWithOutput(command, cwd) {
1153
+ try {
1154
+ Logger.info(`\u6267\u884C\u547D\u4EE4: ${command}`);
1155
+ execSync(command, {
1156
+ cwd,
1157
+ stdio: "inherit"
1158
+ });
1159
+ } catch (error) {
1160
+ Logger.error(`\u547D\u4EE4\u6267\u884C\u5931\u8D25: ${command}`);
1161
+ throw error;
1162
+ }
1163
+ }
1164
+ };
1165
+
1166
+ // src/commands/deploy/lib/backend-deploy/local-docker-build.ts
1167
+ function dockerBuildPlatformFlags() {
1168
+ return platform() === "darwin" ? ["--platform", "linux/amd64"] : [];
1169
+ }
1170
+ function buildDockerImageLocally(params, cwd) {
1171
+ const platformFlags = dockerBuildPlatformFlags();
1172
+ const command = `docker buildx build ${platformFlags.join(" ")} -t ${params.image}:${params.tag} .`;
1173
+ Logger.info("\u5F00\u59CB\u6784\u5EFA Docker \u955C\u50CF...");
1174
+ Logger.info(`\u955C\u50CF\u540D\u79F0: ${params.image}:${params.tag}`);
1175
+ CommandRunner.exec(command, cwd);
1176
+ Logger.info("\u2713 \u955C\u50CF\u6784\u5EFA\u6210\u529F");
1177
+ }
1178
+
1179
+ // src/commands/deploy/lib/backend-deploy/registry-login-push.ts
1180
+ function dockerLoginPrivateRegistry(params, cwd) {
1181
+ const { registryUser, registryPassword, registryHost } = params;
1182
+ Logger.info("\u68C0\u67E5\u914D\u7F6E\u53C2\u6570...");
1183
+ if (!registryUser || !registryPassword || !registryHost) {
1184
+ Logger.error("Docker \u955C\u50CF\u4ED3\u5E93\u914D\u7F6E\u9519\u8BEF\uFF0C\u7F3A\u5C11\u53C2\u6570\uFF0C\u767B\u5F55\u5931\u8D25\uFF01");
1185
+ process.exit(1);
1186
+ }
1187
+ Logger.info("\u767B\u5F55 Docker \u955C\u50CF\u4ED3\u5E93...");
1188
+ CommandRunner.exec(
1189
+ `docker login --username=${registryUser} --password=${registryPassword} ${registryHost}`,
1190
+ cwd
1191
+ );
1192
+ Logger.info("\u2713 Docker \u767B\u5F55\u6210\u529F");
1193
+ }
1194
+ function dockerTagImage(params, cwd) {
1195
+ const { image, tag, registryHost, registryNamespace } = params;
1196
+ Logger.info("\u6807\u8BB0 Docker \u955C\u50CF...");
1197
+ CommandRunner.exec(
1198
+ `docker tag ${image}:${tag} ${registryHost}/${registryNamespace}/${image}:${tag}`,
1199
+ cwd
1200
+ );
1201
+ Logger.info("\u2713 \u955C\u50CF\u6807\u8BB0\u6210\u529F");
1202
+ }
1203
+ function dockerPushImage(params, cwd) {
1204
+ const { image, tag, registryHost, registryNamespace } = params;
1205
+ Logger.info("\u5F00\u59CB\u63A8\u9001 Docker \u955C\u50CF...");
1206
+ CommandRunner.exec(
1207
+ `docker push ${registryHost}/${registryNamespace}/${image}:${tag}`,
1208
+ cwd
1209
+ );
1210
+ Logger.info("\u2713 \u955C\u50CF\u63A8\u9001\u6210\u529F");
1211
+ }
1212
+
1213
+ // src/commands/deploy/lib/backend-deploy/resolve-dockerfile.ts
1214
+ import { existsSync as existsSync6 } from "node:fs";
1215
+ import path3 from "node:path";
1216
+ function resolveDockerBuildPaths(cwd) {
1217
+ const dockerfilePath = path3.join(cwd, "Dockerfile");
1218
+ Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
1219
+ if (!existsSync6(dockerfilePath)) {
1220
+ throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
1221
+ }
1222
+ Logger.info("\u2713 Dockerfile \u5B58\u5728");
1223
+ return dockerfilePath;
1224
+ }
1225
+
1226
+ // src/commands/deploy/lib/backend-deploy/backend-deploy-workflow.ts
1227
+ var BackendDeployWorkflow = class {
1228
+ constructor(params) {
1229
+ this.params = params;
1230
+ }
1231
+ params;
1232
+ /**
1233
+ * @param cwd - 解析相对路径时的基准目录(Dockerfile)
1234
+ */
1235
+ async run(cwd) {
1236
+ resolveDockerBuildPaths(cwd);
1237
+ assertDeployImageTag(this.params.tag);
1238
+ buildDockerImageLocally(this.params, cwd);
1239
+ dockerLoginPrivateRegistry(this.params, cwd);
1240
+ dockerTagImage(this.params, cwd);
1241
+ dockerPushImage(this.params, cwd);
1242
+ await this.deployRemoteContainer(cwd);
1243
+ }
1244
+ async deployRemoteContainer(cwd) {
1245
+ const remoteClient = createDockerodeClient({
1246
+ host: this.params.remoteHost,
1247
+ port: this.params.remotePort,
1248
+ protocol: this.params.remoteProtocol,
1249
+ ca: this.params.remoteCaPath,
1250
+ cert: this.params.remoteCertPath,
1251
+ key: this.params.remoteKeyPath
1252
+ });
1253
+ const serveraddress = `https://${this.params.registryHost}`;
1254
+ const image = `${this.params.registryHost}/${this.params.registryNamespace}/${this.params.image}:${this.params.tag}`;
1255
+ const containerName = `${this.params.image}.${this.params.tag}`;
1256
+ const existingContainerId = await remoteClient.findContainerIdByName(
1257
+ containerName
1258
+ );
1259
+ if (existingContainerId) {
1260
+ Logger.info(`\u8FDC\u7A0B\u5BB9\u5668\u300C${containerName}\u300D\u5DF2\u5B58\u5728\uFF0C\u6B63\u5728\u505C\u6B62\u5E76\u5220\u9664\u5BB9\u5668...`);
1261
+ await remoteClient.stopContainer(existingContainerId);
1262
+ await remoteClient.removeContainer(existingContainerId);
1263
+ }
1264
+ if (await remoteClient.imageExists(image)) {
1265
+ Logger.info("\u8FDC\u7A0B\u5DF2\u5B58\u5728\u540C\u540D\u955C\u50CF\uFF0C\u6B63\u5728\u5220\u9664\u4EE5\u4FBF\u91CD\u65B0\u62C9\u53D6...");
1266
+ await remoteClient.removeImage(image);
1267
+ }
1268
+ await remoteClient.pullImage(image, {
1269
+ username: this.params.registryUser,
1270
+ password: this.params.registryPassword,
1271
+ serveraddress
1272
+ });
1273
+ Logger.success("\u8FDC\u7A0B\u62C9\u53D6\u955C\u50CF\u5B8C\u6210");
1274
+ const envFilePath = path4.resolve(cwd, this.params.envFilePath || ".env");
1275
+ const envObj = loadEnvFromFile(envFilePath);
1276
+ if (this.params.dockerNetwork?.trim()) {
1277
+ Logger.info(`\u8FDC\u7A0B\u5BB9\u5668\u5C06\u52A0\u5165 Docker \u7F51\u7EDC\uFF1A${this.params.dockerNetwork.trim()}`);
1278
+ }
1279
+ const containerId = await remoteClient.createContainer({
1280
+ name: containerName,
1281
+ image,
1282
+ env: toEnvArray(envObj),
1283
+ ...parsePorts(this.params.containerPortsMappings),
1284
+ dockerNetwork: this.params.dockerNetwork
1285
+ });
1286
+ await remoteClient.startContainer(containerId);
1287
+ Logger.success("\u8FDC\u7A0B\u5BB9\u5668\u521B\u5EFA\u5E76\u542F\u52A8\u5B8C\u6210");
1288
+ }
1289
+ };
1290
+
1291
+ // src/commands/deploy/backend.ts
1292
+ function registerDeployBackendCommands(program) {
1293
+ program.command("deploy-backend").description(
1294
+ "\u4F7F\u7528\u672C\u5730 Docker \u6784\u5EFA\u540E\u7AEF\u955C\u50CF\u3001\u63A8\u9001\u5230\u79C1\u6709 Registry\uFF0C\u5E76\u5728\u8FDC\u7A0B Docker \u4E3B\u673A\u4E0A\u62C9\u53D6\u5E76\u8FD0\u884C\u5BB9\u5668\uFF08\u8FDE\u63A5\u4FE1\u606F\u89C1 .apm/apm.config.json \u7684 backendDeploy\uFF09"
1295
+ ).argument("[name]", "\u955C\u50CF tag\uFF08\u53EF\u7528\u5206\u652F\u540D\u6216\u8DEF\u5F84\u672B\u6BB5\uFF09", "online").option(
1296
+ "--dir <path>",
1297
+ "\u6784\u5EFA\u4E0A\u4E0B\u6587\u76EE\u5F55\uFF08\u5185\u542B Dockerfile\uFF09\uFF1B\u5355\u4ED3\u9ED8\u8BA4 servers/api",
1298
+ "servers/api"
1299
+ ).option(
1300
+ "--config <path>",
1301
+ "apm.config.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 .apm/apm.config.json\uFF09"
1302
+ ).option(
1303
+ "-q, --quick",
1304
+ "\u4EC5\u8FDC\u7AEF\u62C9\u53D6\u5E76\u91CD\u542F\u5BB9\u5668\uFF0C\u8DF3\u8FC7\u672C\u5730\u6784\u5EFA\u4E0E\u63A8\u9001\uFF08\u955C\u50CF\u987B\u5DF2\u5728\u4ED3\u5E93\u4E2D\uFF09"
1305
+ ).action(
1306
+ async (tag, opts) => {
1307
+ if (!tag) {
1308
+ console.error("\u8BF7\u4F20\u5165\u6709\u6548\u7684\u540D\u79F0\uFF08\u7B2C\u4E00\u4E2A\u4F4D\u7F6E\u53C2\u6570 <name>\uFF09");
1309
+ process.exit(1);
1310
+ }
1311
+ assertDeployImageTag(tag);
1312
+ const cfg = loadApmConfig({ configPath: opts.config });
1313
+ const fromApm = resolveBackendDeployFromApmConfig(cfg);
1314
+ const dirAbs = path5.resolve(process.cwd(), opts.dir || "servers/api");
1315
+ const params = {
1316
+ image: fromApm.name,
1317
+ tag,
1318
+ registryHost: fromApm.registryHost,
1319
+ registryNamespace: fromApm.registryNamespace,
1320
+ registryUser: fromApm.registryUser,
1321
+ registryPassword: fromApm.registryPassword,
1322
+ remoteHost: fromApm.remoteHost,
1323
+ remotePort: fromApm.remotePort,
1324
+ remoteProtocol: fromApm.remoteProtocol,
1325
+ remoteCaPath: fromApm.caPath ?? "",
1326
+ remoteCertPath: fromApm.certPath ?? "",
1327
+ remoteKeyPath: fromApm.keyPath ?? "",
1328
+ envFilePath: fromApm.envFilePath,
1329
+ containerPortsMappings: fromApm.containerPortsMappings,
1330
+ dockerNetwork: fromApm.dockerNetwork
1331
+ };
1332
+ const workflow = new BackendDeployWorkflow(params);
1333
+ if (opts.quick) {
1334
+ await workflow.deployRemoteContainer(dirAbs);
1335
+ } else {
1336
+ await workflow.run(dirAbs);
1337
+ }
1338
+ console.log("\u540E\u7AEF\u90E8\u7F72\u6210\u529F");
1339
+ }
1340
+ );
1341
+ }
1342
+
1343
+ // src/commands/deploy/frontend.ts
1344
+ import { copyFile, readdir as readdir2, stat } from "node:fs/promises";
1345
+ import path7 from "node:path";
1346
+
1347
+ // src/commands/deploy/lib/load-apm-dotenv.ts
1348
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
1349
+ import { join as join7 } from "node:path";
1350
+ function loadApmDotEnvIfPresent() {
1351
+ const p = join7(WORKSPACE_APM_DIR, ".env");
1352
+ if (!existsSync7(p)) {
1353
+ return;
1354
+ }
1355
+ let text;
1356
+ try {
1357
+ text = readFileSync9(p, "utf8");
1358
+ } catch {
1359
+ return;
1360
+ }
1361
+ for (const line of text.split("\n")) {
1362
+ const t = line.trim();
1363
+ if (!t || t.startsWith("#")) {
1364
+ continue;
1365
+ }
1366
+ const eq = t.indexOf("=");
1367
+ if (eq <= 0) {
1368
+ continue;
1369
+ }
1370
+ const key = t.slice(0, eq).trim();
1371
+ let val = t.slice(eq + 1).trim();
1372
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
1373
+ val = val.slice(1, -1);
1374
+ }
1375
+ if (process.env[key] === void 0) {
1376
+ process.env[key] = val;
1377
+ }
1378
+ }
1379
+ }
1380
+
1381
+ // src/commands/deploy/lib/minio.ts
1382
+ import { statSync as statSync4 } from "node:fs";
1383
+ import { readdir } from "node:fs/promises";
1384
+ import path6 from "node:path";
1385
+ import * as Minio from "minio";
1386
+ var DEFAULT_MAX_FILE_SIZE_MB = 50;
1387
+ async function isDirectoryPath(dir) {
1388
+ try {
1389
+ const st = statSync4(dir);
1390
+ return st.isDirectory();
1391
+ } catch {
1392
+ return false;
1393
+ }
1394
+ }
1395
+ function sanitizeRelativePath(rel) {
1396
+ const norm = rel.replace(/\\/g, "/").replace(/^\/+/, "");
1397
+ const segments = norm.split("/").filter(Boolean);
1398
+ for (const s of segments) {
1399
+ if (s === "." || s === "..") {
1400
+ throw new Error(`\u975E\u6CD5\u76F8\u5BF9\u8DEF\u5F84\u7247\u6BB5\uFF1A${s}`);
1401
+ }
1402
+ }
1403
+ return segments.join("/");
1404
+ }
1405
+ async function collectFiles(root) {
1406
+ const out = [];
1407
+ async function walk(dir, prefix) {
1408
+ const entries = await readdir(dir, { withFileTypes: true });
1409
+ for (const e of entries) {
1410
+ const name = e.name;
1411
+ if (name === "." || name === "..") {
1412
+ continue;
1413
+ }
1414
+ const abs = path6.join(dir, name);
1415
+ const rel = prefix ? `${prefix}/${name}` : name;
1416
+ if (e.isDirectory()) {
1417
+ await walk(abs, rel);
1418
+ } else if (e.isFile()) {
1419
+ const st = statSync4(abs);
1420
+ out.push({
1421
+ absPath: abs,
1422
+ relativePath: rel.replace(/\\/g, "/"),
1423
+ size: st.size
1424
+ });
1425
+ }
1426
+ }
1427
+ }
1428
+ await walk(root, "");
1429
+ out.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1430
+ return out;
1431
+ }
1432
+ async function readArtifactFile(absPath) {
1433
+ const { readFile } = await import("node:fs/promises");
1434
+ return readFile(absPath);
1435
+ }
1436
+ function toMB(bytes) {
1437
+ return Math.round(bytes / (1024 * 1024) * 1e3) / 1e3;
1438
+ }
1439
+ var MIME = {
1440
+ ".html": "text/html; charset=utf-8",
1441
+ ".css": "text/css; charset=utf-8",
1442
+ ".js": "application/javascript; charset=utf-8",
1443
+ ".json": "application/json; charset=utf-8",
1444
+ ".svg": "image/svg+xml",
1445
+ ".png": "image/png",
1446
+ ".jpg": "image/jpeg",
1447
+ ".jpeg": "image/jpeg",
1448
+ ".gif": "image/gif",
1449
+ ".webp": "image/webp",
1450
+ ".woff": "font/woff",
1451
+ ".woff2": "font/woff2",
1452
+ ".ttf": "font/ttf",
1453
+ ".ico": "image/x-icon",
1454
+ ".txt": "text/plain; charset=utf-8",
1455
+ ".map": "application/json"
1456
+ };
1457
+ function detectMimeType(filePath) {
1458
+ const ext = path6.extname(filePath).toLowerCase();
1459
+ return MIME[ext] ?? "";
1460
+ }
1461
+ var MinioClient = class {
1462
+ inner;
1463
+ constructor(opts) {
1464
+ const endPoint = opts.endPoint.replace(/^https?:\/\//i, "").split("/")[0] ?? opts.endPoint;
1465
+ this.inner = new Minio.Client({
1466
+ endPoint,
1467
+ port: opts.port,
1468
+ useSSL: opts.useSSL,
1469
+ accessKey: opts.accessKey,
1470
+ secretKey: opts.secretKey
1471
+ });
1472
+ }
1473
+ async ensureBucket(bucket) {
1474
+ const exists = await this.inner.bucketExists(bucket);
1475
+ if (!exists) {
1476
+ await this.inner.makeBucket(bucket);
1477
+ }
1478
+ }
1479
+ async deleteObjectsByPrefix(bucket, prefix) {
1480
+ const objectsStream = this.inner.listObjectsV2(bucket, prefix, true);
1481
+ const keys = [];
1482
+ await new Promise((resolve4, reject) => {
1483
+ objectsStream.on("data", (obj) => {
1484
+ if (obj.name) {
1485
+ keys.push(obj.name);
1486
+ }
1487
+ });
1488
+ objectsStream.on("error", reject);
1489
+ objectsStream.on("end", resolve4);
1490
+ });
1491
+ const chunkSize = 500;
1492
+ for (let i = 0; i < keys.length; i += chunkSize) {
1493
+ const chunk = keys.slice(i, i + chunkSize);
1494
+ await this.inner.removeObjects(
1495
+ bucket,
1496
+ chunk.map((name) => name)
1497
+ );
1498
+ }
1499
+ }
1500
+ async putObject(bucket, objectKey, body, meta) {
1501
+ await this.inner.putObject(bucket, objectKey, body, body.length, meta);
1502
+ }
1503
+ /** 匿名可读当前桶全部对象(便于静态站点直链) */
1504
+ async setBucketPublicRead(bucket) {
1505
+ const policy = {
1506
+ Version: "2012-10-17",
1507
+ Statement: [
1508
+ {
1509
+ Effect: "Allow",
1510
+ Principal: { AWS: ["*"] },
1511
+ Action: ["s3:GetObject"],
1512
+ Resource: [`arn:aws:s3:::${bucket}/*`]
1513
+ }
1514
+ ]
1515
+ };
1516
+ await this.inner.setBucketPolicy(bucket, JSON.stringify(policy));
1517
+ }
1518
+ };
1519
+
1520
+ // src/commands/deploy/frontend.ts
1521
+ function resolveArtifactNamePrefix(cfg) {
1522
+ const nameRaw = (cfg.name ?? "").trim();
1523
+ if (!nameRaw) {
1524
+ console.error(
1525
+ "\u8BF7\u5728 .apm/apm.config.json \u9876\u5C42\u914D\u7F6E name\uFF08\u4F5C\u4E3A MinIO \u5BF9\u8C61\u952E\u524D\u7F00\uFF09"
1526
+ );
1527
+ process.exit(1);
1528
+ }
1529
+ if (nameRaw.includes(":")) {
1530
+ console.error("apm.config.json \u7684 name \u4E0D\u80FD\u5305\u542B ':'");
1531
+ process.exit(1);
1532
+ }
1533
+ try {
1534
+ return sanitizeRelativePath(nameRaw.replace(/\\/g, "/"));
1535
+ } catch {
1536
+ console.error("apm.config.json \u7684 name \u975E\u6CD5\uFF08\u8DEF\u5F84\u7247\u6BB5\u4E0D\u80FD\u4E3A . \u6216 ..\uFF09");
1537
+ process.exit(1);
1538
+ }
1539
+ }
1540
+ function artifactObjectKey(namePrefix, branchSegment, relativePath) {
1541
+ const base = sanitizeRelativePath(namePrefix);
1542
+ const rel = sanitizeRelativePath(relativePath);
1543
+ return `${base}/${branchSegment}/dist/${rel}`;
1544
+ }
1545
+ function mergeMinioFromEnv(settings) {
1546
+ const ep = process.env.MINIO_ENDPOINT?.trim();
1547
+ const portRaw = process.env.MINIO_PORT?.trim();
1548
+ const sslRaw = process.env.MINIO_USE_SSL?.trim().toLowerCase();
1549
+ const ak = process.env.MINIO_ACCESS_KEY?.trim();
1550
+ const sk = process.env.MINIO_SECRET_KEY?.trim();
1551
+ const bucket = process.env.MINIO_BUCKET?.trim();
1552
+ const port = portRaw ? Number.parseInt(portRaw, 10) : void 0;
1553
+ let useSsl = settings.useSsl;
1554
+ if (sslRaw === "true" || sslRaw === "1") {
1555
+ useSsl = true;
1556
+ }
1557
+ if (sslRaw === "false" || sslRaw === "0") {
1558
+ useSsl = false;
1559
+ }
1560
+ return {
1561
+ ...settings,
1562
+ endpoint: ep || settings.endpoint,
1563
+ port: port !== void 0 && Number.isFinite(port) && port > 0 ? port : settings.port,
1564
+ useSsl,
1565
+ accessKey: ak || settings.accessKey,
1566
+ secretKey: sk || settings.secretKey,
1567
+ bucket: bucket || settings.bucket
1568
+ };
1569
+ }
1570
+ async function ensureArtifactRootIndexHtml(root) {
1571
+ const indexHtmlPath = path7.join(root, "index.html");
1572
+ try {
1573
+ const st = await stat(indexHtmlPath);
1574
+ if (st.isFile()) {
1575
+ return;
1576
+ }
1577
+ } catch {
1578
+ }
1579
+ let entries;
1580
+ try {
1581
+ entries = await readdir2(root, { withFileTypes: true });
1582
+ } catch (e) {
1583
+ console.error(`\u65E0\u6CD5\u8BFB\u53D6\u4EA7\u7269\u76EE\u5F55\uFF1A${root}`, e);
1584
+ process.exit(1);
1585
+ }
1586
+ const dirNames = entries.filter((e) => e.isDirectory()).map((e) => String(e.name)).sort();
1587
+ for (const name of dirNames) {
1588
+ const candidate = path7.join(root, name, "index.html");
1589
+ try {
1590
+ const st = await stat(candidate);
1591
+ if (!st.isFile()) {
1592
+ continue;
1593
+ }
1594
+ } catch {
1595
+ continue;
1596
+ }
1597
+ await copyFile(candidate, indexHtmlPath);
1598
+ console.error(
1599
+ `\u4EA7\u7269\u534F\u8BAE\uFF1A\u5DF2\u5C06 ${path7.join(name, "index.html")} \u590D\u5236\u4E3A\u6839\u76EE\u5F55 index.html`
1600
+ );
1601
+ return;
1602
+ }
1603
+ console.error(
1604
+ `\u4EA7\u7269\u534F\u8BAE\uFF1A\u9700\u5728\u4EA7\u7269\u6839\u76EE\u5F55\u5B58\u5728 index.html\uFF0C\u6216\u5728\u5176\u4E00\u7EA7\u5B50\u76EE\u5F55\u4E2D\u5B58\u5728\u53EF\u590D\u5236\u7684 index.html\uFF08\u672A\u5728 ${root} \u4E0B\u627E\u5230\uFF09`
1605
+ );
1606
+ process.exit(1);
1607
+ }
1608
+ function registerDeployFrontendCommands(program) {
1609
+ program.command("deploy-frontend").description(
1610
+ "\u9012\u5F52\u4E0A\u4F20\u524D\u7AEF\u4EA7\u7269\u76EE\u5F55\u5230 MinIO\uFF0C\u5E76\u5728\u6210\u529F\u540E\u4E3A\u8BE5\u6876\u8BBE\u7F6E\u533F\u540D\u53EF\u8BFB\u7B56\u7565\uFF08\u53EF\u5148\u914D\u7F6E .apm/.env \u7684 MINIO_*\uFF0C\u5DF2\u6709\u73AF\u5883\u53D8\u91CF\u4F18\u5148\uFF09"
1611
+ ).argument("[name]", "\u73AF\u5883\u540D", "online").option(
1612
+ "--dir <path>",
1613
+ "\u4EA7\u7269\u76EE\u5F55\uFF1B\u5355\u4ED3\u9ED8\u8BA4 apps/web/dist\uFF08\u9700\u5148 rush build / vite build\uFF09",
1614
+ "apps/web/dist"
1615
+ ).option(
1616
+ "--config <path>",
1617
+ "apm.config.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 .apm/apm.config.json\uFF09"
1618
+ ).option(
1619
+ "--max-file-size-mb <mb>",
1620
+ `\u5355\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\uFF0C\u9ED8\u8BA4 ${DEFAULT_MAX_FILE_SIZE_MB}`,
1621
+ (v) => Number.parseInt(String(v), 10)
1622
+ ).action(
1623
+ async (name, opts) => {
1624
+ loadApmDotEnvIfPresent();
1625
+ const cfg = loadApmConfig({ configPath: opts.config });
1626
+ const namePrefix = resolveArtifactNamePrefix(cfg);
1627
+ const settings = mergeMinioFromEnv(
1628
+ resolveFrontendDeployFromApmConfig(cfg)
1629
+ );
1630
+ const minio = new MinioClient({
1631
+ endPoint: settings.endpoint,
1632
+ port: settings.port,
1633
+ useSSL: settings.useSsl,
1634
+ accessKey: settings.accessKey,
1635
+ secretKey: settings.secretKey
1636
+ });
1637
+ const bucket = settings.bucket;
1638
+ const root = path7.resolve(process.cwd(), opts.dir || "apps/web/dist");
1639
+ if (!await isDirectoryPath(root)) {
1640
+ console.error(`\u4EA7\u7269\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${root}`);
1641
+ process.exit(1);
1642
+ }
1643
+ await ensureArtifactRootIndexHtml(root);
1644
+ const maxBytes = Math.max(
1645
+ 1,
1646
+ Number.isFinite(opts.maxFileSizeMb) ? Math.floor(
1647
+ (opts.maxFileSizeMb ?? DEFAULT_MAX_FILE_SIZE_MB) * 1024 * 1024
1648
+ ) : DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024
1649
+ );
1650
+ const files = await collectFiles(root);
1651
+ if (files.length === 0) {
1652
+ console.error(`\u76EE\u5F55\u4E3A\u7A7A\uFF1A${root}`);
1653
+ process.exit(1);
1654
+ }
1655
+ let uploaded = 0;
1656
+ let totalBytes = 0;
1657
+ console.error(`\u5F00\u59CB\u76F4\u8FDE MinIO \u4E0A\u4F20\uFF0C\u5171 ${files.length} \u4E2A\u6587\u4EF6\u2026`);
1658
+ await minio.ensureBucket(bucket);
1659
+ await minio.deleteObjectsByPrefix(
1660
+ bucket,
1661
+ `${namePrefix}/${name}/dist/`
1662
+ );
1663
+ for (const f of files) {
1664
+ if (f.size > maxBytes) {
1665
+ console.error(
1666
+ `\u6587\u4EF6\u8FC7\u5927\uFF0C\u5DF2\u62D2\u7EDD\uFF1A${f.relativePath}\uFF08${toMB(f.size)}MB > ${toMB(
1667
+ maxBytes
1668
+ )}MB\uFF09`
1669
+ );
1670
+ process.exit(1);
1671
+ }
1672
+ const buf = await readArtifactFile(f.absPath);
1673
+ const cleanRel = sanitizeRelativePath(f.relativePath);
1674
+ const objectKey = artifactObjectKey(namePrefix, name, cleanRel);
1675
+ const mimeType = detectMimeType(f.relativePath).trim().toLowerCase() || "application/octet-stream";
1676
+ await minio.putObject(bucket, objectKey, buf, {
1677
+ "Content-Type": mimeType
1678
+ });
1679
+ uploaded += 1;
1680
+ totalBytes += buf.length;
1681
+ console.error(
1682
+ `[${uploaded}/${files.length}] ${f.relativePath} -> ${objectKey}`
1683
+ );
1684
+ }
1685
+ console.error("\u6B63\u5728\u8BBE\u7F6E\u6876\u7B56\u7565\uFF08\u533F\u540D\u53EF\u8BFB\u5BF9\u8C61\uFF0C\u4FBF\u4E8E HTTP \u76F4\u94FE\uFF09\u2026");
1686
+ await minio.setBucketPublicRead(bucket);
1687
+ console.log(
1688
+ JSON.stringify(
1689
+ {
1690
+ ok: true,
1691
+ namePrefix,
1692
+ envName: name,
1693
+ bucket,
1694
+ fileCount: uploaded,
1695
+ totalBytes,
1696
+ bucketPublicRead: true
1697
+ },
1698
+ null,
1699
+ 2
1700
+ )
1701
+ );
1702
+ }
1703
+ );
1704
+ }
1705
+
1706
+ // src/commands/deploy/index.ts
1707
+ function registerDeployCommands(program) {
1708
+ registerDeployBackendCommands(program);
1709
+ registerDeployFrontendCommands(program);
1710
+ }
1711
+
587
1712
  // src/index.ts
588
1713
  function readCliVersion() {
589
1714
  try {
590
1715
  const dir = dirname2(fileURLToPath2(import.meta.url));
591
- const pkgPath = join6(dir, "..", "package.json");
592
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1716
+ const pkgPath = join8(dir, "..", "package.json");
1717
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
593
1718
  return pkg.version ?? "0.0.0";
594
1719
  } catch {
595
1720
  return "0.0.0";
@@ -617,6 +1742,11 @@ function buildProgram() {
617
1742
  program.command("pull").description("GET /api/cli/requirements/pull\uFF0C\u540C\u6B65\u6570\u636E\u5230 .apm \u76EE\u5F55").argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
618
1743
  await runPull(requirementId);
619
1744
  });
1745
+ program.command("upload-artifact").description(
1746
+ "\u5148\u6E05\u7A7A\u8BE5\u9700\u6C42\u5728\u5E73\u53F0\u4E0A\u7684\u4EA7\u7269\u6587\u6863\uFF0C\u518D\u5C06 .apm/workitems/<\u9700\u6C42ID> \u4E0B Markdown \u540C\u6B65\u4E0A\u53BB\uFF08\u6392\u9664 pull \u7CFB\u7EDF\u6587\u4EF6\uFF09"
1747
+ ).argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
1748
+ await runUploadArtifact(requirementId);
1749
+ });
620
1750
  program.command("branch").description(
621
1751
  "\u5207\u6362\u6216\u521B\u5EFA\u9700\u6C42\u5206\u652F feat/req-<ID>\uFF1A\u8FDC\u7AEF\u5B58\u5728\u5219\u62C9\u53D6\u6700\u65B0\uFF1B\u8FDC\u7AEF\u5C1A\u65E0\u8BE5\u5206\u652F\u4E14\u672C\u5730\u4E5F\u65E0\u540C\u540D\u5206\u652F\u65F6\uFF0C\u9700\u5DF2 login\uFF0C\u5E76\u7531\u5E73\u53F0\u6839\u636E\u5F53\u524D\u76EE\u5F55\u8DEF\u5F84\u89E3\u6790\u4ED3\u5E93\u57FA\u7EBF\u5206\u652F\u540E\u4ECE origin \u68C0\u51FA\u518D\u63A8\u9001\uFF1B\u6709\u672C\u5730\u672A\u63D0\u4EA4\u6539\u52A8\u65F6\u5728\u975E\u76EE\u6807\u5206\u652F\u5148 stash\uFF08\u4E0D\u81EA\u52A8\u6062\u590D\uFF09\uFF0C\u5728\u76EE\u6807\u5206\u652F\u5219\u5148 commit"
622
1752
  ).argument("<requirementId>", "\u9700\u6C42 ID").option(
@@ -641,6 +1771,7 @@ function buildProgram() {
641
1771
  program.command("update-dev-status").description("POST /api/cli/requirements/update-dev-status").argument("<requirementId>", "\u9700\u6C42 ID").requiredOption("--status <status>", "\u6210\u5458\u5F00\u53D1\u72B6\u6001\uFF08\u81EA\u7531\u6587\u672C\uFF09").action(async (requirementId, options) => {
642
1772
  await runUpdateDevStatus(requirementId, options.status);
643
1773
  });
1774
+ registerDeployCommands(program);
644
1775
  return program;
645
1776
  }
646
1777
  async function main() {