ai-project-manage-cli 3.0.3 → 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 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 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
 
@@ -95,6 +95,10 @@ var requestConfig = {
95
95
  method: "GET",
96
96
  path: "/cli/requirements/pull"
97
97
  }),
98
+ branchBaseline: defineEndpoint({
99
+ method: "GET",
100
+ path: "/cli/requirements/branch-baseline"
101
+ }),
98
102
  comment: defineEndpoint({
99
103
  method: "POST",
100
104
  path: "/cli/requirements/comment"
@@ -307,6 +311,144 @@ async function runLogin(opts) {
307
311
  console.log(JSON.stringify({ userId: cfg.userId, baseUrl: cfg.baseUrl }, null, 2));
308
312
  }
309
313
 
314
+ // src/commands/branch.ts
315
+ import { execFile } from "child_process";
316
+ import { resolve as resolve2 } from "path";
317
+ import { promisify } from "util";
318
+ var execFileAsync = promisify(execFile);
319
+ async function fetchBaselineBranchFromApi(requirementId, cwd) {
320
+ const cfg = await ensureLoggedConfig();
321
+ const api = createApmApiClient(cfg);
322
+ const workdirPath = resolve2(cwd);
323
+ const { baselineBranch } = await api.cliRequirements.branchBaseline({
324
+ requirementId,
325
+ workdirPath
326
+ });
327
+ const name = baselineBranch.trim();
328
+ if (!name) {
329
+ throw new Error("[apm] \u5E73\u53F0\u8FD4\u56DE\u7684\u57FA\u7EBF\u5206\u652F\u540D\u4E3A\u7A7A");
330
+ }
331
+ return name;
332
+ }
333
+ function branchNameForRequirement(requirementId) {
334
+ const id = requirementId.trim();
335
+ if (!id) {
336
+ throw new Error("[apm] \u9700\u6C42 ID \u4E0D\u80FD\u4E3A\u7A7A");
337
+ }
338
+ if (/[\s/\\]/.test(id)) {
339
+ throw new Error(
340
+ "[apm] \u9700\u6C42 ID \u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u6216\u8DEF\u5F84\u5206\u9694\u7B26\uFF0C\u8BF7\u4F7F\u7528\u5B57\u6BCD\u3001\u6570\u5B57\u3001._- \u7B49"
341
+ );
342
+ }
343
+ return `feat/req-${id}`;
344
+ }
345
+ async function execGit(cwd, args, quiet) {
346
+ try {
347
+ const { stdout, stderr } = await execFileAsync("git", args, {
348
+ cwd,
349
+ encoding: "utf8",
350
+ maxBuffer: 10 * 1024 * 1024
351
+ });
352
+ if (!quiet && stderr.trim()) {
353
+ process.stderr.write(stderr);
354
+ }
355
+ return stdout;
356
+ } catch (err) {
357
+ const e = err;
358
+ const detail = (e.stderr ?? e.message ?? String(err)).trim();
359
+ throw new Error(
360
+ `[apm] git ${args.join(" ")} \u5931\u8D25${detail ? `: ${detail}` : ""}`
361
+ );
362
+ }
363
+ }
364
+ async function ensureGitRepo(cwd) {
365
+ await execGit(cwd, ["rev-parse", "--git-dir"], true);
366
+ }
367
+ async function getCurrentBranch(cwd) {
368
+ const name = (await execGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], true)).trim();
369
+ return name;
370
+ }
371
+ async function isWorkingTreeDirty(cwd) {
372
+ const out = await execGit(cwd, ["status", "--porcelain"], true);
373
+ return out.trim().length > 0;
374
+ }
375
+ async function remoteHeadBranchExists(cwd, branch) {
376
+ const out = await execGit(
377
+ cwd,
378
+ ["ls-remote", "--heads", "origin", branch],
379
+ true
380
+ );
381
+ return out.trim().length > 0;
382
+ }
383
+ async function localBranchExists(cwd, branch) {
384
+ try {
385
+ await execGit(
386
+ cwd,
387
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
388
+ true
389
+ );
390
+ return true;
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+ async function runBranch(requirementId, options = {}) {
396
+ const cwd = options.cwd ?? process.cwd();
397
+ const branch = branchNameForRequirement(requirementId);
398
+ const commitMessage = options.message?.trim() || `chore(apm): \u540C\u6B65\u5DE5\u4F5C\u533A (${branch})`;
399
+ await ensureGitRepo(cwd);
400
+ const current = await getCurrentBranch(cwd);
401
+ const dirty = await isWorkingTreeDirty(cwd);
402
+ if (dirty) {
403
+ if (current === branch) {
404
+ await execGit(cwd, ["add", "-A"]);
405
+ await execGit(cwd, ["commit", "-m", commitMessage]);
406
+ } else {
407
+ await execGit(cwd, [
408
+ "stash",
409
+ "push",
410
+ "-u",
411
+ "-m",
412
+ `apm: switch to ${branch}`
413
+ ]);
414
+ }
415
+ }
416
+ const remoteExists = await remoteHeadBranchExists(cwd, branch);
417
+ if (remoteExists) {
418
+ await execGit(cwd, ["fetch", "origin", branch]);
419
+ const hasLocal = await localBranchExists(cwd, branch);
420
+ if (hasLocal) {
421
+ await execGit(cwd, ["checkout", branch]);
422
+ await execGit(cwd, ["pull", "--no-edit"]);
423
+ } else {
424
+ await execGit(cwd, ["checkout", "-b", branch, `origin/${branch}`]);
425
+ }
426
+ } else {
427
+ const onBranch = await getCurrentBranch(cwd) === branch;
428
+ if (!onBranch) {
429
+ const hasLocal = await localBranchExists(cwd, branch);
430
+ if (hasLocal) {
431
+ await execGit(cwd, ["checkout", branch]);
432
+ } else {
433
+ const baselineBranch = await fetchBaselineBranchFromApi(
434
+ requirementId,
435
+ cwd
436
+ );
437
+ await execGit(cwd, ["fetch", "origin", baselineBranch]);
438
+ await execGit(cwd, [
439
+ "checkout",
440
+ "-b",
441
+ branch,
442
+ `origin/${baselineBranch}`
443
+ ]);
444
+ }
445
+ }
446
+ await execGit(cwd, ["push", "-u", "origin", branch]);
447
+ }
448
+ console.log(`[apm] \u5DF2\u5C31\u7EEA\u5206\u652F ${branch}`);
449
+ return branch;
450
+ }
451
+
310
452
  // src/commands/pull.ts
311
453
  import { writeFileSync as writeFileSync3 } from "fs";
312
454
  import { join as join4 } from "path";
@@ -359,13 +501,13 @@ async function runPull(requirementId) {
359
501
  const data = await api.cliRequirements.pull({ requirementId });
360
502
  const WORKITEMS_DIR = join4(WORKSPACE_APM_DIR, "workitems", requirementId);
361
503
  await ensureDirExists(WORKITEMS_DIR);
362
- const req = data.requirement;
504
+ const req2 = data.requirement;
363
505
  const statusYaml = yamlStringify(
364
506
  {
365
- id: req.id,
366
- status: req.status,
367
- title: req.title,
368
- env: req.envName || "",
507
+ id: req2.id,
508
+ status: req2.status,
509
+ title: req2.title,
510
+ env: req2.envName || "",
369
511
  tasks: tasksForStatusYaml(data.tasks ?? [])
370
512
  },
371
513
  { lineWidth: 0 }
@@ -376,7 +518,7 @@ async function runPull(requirementId) {
376
518
  `,
377
519
  "utf8"
378
520
  );
379
- writeFileSync3(join4(WORKITEMS_DIR, "prd.md"), req.content || "", "utf8");
521
+ writeFileSync3(join4(WORKITEMS_DIR, "prd.md"), req2.content || "", "utf8");
380
522
  const reviews = data.reviews ?? [];
381
523
  const reviewsXml = [
382
524
  "<reviews>",
@@ -442,12 +584,1021 @@ async function runUpdateStatus(requirementId, status) {
442
584
  console.log(JSON.stringify(data, null, 2));
443
585
  }
444
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
+
445
1596
  // src/index.ts
446
1597
  function readCliVersion() {
447
1598
  try {
448
1599
  const dir = dirname2(fileURLToPath2(import.meta.url));
449
- const pkgPath = join6(dir, "..", "package.json");
450
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1600
+ const pkgPath = join7(dir, "..", "package.json");
1601
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
451
1602
  return pkg.version ?? "0.0.0";
452
1603
  } catch {
453
1604
  return "0.0.0";
@@ -475,6 +1626,16 @@ function buildProgram() {
475
1626
  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) => {
476
1627
  await runPull(requirementId);
477
1628
  });
1629
+ program.command("branch").description(
1630
+ "\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"
1631
+ ).argument("<requirementId>", "\u9700\u6C42 ID").option(
1632
+ "-m, --message <text>",
1633
+ "\u5DF2\u5728\u76EE\u6807\u5206\u652F\u4E14\u9700\u63D0\u4EA4\u672C\u5730\u6539\u52A8\u65F6\u4F7F\u7528\u7684\u63D0\u4EA4\u8BF4\u660E\uFF08\u9ED8\u8BA4\u81EA\u52A8\u751F\u6210\uFF09"
1634
+ ).action(
1635
+ async (requirementId, opts) => {
1636
+ await runBranch(requirementId, { message: opts.message });
1637
+ }
1638
+ );
478
1639
  program.command("comment").description("POST /api/cli/requirements/comment\uFF08\u6B63\u6587\u6765\u81EA\u6587\u4EF6\uFF09").argument("<requirementId>", "\u9700\u6C42 ID").requiredOption("--file <path>", "\u8BC4\u8BBA\u6B63\u6587\u6587\u4EF6\u8DEF\u5F84").option("--model <model>", "\u8BC4\u8BBA\u6A21\u578B").action(
479
1640
  async (requirementId, options) => {
480
1641
  await runComment(requirementId, options.file, options.model);
@@ -489,6 +1650,7 @@ function buildProgram() {
489
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) => {
490
1651
  await runUpdateDevStatus(requirementId, options.status);
491
1652
  });
1653
+ registerDeployCommands(program);
492
1654
  return program;
493
1655
  }
494
1656
  async function main() {