@mittwald/cli 1.11.2 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/commands/app/copy.d.ts +1 -0
  2. package/dist/commands/app/copy.js +6 -1
  3. package/dist/commands/app/create/node.d.ts +1 -1
  4. package/dist/commands/app/create/php-worker.d.ts +1 -1
  5. package/dist/commands/app/create/php.d.ts +1 -1
  6. package/dist/commands/app/create/python.d.ts +1 -1
  7. package/dist/commands/app/create/static.d.ts +1 -1
  8. package/dist/commands/app/install/contao.d.ts +1 -1
  9. package/dist/commands/app/install/joomla.d.ts +1 -1
  10. package/dist/commands/app/install/matomo.d.ts +1 -1
  11. package/dist/commands/app/install/nextcloud.d.ts +1 -1
  12. package/dist/commands/app/install/shopware5.d.ts +1 -1
  13. package/dist/commands/app/install/shopware6.d.ts +1 -1
  14. package/dist/commands/app/install/typo3.d.ts +1 -1
  15. package/dist/commands/app/install/wordpress.d.ts +1 -1
  16. package/dist/commands/app/open.d.ts +3 -0
  17. package/dist/commands/app/open.js +35 -9
  18. package/dist/commands/container/port-forward.js +2 -2
  19. package/dist/commands/container/run.d.ts +9 -0
  20. package/dist/commands/container/run.js +42 -4
  21. package/dist/commands/container/update.js +2 -2
  22. package/dist/commands/database/mysql/create.js +1 -1
  23. package/dist/lib/resources/app/Installer.d.ts +1 -1
  24. package/dist/lib/resources/app/Installer.js +7 -1
  25. package/dist/lib/resources/app/flags.d.ts +1 -0
  26. package/dist/lib/resources/app/flags.js +6 -0
  27. package/dist/lib/resources/app/install.d.ts +1 -0
  28. package/dist/lib/resources/app/install.js +1 -0
  29. package/dist/lib/resources/database/mysql/flags.js +1 -1
  30. package/dist/lib/units/PortMapping.d.ts +2 -0
  31. package/dist/lib/units/PortMapping.js +21 -6
  32. package/dist/lib/units/PortMapping.test.js +10 -0
  33. package/package.json +1 -1
@@ -10,6 +10,7 @@ export declare class Copy extends ExecRenderBaseCommand<typeof Copy, Result> {
10
10
  };
11
11
  static flags: {
12
12
  description: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ "install-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  };
15
16
  protected exec(): Promise<Result>;
@@ -14,16 +14,21 @@ export class Copy extends ExecRenderBaseCommand {
14
14
  summary: "set a description for the new app installation",
15
15
  required: true,
16
16
  }),
17
+ "install-path": Flags.string({
18
+ summary: "set the installation path for the new app installation; if omitted, this will default to an autogenerated directory name",
19
+ required: false,
20
+ }),
17
21
  };
18
22
  async exec() {
19
23
  const appInstallationId = await this.withAppInstallationId(Copy);
20
- const { description } = this.flags;
24
+ const { description, "install-path": installPath } = this.flags;
21
25
  const p = makeProcessRenderer(this.flags, "Copying app installation");
22
26
  const result = await p.runStep("requesting app copy", async () => {
23
27
  const r = await this.apiClient.app.requestAppinstallationCopy({
24
28
  appInstallationId,
25
29
  data: {
26
30
  description,
31
+ installationPath: installPath,
27
32
  },
28
33
  });
29
34
  assertStatus(r, 201);
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const nodeInstaller: AppInstaller<"site-title" | "entrypoint">;
5
5
  export default class InstallNode extends ExecRenderBaseCommand<typeof InstallNode, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const phpWorkerInstaller: AppInstaller<"site-title" | "entrypoint">;
5
5
  export default class InstallPhpWorker extends ExecRenderBaseCommand<typeof InstallPhpWorker, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const phpInstaller: AppInstaller<"site-title" | "document-root">;
5
5
  export default class InstallPhp extends ExecRenderBaseCommand<typeof InstallPhp, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("document-root" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("document-root" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const pythonInstaller: AppInstaller<"site-title" | "entrypoint">;
5
5
  export default class InstallPython extends ExecRenderBaseCommand<typeof InstallPython, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const staticInstaller: AppInstaller<"site-title" | "document-root">;
5
5
  export default class InstallStatic extends ExecRenderBaseCommand<typeof InstallStatic, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("document-root" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("document-root" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { AppInstallationResult } from "../../../lib/resources/app/Installer.js";
4
4
  export default class InstallContao extends ExecRenderBaseCommand<typeof InstallContao, AppInstallationResult> {
5
5
  static description: string;
6
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | ("wait" | "wait-timeout" | "site-title"))[]>>;
6
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
7
7
  protected exec(): Promise<AppInstallationResult>;
8
8
  protected render(result: AppInstallationResult): React.ReactNode;
9
9
  }
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { AppInstallationResult } from "../../../lib/resources/app/Installer.js";
4
4
  export default class InstallJoomla extends ExecRenderBaseCommand<typeof InstallJoomla, AppInstallationResult> {
5
5
  static description: string;
6
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | ("wait" | "wait-timeout" | "site-title"))[]>>;
6
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
7
7
  protected exec(): Promise<AppInstallationResult>;
8
8
  protected render(result: AppInstallationResult): React.ReactNode;
9
9
  }
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { AppInstallationResult } from "../../../lib/resources/app/Installer.js";
4
4
  export default class InstallMatomo extends ExecRenderBaseCommand<typeof InstallMatomo, AppInstallationResult> {
5
5
  static description: string;
6
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | ("wait" | "wait-timeout" | "site-title"))[]>>;
6
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
7
7
  protected exec(): Promise<AppInstallationResult>;
8
8
  protected render(result: AppInstallationResult): React.ReactNode;
9
9
  }
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { AppInstallationResult } from "../../../lib/resources/app/Installer.js";
4
4
  export default class InstallNextcloud extends ExecRenderBaseCommand<typeof InstallNextcloud, AppInstallationResult> {
5
5
  static description: string;
6
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | ("wait" | "wait-timeout" | "site-title"))[]>>;
6
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
7
7
  protected exec(): Promise<AppInstallationResult>;
8
8
  protected render(result: AppInstallationResult): React.ReactNode;
9
9
  }
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { AppInstallationResult } from "../../../lib/resources/app/Installer.js";
4
4
  export default class InstallShopware5 extends ExecRenderBaseCommand<typeof InstallShopware5, AppInstallationResult> {
5
5
  static description: string;
6
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | "shop-email" | "shop-lang" | "shop-currency" | ("wait" | "wait-timeout" | "site-title"))[]>>;
6
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | "shop-email" | "shop-lang" | "shop-currency" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
7
7
  protected exec(): Promise<AppInstallationResult>;
8
8
  protected render(result: AppInstallationResult): React.ReactNode;
9
9
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const shopware6Installer: AppInstaller<"version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | "site-title" | "shop-email" | "shop-lang" | "shop-currency">;
5
5
  export default class InstallShopware6 extends ExecRenderBaseCommand<typeof InstallShopware6, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | "shop-email" | "shop-lang" | "shop-currency" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "admin-firstname" | "admin-lastname" | "shop-email" | "shop-lang" | "shop-currency" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const typo3Installer: AppInstaller<"version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "site-title" | "install-mode">;
5
5
  export default class InstallTYPO3 extends ExecRenderBaseCommand<typeof InstallTYPO3, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "install-mode" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "install-mode" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -4,7 +4,7 @@ import { AppInstallationResult, AppInstaller } from "../../../lib/resources/app/
4
4
  export declare const wordpressInstaller: AppInstaller<"version" | "host" | "admin-user" | "admin-email" | "admin-pass" | "site-title">;
5
5
  export default class InstallWordPress extends ExecRenderBaseCommand<typeof InstallWordPress, AppInstallationResult> {
6
6
  static description: string;
7
- static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | ("wait" | "wait-timeout" | "site-title"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("version" | "host" | "admin-user" | "admin-email" | "admin-pass" | ("wait" | "wait-timeout" | "site-title" | "install-path"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -5,5 +5,8 @@ export declare class Open extends ExtendedBaseCommand<typeof Open> {
5
5
  static args: {
6
6
  "installation-id": import("@oclif/core/interfaces").Arg<string>;
7
7
  };
8
+ static flags: {
9
+ backend: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
8
11
  run(): Promise<void>;
9
12
  }
@@ -3,27 +3,53 @@ import open from "open";
3
3
  import { appInstallationArgs, withAppInstallationId, } from "../../lib/resources/app/flags.js";
4
4
  import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
5
5
  import buildAppURLsFromIngressList from "../../lib/resources/app/buildAppURLsFromIngressList.js";
6
+ import { Flags } from "@oclif/core";
6
7
  export class Open extends ExtendedBaseCommand {
7
8
  static summary = "Open an app installation in the browser.";
8
9
  static description = "This command opens an app installation in the browser. For this to work, there needs to be at least one virtual host linked to the app installation.";
9
10
  static args = { ...appInstallationArgs };
11
+ static flags = {
12
+ backend: Flags.boolean({
13
+ summary: "open the backend of the app installation",
14
+ description: "If this flag is set, the backend of the app installation will be opened instead of the frontend. This flag is only available for some types of apps (like PHP and Node.js).",
15
+ default: false,
16
+ required: false,
17
+ }),
18
+ };
10
19
  async run() {
11
20
  const appInstallationId = await withAppInstallationId(this.apiClient, Open, this.flags, this.args, this.config);
12
21
  const installation = await this.apiClient.app.getAppinstallation({
13
22
  appInstallationId,
14
23
  });
15
24
  assertStatus(installation, 200);
16
- const domains = await this.apiClient.domain.ingressListIngresses({
17
- queryParameters: {
18
- projectId: installation.data.projectId,
19
- },
20
- });
21
- assertStatus(domains, 200);
22
- const urls = buildAppURLsFromIngressList(domains.data, installation.data.id);
25
+ const [appVersion, domains] = await Promise.all([
26
+ (async () => {
27
+ const appVersion = await this.apiClient.app.getAppversion({
28
+ appId: installation.data.appId,
29
+ appVersionId: installation.data.appVersion.desired,
30
+ });
31
+ assertStatus(appVersion, 200);
32
+ return appVersion.data;
33
+ })(),
34
+ (async () => {
35
+ const domains = await this.apiClient.domain.ingressListIngresses({
36
+ queryParameters: {
37
+ projectId: installation.data.projectId,
38
+ },
39
+ });
40
+ assertStatus(domains, 200);
41
+ return domains.data;
42
+ })(),
43
+ ]);
44
+ const urls = buildAppURLsFromIngressList(domains, installation.data.id);
23
45
  if (urls.length === 0) {
24
46
  throw new Error("This app installation is not linked to any virtual hosts.");
25
47
  }
26
- console.log("opening " + urls[0]);
27
- await open(urls[0]);
48
+ let url = urls[0];
49
+ if (this.flags.backend && appVersion.backendPathTemplate) {
50
+ url = appVersion.backendPathTemplate.replace("{domain}", url.replace(/\/$/, ""));
51
+ }
52
+ console.log("opening " + url);
53
+ await open(url);
28
54
  }
29
55
  }
@@ -28,8 +28,8 @@ export class PortForward extends ExecRenderBaseCommand {
28
28
  required: true,
29
29
  }),
30
30
  port: PortMapping.arg({
31
- summary: "Port mapping in the format 'local-port:container-port'",
32
- description: "Specifies the port mapping between your local machine and the container. Format: 'local-port:container-port'. If not specified, available ports will be detected automatically.",
31
+ summary: "Port mapping in the format 'local-port:container-port' or 'port'",
32
+ description: "Specifies the port mapping between your local machine and the container. Format: 'local-port:container-port' or just 'port' (in which case the same port is used locally and in the container). If not specified, available ports will be detected automatically.",
33
33
  required: false,
34
34
  }),
35
35
  };
@@ -19,6 +19,8 @@ export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
19
19
  "publish-all": import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
20
  volume: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
21
  "create-volumes": import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ cpus: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ memory: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
24
  "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
23
25
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
26
  };
@@ -46,6 +48,13 @@ export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
46
48
  * or undefined if no specific command is set.
47
49
  */
48
50
  private buildContainerCommand;
51
+ /**
52
+ * Builds the deploy.resources structure from command line flags
53
+ *
54
+ * @returns The deploy configuration with resource limits, or undefined if no
55
+ * limits are specified
56
+ */
57
+ private buildDeployResources;
49
58
  /**
50
59
  * Builds a container service request from command line arguments and image
51
60
  * metadata
@@ -48,10 +48,10 @@ export class Run extends ExecRenderBaseCommand {
48
48
  required: false,
49
49
  }),
50
50
  publish: Flags.string({
51
- summary: "publish a container's port(s) to the host",
52
- description: "Map a container's port to a port on the host system. " +
53
- "Format: <host-port>:<container-port> or just <container-port> (in which case the host port will be automatically assigned). " +
54
- "For example, --publish 8080:80 maps port 80 in the container to port 8080 on the host. " +
51
+ summary: "publish a container's port(s)",
52
+ description: "Expose a container's port within the cluster. " +
53
+ "Format: <cluster-port>:<container-port> or just <port> (in which case the same port is used for both cluster and container). " +
54
+ "For example, --publish 8080:80 maps port 80 in the container to port 8080 within the cluster, while --publish 8080 exposes port 8080 as port 8080. " +
55
55
  "Use multiple --publish flags to publish multiple ports.\n\n" +
56
56
  "NOTE: Please note that the usual shorthand -p is not supported for this flag, as it would conflict with the --project flag.",
57
57
  required: false,
@@ -83,6 +83,19 @@ export class Run extends ExecRenderBaseCommand {
83
83
  required: false,
84
84
  default: false,
85
85
  }),
86
+ cpus: Flags.string({
87
+ summary: "set CPU limit for the container",
88
+ description: "Specify the number of CPUs available to the container (e.g., '0.5', '1', '2'). " +
89
+ "This is equivalent to the docker run --cpus flag or the deploy.resources.limits.cpus field in docker-compose.",
90
+ required: false,
91
+ }),
92
+ memory: Flags.string({
93
+ summary: "set memory limit for the container",
94
+ description: "Specify the maximum amount of memory the container can use (e.g., '512m', '1g', '2g'). " +
95
+ "This is equivalent to the docker run --memory flag or the deploy.resources.limits.memory field in docker-compose.",
96
+ required: false,
97
+ char: "m",
98
+ }),
86
99
  };
87
100
  static args = {
88
101
  image: Args.string({
@@ -180,6 +193,29 @@ export class Run extends ExecRenderBaseCommand {
180
193
  const command = [this.args.command, ...this.argv.slice(firstArg)];
181
194
  return command;
182
195
  }
196
+ /**
197
+ * Builds the deploy.resources structure from command line flags
198
+ *
199
+ * @returns The deploy configuration with resource limits, or undefined if no
200
+ * limits are specified
201
+ */
202
+ buildDeployResources() {
203
+ if (!this.flags.cpus && !this.flags.memory) {
204
+ return undefined;
205
+ }
206
+ const limits = {};
207
+ if (this.flags.cpus) {
208
+ limits.cpus = this.flags.cpus;
209
+ }
210
+ if (this.flags.memory) {
211
+ limits.memory = this.flags.memory;
212
+ }
213
+ return {
214
+ resources: {
215
+ limits,
216
+ },
217
+ };
218
+ }
183
219
  /**
184
220
  * Builds a container service request from command line arguments and image
185
221
  * metadata
@@ -198,6 +234,7 @@ export class Run extends ExecRenderBaseCommand {
198
234
  const environment = await parseEnvironmentVariables(this.flags.env, this.flags["env-file"]);
199
235
  const ports = getPortMappings(imageMeta, this.flags["publish-all"], this.flags.publish);
200
236
  const volumes = this.flags.volume;
237
+ const deploy = this.buildDeployResources();
201
238
  return {
202
239
  image,
203
240
  command,
@@ -206,6 +243,7 @@ export class Run extends ExecRenderBaseCommand {
206
243
  environment,
207
244
  ports,
208
245
  volumes,
246
+ deploy,
209
247
  };
210
248
  }
211
249
  async getImageAndMeta(projectId) {
@@ -50,8 +50,8 @@ export class Update extends ExecRenderBaseCommand {
50
50
  }),
51
51
  publish: Flags.string({
52
52
  summary: "update the container's port mappings",
53
- description: "Map a container's port to a port on the host system. " +
54
- "Format: <host-port>:<container-port> or just <container-port> (in which case the host port will be automatically assigned). " +
53
+ description: "Expose a container's port within the cluster. " +
54
+ "Format: <cluster-port>:<container-port> or just <port> (in which case the same port is used for both cluster and container). " +
55
55
  "Use multiple -p flags to publish multiple ports.",
56
56
  required: false,
57
57
  multiple: true,
@@ -30,7 +30,7 @@ export class Create extends ExecRenderBaseCommand {
30
30
  default: "utf8mb4",
31
31
  }),
32
32
  "user-password": Flags.string({
33
- summary: "the password to use for the default user (env: MYSQL_PWD)",
33
+ summary: "the password to use for the default user",
34
34
  env: "MYSQL_PWD",
35
35
  }),
36
36
  "user-external": Flags.boolean({
@@ -5,7 +5,7 @@ import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
5
5
  import { Config } from "@oclif/core";
6
6
  type AppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
7
7
  type AppInstallation = MittwaldAPIV2.Components.Schemas.AppAppInstallation;
8
- type ImplicitDefaultFlag = "wait" | "wait-timeout" | "site-title";
8
+ type ImplicitDefaultFlag = "wait" | "wait-timeout" | "site-title" | "install-path";
9
9
  export interface AppInstallationResult {
10
10
  appInstallation: AppInstallation;
11
11
  appVersion: AppVersion;
@@ -25,7 +25,13 @@ export class AppInstaller {
25
25
  this.description = AppInstaller.makeDescription(appName);
26
26
  }
27
27
  get flags() {
28
- const flags = provideSupportedFlags([...this.appSupportedFlags, "wait", "wait-timeout", "site-title"], this.appName);
28
+ const flags = provideSupportedFlags([
29
+ ...this.appSupportedFlags,
30
+ "wait",
31
+ "wait-timeout",
32
+ "site-title",
33
+ "install-path",
34
+ ], this.appName);
29
35
  if (this.mutateFlags) {
30
36
  this.mutateFlags(flags);
31
37
  }
@@ -22,6 +22,7 @@ type AvailableFlags = typeof waitFlags & {
22
22
  "shop-lang": OptionFlag<string | undefined>;
23
23
  "shop-currency": OptionFlag<string | undefined>;
24
24
  "install-mode": OptionFlag<string>;
25
+ "install-path": OptionFlag<string>;
25
26
  "document-root": OptionFlag<string>;
26
27
  "opensearch-host": OptionFlag<string>;
27
28
  "opensearch-port": OptionFlag<string>;
@@ -113,6 +113,12 @@ function buildFlagsWithDescription(appName) {
113
113
  options: ["composer", "symlink"],
114
114
  default: "composer",
115
115
  }),
116
+ "install-path": Flags.string({
117
+ required: false,
118
+ summary: `the installation path of your ${appName} application`,
119
+ description: "This is the path where your application will be installed. If omitted, this will default to an automatically-generated path.",
120
+ default: undefined,
121
+ }),
116
122
  "document-root": Flags.string({
117
123
  required: true,
118
124
  summary: `the document root from which your ${appName} will be served (relative to the installation path)`,
@@ -5,6 +5,7 @@ type AppAppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
5
5
  export declare function triggerAppInstallation(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, projectId: string, flags: {
6
6
  "site-title": string;
7
7
  "document-root"?: string;
8
+ "install-path"?: string;
8
9
  } & {
9
10
  [k: string]: unknown;
10
11
  }, appVersion: AppAppVersion): Promise<AppAppInstallation>;
@@ -7,6 +7,7 @@ export async function triggerAppInstallation(apiClient, process, projectId, flag
7
7
  appVersionId: appVersion.id,
8
8
  description: flags["site-title"],
9
9
  updatePolicy: "none",
10
+ installationPath: flags["install-path"],
10
11
  userInputs: Object.keys(flags).map((k) => ({
11
12
  name: k.replace("-", "_"),
12
13
  value: flags[k],
@@ -3,7 +3,7 @@ import { assertStatus } from "@mittwald/api-client-commons";
3
3
  export const mysqlConnectionFlags = {
4
4
  "mysql-password": Flags.string({
5
5
  char: "p",
6
- summary: "the password to use for the MySQL user (env: MYSQL_PWD)",
6
+ summary: "the password to use for the MySQL user",
7
7
  description: `\
8
8
  The password to use for the MySQL user. If not provided, the environment variable MYSQL_PWD will be used. If that is not set either, the command will interactively ask for the password.
9
9
 
@@ -4,6 +4,8 @@ export default class PortMapping {
4
4
  readonly remotePort: number;
5
5
  constructor(localPort: number, remotePort: number);
6
6
  private static validatePort;
7
+ private static isValidPortString;
8
+ private static parseAndValidatePort;
7
9
  static arg: import("@oclif/core/interfaces").ArgDefinition<PortMapping, Record<string, unknown>>;
8
10
  /** @param str Port and protocol; example: `8080/tcp` */
9
11
  static fromPortAndProtocol(str: string): PortMapping;
@@ -10,6 +10,19 @@ export default class PortMapping {
10
10
  static validatePort(port) {
11
11
  return !isNaN(port) && port > 0 && port <= 65535;
12
12
  }
13
+ static isValidPortString(str) {
14
+ return /^\d+$/.test(str);
15
+ }
16
+ static parseAndValidatePort(str) {
17
+ if (!PortMapping.isValidPortString(str)) {
18
+ throw new Error("Invalid port number. Ports must be between 1 and 65535.");
19
+ }
20
+ const portNum = parseInt(str);
21
+ if (!PortMapping.validatePort(portNum)) {
22
+ throw new Error("Invalid port number. Ports must be between 1 and 65535.");
23
+ }
24
+ return portNum;
25
+ }
13
26
  static arg = Args.custom({
14
27
  parse: async (input) => PortMapping.fromString(input),
15
28
  });
@@ -26,13 +39,15 @@ export default class PortMapping {
26
39
  return new PortMapping(portNum, portNum);
27
40
  }
28
41
  static fromString(str) {
29
- const [localPort, remotePort] = str.split(":");
30
- const localPortNum = parseInt(localPort);
31
- const remotePortNum = parseInt(remotePort);
32
- if (!PortMapping.validatePort(localPortNum) ||
33
- !PortMapping.validatePort(remotePortNum)) {
34
- throw new Error("Invalid port number. Ports must be between 1 and 65535.");
42
+ const parts = str.split(":");
43
+ // If only one part, use it for both local and remote port
44
+ if (parts.length === 1) {
45
+ const portNum = PortMapping.parseAndValidatePort(parts[0]);
46
+ return new PortMapping(portNum, portNum);
35
47
  }
48
+ const [localPort, remotePort] = parts;
49
+ const localPortNum = PortMapping.parseAndValidatePort(localPort);
50
+ const remotePortNum = PortMapping.parseAndValidatePort(remotePort);
36
51
  return new PortMapping(localPortNum, remotePortNum);
37
52
  }
38
53
  }
@@ -7,6 +7,12 @@ describe("PortMapping", () => {
7
7
  expect(result.localPort).toBe(8080);
8
8
  expect(result.remotePort).toBe(9090);
9
9
  });
10
+ // Test: Successfully parse single integer as identical local and remote port
11
+ it("should correctly parse single integer to identical ports", () => {
12
+ const result = PortMapping.fromString("8080");
13
+ expect(result.localPort).toBe(8080);
14
+ expect(result.remotePort).toBe(8080);
15
+ });
10
16
  // Test: Throws an error for invalid local port
11
17
  it("should throw an error for invalid local port", () => {
12
18
  expect(() => PortMapping.fromString("100000:8080")).toThrow("Invalid port number. Ports must be between 1 and 65535.");
@@ -19,6 +25,10 @@ describe("PortMapping", () => {
19
25
  it("should throw an error for invalid string format", () => {
20
26
  expect(() => PortMapping.fromString("8080-9090")).toThrow("Invalid port number. Ports must be between 1 and 65535.");
21
27
  });
28
+ // Test: Throws an error for invalid single port
29
+ it("should throw an error for invalid single port", () => {
30
+ expect(() => PortMapping.fromString("100000")).toThrow("Invalid port number. Ports must be between 1 and 65535.");
31
+ });
22
32
  // Test: Successfully assign local and remote ports via constructor
23
33
  it("should correctly initialize PortMapping with valid ports", () => {
24
34
  const portMapping = new PortMapping(3000, 4000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.11.2",
3
+ "version": "1.12.0",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {