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