@mittwald/cli 1.12.0 → 1.13.1-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +5 -0
  2. package/bin/dev.js +0 -0
  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/dependency/update.js +4 -6
  9. package/dist/commands/app/dependency/versions.js +3 -3
  10. package/dist/commands/app/install/contao.d.ts +1 -1
  11. package/dist/commands/app/install/joomla.d.ts +1 -1
  12. package/dist/commands/app/install/matomo.d.ts +1 -1
  13. package/dist/commands/app/install/nextcloud.d.ts +1 -1
  14. package/dist/commands/app/install/shopware5.d.ts +1 -1
  15. package/dist/commands/app/install/shopware6.d.ts +1 -1
  16. package/dist/commands/app/install/typo3.d.ts +1 -1
  17. package/dist/commands/app/install/wordpress.d.ts +1 -1
  18. package/dist/commands/container/logs.d.ts +1 -0
  19. package/dist/commands/container/logs.js +8 -0
  20. package/dist/commands/context/get.js +1 -0
  21. package/dist/commands/database/mysql/create.d.ts +1 -1
  22. package/dist/commands/database/mysql/create.js +2 -1
  23. package/dist/commands/ddev/init.js +2 -1
  24. package/dist/commands/org/invite/list-own.d.ts +1 -0
  25. package/dist/commands/org/membership/list-own.d.ts +1 -0
  26. package/dist/commands/project/create.js +12 -3
  27. package/dist/commands/stack/deploy.d.ts +2 -0
  28. package/dist/commands/stack/deploy.js +49 -6
  29. package/dist/lib/context/FlagSetBuilder.d.ts +1 -0
  30. package/dist/lib/context/FlagSetBuilder.js +7 -1
  31. package/dist/lib/intellij/config.test.js +9 -2
  32. package/dist/lib/resources/app/Installer.d.ts +1 -1
  33. package/dist/lib/resources/app/Installer.js +7 -0
  34. package/dist/lib/resources/app/flags.d.ts +2 -1
  35. package/dist/lib/resources/app/flags.js +6 -0
  36. package/dist/lib/resources/app/versions.d.ts +7 -1
  37. package/dist/lib/resources/app/versions.js +35 -2
  38. package/dist/lib/resources/container/containerconfig.js +7 -1
  39. package/dist/lib/resources/container/containerconfig.test.d.ts +1 -0
  40. package/dist/lib/resources/container/containerconfig.test.js +25 -0
  41. package/dist/lib/resources/stack/env.d.ts +10 -0
  42. package/dist/lib/resources/stack/env.js +22 -0
  43. package/dist/lib/resources/stack/flags.js +16 -1
  44. package/dist/lib/resources/stack/template-loader.d.ts +18 -0
  45. package/dist/lib/resources/stack/template-loader.js +94 -0
  46. package/dist/lib/resources/stack/template-loader.test.d.ts +1 -0
  47. package/dist/lib/resources/stack/template-loader.test.js +125 -0
  48. package/package.json +13 -9
  49. package/dist/commands/cronjob/execution/abort.d.ts +0 -18
  50. package/dist/commands/cronjob/execution/abort.js +0 -41
package/README.md CHANGED
@@ -42,6 +42,9 @@ using NPM; remember to run `npm upgrade -g @mittwald/cli` occasionally.
42
42
  $ npm install -g @mittwald/cli
43
43
  ```
44
44
 
45
+ Attention! When installing via `-g` flag, make sure you have nodejs >= 20.7.0
46
+ installed, as package definition is ignored for global installations!
47
+
45
48
  #### Any OS, using Docker
46
49
 
47
50
  There is also the
@@ -101,6 +104,7 @@ USAGE
101
104
  ...
102
105
  ```
103
106
 
107
+ <!-- prettier-ignore-start -->
104
108
  <!-- commands -->
105
109
  # Command Topics
106
110
 
@@ -130,3 +134,4 @@ USAGE
130
134
  * [`mw volume`](docs/volume.md) - Manage volumes
131
135
 
132
136
  <!-- commandsstop -->
137
+ <!-- prettier-ignore-end -->
package/bin/dev.js CHANGED
File without changes
@@ -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" | "install-path"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title" | "install-path" | "update-context"))[]>>;
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" | "install-path"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title" | "install-path" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
7
+ static flags: import("@oclif/core/interfaces").FlagInput<import("../../../lib/resources/app/flags.js").RelevantFlags<readonly ("entrypoint" | ("wait" | "wait-timeout" | "site-title" | "install-path" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -5,7 +5,8 @@ import { makeProcessRenderer, processFlags, } from "../../../rendering/process/p
5
5
  import { Flags } from "@oclif/core";
6
6
  import { assertStatus } from "@mittwald/api-client-commons";
7
7
  import { Success } from "../../../rendering/react/components/Success.js";
8
- import { Range, SemVer } from "semver";
8
+ import { Range } from "semver";
9
+ import { compareVersionsBy } from "../../../lib/resources/app/versions.js";
9
10
  export default class Update extends ExecRenderBaseCommand {
10
11
  static summary = "Update the dependencies of an app";
11
12
  static args = { ...appInstallationArgs };
@@ -82,7 +83,7 @@ export default class Update extends ExecRenderBaseCommand {
82
83
  });
83
84
  assertStatus(r, 204);
84
85
  });
85
- process.complete(_jsx(Success, { children: "The dependencies of this app were successfully updated!" }));
86
+ await process.complete(_jsx(Success, { children: "The dependencies of this app were successfully updated!" }));
86
87
  }
87
88
  async getVersions(p, systemSoftware, versionRange) {
88
89
  const versions = await p.runStep(`fetching versions for ${systemSoftware.name}`, async () => {
@@ -95,10 +96,7 @@ export default class Update extends ExecRenderBaseCommand {
95
96
  assertStatus(r, 200);
96
97
  return r.data;
97
98
  });
98
- versions.sort((a, b) => {
99
- return (new SemVer(a.externalVersion).compare(new SemVer(b.externalVersion)) *
100
- -1);
101
- });
99
+ versions.sort(compareVersionsBy("internal"));
102
100
  return versions;
103
101
  }
104
102
  render() {
@@ -1,7 +1,7 @@
1
1
  import { assertStatus } from "@mittwald/api-client-commons";
2
2
  import { ListBaseCommand, } from "../../../lib/basecommands/ListBaseCommand.js";
3
- import { SemVer } from "semver";
4
3
  import { Args } from "@oclif/core";
4
+ import { compareVersionsBy } from "../../../lib/resources/app/versions.js";
5
5
  export class Versions extends ListBaseCommand {
6
6
  static description = "Get all available versions of a particular dependency";
7
7
  static args = {
@@ -13,7 +13,7 @@ export class Versions extends ListBaseCommand {
13
13
  static flags = {
14
14
  ...ListBaseCommand.baseFlags,
15
15
  };
16
- sorter = (a, b) => new SemVer(a.externalVersion).compare(b.externalVersion);
16
+ sorter = compareVersionsBy("internal");
17
17
  async getData() {
18
18
  const systemSoftwareName = this.args["systemsoftware"];
19
19
  const systemSoftwares = await this.apiClient.app.listSystemsoftwares({});
@@ -22,7 +22,7 @@ export class Versions extends ListBaseCommand {
22
22
  if (!systemSoftware) {
23
23
  throw new Error(`system software ${systemSoftwareName} not found`);
24
24
  }
25
- return await this.apiClient.app.listSystemsoftwareversions({
25
+ return this.apiClient.app.listSystemsoftwareversions({
26
26
  systemSoftwareId: systemSoftware.id,
27
27
  });
28
28
  }
@@ -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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
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" | "install-path"))[]>>;
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" | "update-context"))[]>>;
8
8
  protected exec(): Promise<AppInstallationResult>;
9
9
  protected render(result: AppInstallationResult): React.ReactNode;
10
10
  }
@@ -5,6 +5,7 @@ export declare class Logs extends BaseCommand {
5
5
  static aliases: string[];
6
6
  static flags: {
7
7
  "no-pager": import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ tail: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
9
10
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  };
@@ -17,6 +17,11 @@ export class Logs extends BaseCommand {
17
17
  "no-pager": Flags.boolean({
18
18
  description: "Disable pager for output.",
19
19
  }),
20
+ tail: Flags.integer({
21
+ char: "t",
22
+ description: "Number of lines to show from the end of the logs (minimum: 1).",
23
+ min: 1,
24
+ }),
20
25
  };
21
26
  static args = {
22
27
  "container-id": Args.string({
@@ -31,6 +36,9 @@ export class Logs extends BaseCommand {
31
36
  const logsResp = await this.apiClient.container.getServiceLogs({
32
37
  stackId,
33
38
  serviceId,
39
+ queryParameters: {
40
+ ...(flags.tail && { tail: flags.tail }),
41
+ },
34
42
  });
35
43
  assertStatus(logsResp, 200);
36
44
  // This is to work around a bug which causes the response to
@@ -42,6 +42,7 @@ const GetContext = ({ ctx }) => {
42
42
  "server-id",
43
43
  "org-id",
44
44
  "installation-id",
45
+ "stack-id",
45
46
  ]) {
46
47
  const value = usePromise(ctx.getContextValue.bind(ctx), [key]);
47
48
  if (value) {
@@ -13,7 +13,7 @@ export declare class Create extends ExecRenderBaseCommand<typeof Create, Result>
13
13
  "character-set": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
14
  "user-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
15
  "user-external": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
- "user-access-level": import("@oclif/core/interfaces").OptionFlag<"full" | "readonly", import("@oclif/core/interfaces").CustomOptions>;
16
+ "user-access-level": import("@oclif/core/interfaces").OptionFlag<"full", import("@oclif/core/interfaces").CustomOptions>;
17
17
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
18
  "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
19
19
  };
@@ -39,7 +39,8 @@ export class Create extends ExecRenderBaseCommand {
39
39
  }),
40
40
  "user-access-level": Flags.custom({
41
41
  summary: "the access level preset for the default user",
42
- options: ["full", "readonly"],
42
+ deprecated: true,
43
+ options: ["full"],
43
44
  default: "full",
44
45
  })(),
45
46
  };
@@ -46,7 +46,7 @@ export class Init extends ExecRenderBaseCommand {
46
46
  summary: "override the mittwald plugin",
47
47
  helpGroup: "Development",
48
48
  description: "This flag allows you to override the mittwald plugin that should be installed by default; this is useful for testing purposes",
49
- default: "mittwald/ddev",
49
+ default: "mittwald/ddev-mittwald",
50
50
  }),
51
51
  };
52
52
  static args = {
@@ -89,6 +89,7 @@ export class Init extends ExecRenderBaseCommand {
89
89
  async installMittwaldPlugin(r) {
90
90
  const { "override-mittwald-plugin": mittwaldPlugin } = this.flags;
91
91
  await spawnInProcess(r, "installing mittwald plugin", "ddev", [
92
+ "add-on",
92
93
  "get",
93
94
  mittwaldPlugin,
94
95
  ]);
@@ -33,6 +33,7 @@ export declare class List extends ListBaseCommand<typeof List, ResponseItem, Res
33
33
  creationDate: string;
34
34
  customerId: string;
35
35
  customerNumber: string;
36
+ deletionProhibitedBy?: ("hasOpenInvoices" | "hasActiveContracts" | "hasActiveExtensionSubscriptions" | "isActiveContributor")[] | undefined;
36
37
  executingUserRoles?: import("node_modules/@mittwald/api-client/dist/types/generated/v2/types.js").MittwaldAPIV2.Components.Schemas.CustomerRole[] | undefined;
37
38
  flags?: import("node_modules/@mittwald/api-client/dist/types/generated/v2/types.js").MittwaldAPIV2.Components.Schemas.CustomerCustomerFlag[] | undefined;
38
39
  isAllowedToPlaceOrders?: boolean | undefined;
@@ -33,6 +33,7 @@ export declare class ListOwn extends ListBaseCommand<typeof ListOwn, ResponseIte
33
33
  creationDate: string;
34
34
  customerId: string;
35
35
  customerNumber: string;
36
+ deletionProhibitedBy?: ("hasOpenInvoices" | "hasActiveContracts" | "hasActiveExtensionSubscriptions" | "isActiveContributor")[] | undefined;
36
37
  executingUserRoles?: import("node_modules/@mittwald/api-client/dist/types/generated/v2/types.js").MittwaldAPIV2.Components.Schemas.CustomerRole[] | undefined;
37
38
  flags?: import("node_modules/@mittwald/api-client/dist/types/generated/v2/types.js").MittwaldAPIV2.Components.Schemas.CustomerCustomerFlag[] | undefined;
38
39
  isAllowedToPlaceOrders?: boolean | undefined;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Flags } from "@oclif/core";
3
3
  import { assertStatus } from "@mittwald/api-client-commons";
4
4
  import { serverFlags } from "../../lib/resources/server/flags.js";
@@ -7,6 +7,7 @@ import { Success } from "../../rendering/react/components/Success.js";
7
7
  import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
8
8
  import { waitFlags, waitUntil } from "../../lib/wait.js";
9
9
  import Context from "../../lib/context/Context.js";
10
+ import { Value } from "../../rendering/react/components/Value.js";
10
11
  export default class Create extends ExecRenderBaseCommand {
11
12
  static description = "Create a new project";
12
13
  static flags = {
@@ -19,7 +20,8 @@ export default class Create extends ExecRenderBaseCommand {
19
20
  description: "A description for the project.",
20
21
  }),
21
22
  "update-context": Flags.boolean({
22
- description: "Update the CLI context to use the newly created project",
23
+ description: "update the CLI context to use the newly created project",
24
+ char: "c",
23
25
  }),
24
26
  };
25
27
  async exec() {
@@ -48,12 +50,16 @@ export default class Create extends ExecRenderBaseCommand {
48
50
  }, this.flags["wait-timeout"]);
49
51
  stepWaiting.complete();
50
52
  }
53
+ const projectResult = await this.apiClient.project.getProject({
54
+ projectId: result.data.id,
55
+ });
56
+ assertStatus(projectResult, 200);
51
57
  if (flags["update-context"]) {
52
58
  await process.runStep("updating CLI context", async () => {
53
59
  await new Context(this.apiClient, this.config).setProjectId(result.data.id);
54
60
  });
55
61
  }
56
- process.complete(_jsx(Success, { children: "Your new project was successfully created! \uD83D\uDE80" }));
62
+ await process.complete(_jsx(ProjectCreationSuccess, { shortId: projectResult.data.shortId }));
57
63
  return { projectId: result.data.id };
58
64
  }
59
65
  render({ projectId }) {
@@ -62,3 +68,6 @@ export default class Create extends ExecRenderBaseCommand {
62
68
  }
63
69
  }
64
70
  }
71
+ function ProjectCreationSuccess({ shortId }) {
72
+ return (_jsxs(Success, { children: ["Your new project ", _jsx(Value, { children: shortId }), " was successfully created! \uD83D\uDE80"] }));
73
+ }
@@ -8,10 +8,12 @@ export declare class Deploy extends ExecRenderBaseCommand<typeof Deploy, DeployR
8
8
  static aliases: string[];
9
9
  static flags: {
10
10
  "compose-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ "from-template": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  "env-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
13
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  "stack-id": import("@oclif/core/interfaces").OptionFlag<string>;
14
15
  };
16
+ private loadStackDefinition;
15
17
  protected exec(): Promise<DeployResult>;
16
18
  protected render({ restartedServices }: DeployResult): ReactNode;
17
19
  }
@@ -3,14 +3,16 @@ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseComm
3
3
  import { stackFlags, withStackId } from "../../lib/resources/stack/flags.js";
4
4
  import { Flags } from "@oclif/core";
5
5
  import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
6
- import { loadStackFromFile } from "../../lib/resources/stack/loader.js";
6
+ import { loadStackFromFile, loadStackFromStr, } from "../../lib/resources/stack/loader.js";
7
7
  import { assertStatus } from "@mittwald/api-client";
8
8
  import assertSuccess from "../../lib/apiutil/assert_success.js";
9
- import { collectEnvironment } from "../../lib/resources/stack/env.js";
9
+ import { collectEnvironment, fillMissingEnvironmentVariables, } from "../../lib/resources/stack/env.js";
10
10
  import { sanitizeStackDefinition } from "../../lib/resources/stack/sanitize.js";
11
11
  import { enrichStackDefinition } from "../../lib/resources/stack/enrich.js";
12
12
  import { Success } from "../../rendering/react/components/Success.js";
13
13
  import { Value } from "../../rendering/react/components/Value.js";
14
+ import { loadStackFromTemplate } from "../../lib/resources/stack/template-loader.js";
15
+ import { parse } from "envfile";
14
16
  export class Deploy extends ExecRenderBaseCommand {
15
17
  static description = "Deploys a docker-compose compatible file to a mittwald container stack";
16
18
  static aliases = ["stack:up"];
@@ -21,22 +23,63 @@ export class Deploy extends ExecRenderBaseCommand {
21
23
  summary: 'path to a compose file, or "-" to read from stdin',
22
24
  default: "./docker-compose.yml",
23
25
  char: "c",
26
+ exclusive: ["from-template"],
27
+ }),
28
+ "from-template": Flags.string({
29
+ summary: "deploy from a GitHub template (e.g., mittwald/n8n)",
30
+ description: `\
31
+ Fetch and deploy a stack from a GitHub template repository. Template names are automatically converted to repository names by prefixing "stack-template-" to the name part.
32
+
33
+ For example, "mittwald/n8n" resolves to the repository "mittwald/stack-template-n8n". The command fetches both docker-compose.yml and .env files from the main branch.
34
+
35
+ Environment variable precedence (from lowest to highest):
36
+ 1. Template .env file (if present in the repository)
37
+ 2. System environment variables (process.env)
38
+ 3. Local --env-file (if specified)
39
+
40
+ This flag is mutually exclusive with --compose-file.`,
41
+ exclusive: ["compose-file"],
24
42
  }),
25
43
  "env-file": Flags.file({
26
44
  summary: "alternative path to file with environment variables",
27
45
  default: "./.env",
28
46
  }),
29
47
  };
48
+ async loadStackDefinition(source, envFile, existing, renderer) {
49
+ // Build environment: start with process.env, then template .env, then local --env-file
50
+ let env = { ...process.env };
51
+ if ("template" in source) {
52
+ const hasServices = existing.services?.length ?? 0 > 0;
53
+ if (hasServices) {
54
+ throw new Error("Re-applying templates to existing stacks is currently not supported.");
55
+ }
56
+ // Load from GitHub template
57
+ const { composeYaml, envContent } = await renderer.runStep("fetching template from GitHub", () => loadStackFromTemplate(source.template));
58
+ if (envContent) {
59
+ const templateEnv = parse(envContent);
60
+ env = { ...env, ...templateEnv };
61
+ }
62
+ env = await collectEnvironment(env, envFile);
63
+ env = await fillMissingEnvironmentVariables(env, renderer);
64
+ return loadStackFromStr(composeYaml, env);
65
+ }
66
+ // Load from local file
67
+ env = await collectEnvironment(env, envFile);
68
+ return loadStackFromFile(source.composeFile, env);
69
+ }
30
70
  async exec() {
31
71
  const stackId = await withStackId(this.apiClient, Deploy, this.flags, this.args, this.config);
32
- const { "compose-file": composeFile, "env-file": envFile } = this.flags;
72
+ const { "compose-file": composeFile, "from-template": fromTemplate, "env-file": envFile, } = this.flags;
33
73
  const r = makeProcessRenderer(this.flags, "Deploying container stack");
74
+ const existingStack = await r.runStep("retrieving current stack state", async () => {
75
+ const resp = await this.apiClient.container.getStack({ stackId });
76
+ assertStatus(resp, 200);
77
+ return resp.data;
78
+ });
34
79
  const result = { restartedServices: [] };
35
- const env = await collectEnvironment(process.env, envFile);
36
- let stackDefinition = await loadStackFromFile(composeFile, env);
80
+ let stackDefinition = await this.loadStackDefinition(fromTemplate ? { template: fromTemplate } : { composeFile }, envFile, existingStack, r);
37
81
  stackDefinition = sanitizeStackDefinition(stackDefinition);
38
82
  stackDefinition = await r.runStep("getting image configurations", () => enrichStackDefinition(stackDefinition));
39
- this.debug("complete stack definition: %O", stackDefinition);
40
83
  const declaredStack = await r.runStep("deploying stack", async () => {
41
84
  const resp = await this.apiClient.container.declareStack({
42
85
  stackId,
@@ -27,6 +27,7 @@ export type FlagSetOptions = {
27
27
  normalize: NormalizeFn;
28
28
  displayName: string;
29
29
  retrieveFromContext: boolean;
30
+ retrieveFunction: (client: MittwaldAPIV2Client, ctx: Context) => Promise<string | null>;
30
31
  expectedShortIDFormat: {
31
32
  pattern: RegExp;
32
33
  display: string;
@@ -119,7 +119,7 @@ export default class FlagSetBuilder {
119
119
  buildIDGetter() {
120
120
  const idInputSanityCheck = this.buildSanityCheck();
121
121
  const idFromArgsOrFlag = this.buildIDFromArgsOrFlag();
122
- const { normalize = (_, id) => id, retrieveFromContext = true } = this.opts;
122
+ const { normalize = (_, id) => id, retrieveFromContext = true, retrieveFunction, } = this.opts;
123
123
  return async (apiClient, commandType, flags, args, cfg) => {
124
124
  const context = new Context(apiClient, cfg);
125
125
  const idInput = idFromArgsOrFlag(flags, args);
@@ -133,6 +133,12 @@ export default class FlagSetBuilder {
133
133
  return idFromContext.value;
134
134
  }
135
135
  }
136
+ if (retrieveFunction) {
137
+ const idFromFunction = await retrieveFunction(apiClient, context);
138
+ if (idFromFunction) {
139
+ return idFromFunction;
140
+ }
141
+ }
136
142
  throw makeMissingContextInputError(commandType, this.name, this.flagName);
137
143
  };
138
144
  }
@@ -7,7 +7,10 @@ import { generateIntellijConfigs } from "./config.js";
7
7
  describe("IntelliJ Config Generator", () => {
8
8
  let tempDir;
9
9
  let testData;
10
- const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_" });
10
+ const parser = new XMLParser({
11
+ ignoreAttributes: false,
12
+ attributeNamePrefix: "@_",
13
+ });
11
14
  beforeEach(() => {
12
15
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "intellij-test-"));
13
16
  testData = {
@@ -74,7 +77,11 @@ describe("IntelliJ Config Generator", () => {
74
77
  });
75
78
  test("should add multiple SSH configs for different hosts", () => {
76
79
  generateIntellijConfigs(testData, tempDir);
77
- const differentHostData = { ...testData, host: "ssh.other.example.com", appShortId: "app456" };
80
+ const differentHostData = {
81
+ ...testData,
82
+ host: "ssh.other.example.com",
83
+ appShortId: "app456",
84
+ };
78
85
  generateIntellijConfigs(differentHostData, tempDir);
79
86
  const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
80
87
  const content = fs.readFileSync(configPath, "utf8");
@@ -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" | "install-path";
8
+ type ImplicitDefaultFlag = "wait" | "wait-timeout" | "site-title" | "install-path" | "update-context";
9
9
  export interface AppInstallationResult {
10
10
  appInstallation: AppInstallation;
11
11
  appVersion: AppVersion;
@@ -7,6 +7,7 @@ import { triggerAppInstallation } from "./install.js";
7
7
  import { waitUntilAppStateHasNormalized } from "./wait.js";
8
8
  import { Success } from "../../../rendering/react/components/Success.js";
9
9
  import AppUsageHints from "../../../rendering/react/components/AppInstallation/AppUsageHints.js";
10
+ import Context from "../../context/Context.js";
10
11
  export class AppInstaller {
11
12
  appId;
12
13
  appName;
@@ -31,6 +32,7 @@ export class AppInstaller {
31
32
  "wait-timeout",
32
33
  "site-title",
33
34
  "install-path",
35
+ "update-context",
34
36
  ], this.appName);
35
37
  if (this.mutateFlags) {
36
38
  this.mutateFlags(flags);
@@ -51,6 +53,11 @@ export class AppInstaller {
51
53
  else {
52
54
  successText = `Your ${this.appName} installation has started. Have fun when it's ready! 🎉`;
53
55
  }
56
+ if (flags["update-context"]) {
57
+ const context = new Context(apiClient, config);
58
+ await context.setProjectId(appInstallation.projectId);
59
+ await context.setAppInstallationId(appInstallation.id);
60
+ }
54
61
  await process.complete(_jsx(Success, { children: successText }));
55
62
  return {
56
63
  appInstallation,
@@ -1,7 +1,7 @@
1
1
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
2
  import { ProcessRenderer } from "../../../rendering/process/process.js";
3
3
  import { ProcessFlags } from "../../../rendering/process/process_flags.js";
4
- import { FlagInput, OptionFlag, OutputFlags } from "@oclif/core/interfaces";
4
+ import { BooleanFlag, FlagInput, OptionFlag, OutputFlags } from "@oclif/core/interfaces";
5
5
  import { waitFlags } from "../../wait.js";
6
6
  export declare const appInstallationFlags: import("../../context/FlagSetBuilder.js").ContextFlags<"installation">, appInstallationArgs: import("../../context/FlagSetBuilder.js").ContextArgs<"installation">, withAppInstallationId: (apiClient: MittwaldAPIV2Client, command: "flag" | "arg" | import("../../context/FlagSetBuilder.js").CommandType<"installation">, flags: {
7
7
  [k: string]: unknown;
@@ -27,6 +27,7 @@ type AvailableFlags = typeof waitFlags & {
27
27
  "opensearch-host": OptionFlag<string>;
28
28
  "opensearch-port": OptionFlag<string>;
29
29
  entrypoint: OptionFlag<string | undefined>;
30
+ "update-context": BooleanFlag<boolean>;
30
31
  };
31
32
  export type RelevantFlags<TFlags extends readonly AvailableFlagName[]> = ProcessFlags & Pick<AvailableFlags, TFlags[number]>;
32
33
  export type RelevantFlagInput<TFlags extends readonly AvailableFlagName[]> = FlagInput<RelevantFlags<TFlags>>;
@@ -143,6 +143,12 @@ function buildFlagsWithDescription(appName) {
143
143
  required: false,
144
144
  default: undefined,
145
145
  }),
146
+ "update-context": Flags.boolean({
147
+ description: "update the CLI context to use the newly created app installation",
148
+ char: "c",
149
+ required: false,
150
+ default: false,
151
+ }),
146
152
  ...waitFlags,
147
153
  };
148
154
  }
@@ -1,11 +1,17 @@
1
1
  import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
2
2
  import { ProcessRenderer } from "../../../rendering/process/process.js";
3
3
  type AppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
4
+ type ObjectWithVersions = {
5
+ internalVersion: string;
6
+ externalVersion: string;
7
+ };
4
8
  export declare function normalizeToAppVersionUuid(apiClient: MittwaldAPIV2Client, version: string, process: ProcessRenderer, appUuid: string): Promise<MittwaldAPIV2.Components.Schemas.AppAppVersion>;
5
9
  export declare function getLatestAvailableAppVersionForApp(apiClient: MittwaldAPIV2Client, appId: string): Promise<AppVersion | undefined>;
6
10
  export declare function getAllUpgradeCandidatesFromAppInstallationId(apiClient: MittwaldAPIV2Client, appInstallationId: string): Promise<AppVersion[]>;
7
11
  export declare function getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(apiClient: MittwaldAPIV2Client, appId: string, baseAppVersionId: string): Promise<AppVersion | undefined>;
8
12
  export declare function getAvailableTargetAppVersionFromExternalVersion(apiClient: MittwaldAPIV2Client, appId: string, baseAppVersionId: string, targetExternalVersion: string): Promise<AppVersion | undefined>;
9
13
  export declare function getAppVersionUuidFromAppVersion(apiClient: MittwaldAPIV2Client, appId: string, appVersion: string | undefined): Promise<AppVersion | undefined>;
10
- export declare function sortArrayByExternalVersion(versions: AppVersion[]): AppVersion[];
14
+ export declare function sortArrayByExternalVersion<T extends ObjectWithVersions>(versions: T[]): T[];
15
+ export declare function sortArrayByInternalVersion<T extends ObjectWithVersions>(versions: T[]): T[];
16
+ export declare function compareVersionsBy<T extends ObjectWithVersions>(field: "internal" | "external"): (a: T, b: T) => -1 | 0 | 1;
11
17
  export {};
@@ -1,5 +1,5 @@
1
1
  import { assertStatus } from "@mittwald/api-client-commons";
2
- import { gt } from "semver";
2
+ import { coerce, gt } from "semver";
3
3
  import { getAppInstallationFromUuid, getAppNameFromUuid } from "./uuid.js";
4
4
  import { compare } from "semver";
5
5
  export async function normalizeToAppVersionUuid(apiClient, version, process, appUuid) {
@@ -78,5 +78,38 @@ export async function getAppVersionUuidFromAppVersion(apiClient, appId, appVersi
78
78
  item.externalVersion === appVersion);
79
79
  }
80
80
  export function sortArrayByExternalVersion(versions) {
81
- return versions.sort((a, b) => compare(b.externalVersion, a.externalVersion));
81
+ return versions.sort(compareVersionsBy("external"));
82
+ }
83
+ export function sortArrayByInternalVersion(versions) {
84
+ return versions.sort(compareVersionsBy("internal"));
85
+ }
86
+ export function compareVersionsBy(field) {
87
+ const fullField = `${field}Version`;
88
+ return (a, b) => {
89
+ const aCoerced = coerce(a[fullField]);
90
+ const bCoerced = coerce(b[fullField]);
91
+ if (!aCoerced || !bCoerced) {
92
+ return naiveVersionCompare(a.internalVersion, b.internalVersion);
93
+ }
94
+ return compare(aCoerced, bCoerced);
95
+ };
96
+ }
97
+ /**
98
+ * A naive version comparison function that compares version strings in the
99
+ * format "x.y.z". This function does not handle pre-release or build metadata.
100
+ */
101
+ function naiveVersionCompare(a, b) {
102
+ const aParts = a.split(".").map((part) => parseInt(part, 10));
103
+ const bParts = b.split(".").map((part) => parseInt(part, 10));
104
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
105
+ const aPart = aParts[i] || 0;
106
+ const bPart = bParts[i] || 0;
107
+ if (aPart > bPart) {
108
+ return 1;
109
+ }
110
+ if (aPart < bPart) {
111
+ return -1;
112
+ }
113
+ }
114
+ return 0;
82
115
  }
@@ -40,7 +40,13 @@ export async function parseEnvironmentVariablesFromFile(envFiles = []) {
40
40
  * @returns An object containing environment variable key-value pairs
41
41
  */
42
42
  export function parseEnvironmentVariablesFromEnvFlags(envFlags = []) {
43
- const splitIntoKeyAndValue = (e) => e.split("=", 2);
43
+ const splitIntoKeyAndValue = (e) => {
44
+ const index = e.indexOf("=");
45
+ if (index < 0) {
46
+ throw new Error(`Invalid environment variable format: ${e}`);
47
+ }
48
+ return [e.slice(0, index), e.slice(index + 1)];
49
+ };
44
50
  return Object.fromEntries(envFlags.map(splitIntoKeyAndValue));
45
51
  }
46
52
  /**
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { parseEnvironmentVariablesFromEnvFlags } from "./containerconfig.js";
3
+ describe("Containerconfig handling", () => {
4
+ describe("Config parsing", () => {
5
+ test("call parser with simple flags", () => {
6
+ const args = ["foo=bar", "ham=eggs"];
7
+ const res = parseEnvironmentVariablesFromEnvFlags(args);
8
+ expect(res).toStrictEqual({
9
+ foo: "bar",
10
+ ham: "eggs",
11
+ });
12
+ });
13
+ test("call parser with flag containing another '='", () => {
14
+ const args = ["extra_args=first=1 second=2 third=littlebitoflove"];
15
+ const res = parseEnvironmentVariablesFromEnvFlags(args);
16
+ expect(res).toStrictEqual({
17
+ extra_args: "first=1 second=2 third=littlebitoflove",
18
+ });
19
+ });
20
+ test("throw error for invalid flag format", () => {
21
+ const args = ["invalidFlagWithoutEqualsSign"];
22
+ expect(() => parseEnvironmentVariablesFromEnvFlags(args)).toThrow("Invalid environment variable format: invalidFlagWithoutEqualsSign");
23
+ });
24
+ });
25
+ });
@@ -1 +1,11 @@
1
+ import { ProcessRenderer } from "../../../rendering/process/process.js";
1
2
  export declare function collectEnvironment(base: NodeJS.ProcessEnv, envFile: string): Promise<Record<string, string | undefined>>;
3
+ /**
4
+ * Fills in missing environment variables in the base environment.
5
+ *
6
+ * This is done by checking for special placeholder values:
7
+ *
8
+ * - `__PROMPT__`: prompts the user to input a value.
9
+ * - `__GENERATE__`: generates a random base64-encoded string.
10
+ */
11
+ export declare function fillMissingEnvironmentVariables(base: NodeJS.ProcessEnv, renderer: ProcessRenderer): Promise<Record<string, string | undefined>>;
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import { parse } from "envfile";
3
3
  import { pathExists } from "../../util/fs/pathExists.js";
4
+ import { getRandomValues } from "node:crypto";
4
5
  export async function collectEnvironment(base, envFile) {
5
6
  if (!(await pathExists(envFile))) {
6
7
  return base;
@@ -9,3 +10,24 @@ export async function collectEnvironment(base, envFile) {
9
10
  const parsed = parse(defs);
10
11
  return { ...base, ...parsed };
11
12
  }
13
+ /**
14
+ * Fills in missing environment variables in the base environment.
15
+ *
16
+ * This is done by checking for special placeholder values:
17
+ *
18
+ * - `__PROMPT__`: prompts the user to input a value.
19
+ * - `__GENERATE__`: generates a random base64-encoded string.
20
+ */
21
+ export async function fillMissingEnvironmentVariables(base, renderer) {
22
+ const output = { ...base };
23
+ for (const [key, value] of Object.entries(base)) {
24
+ if (value === "__PROMPT__") {
25
+ output[key] = await renderer.addInput(`enter value for environment variable ${key}`, false);
26
+ }
27
+ if (value === "__GENERATE__") {
28
+ renderer.addInfo(`generating random value for environment variable ${key}`);
29
+ output[key] = Buffer.from(getRandomValues(new Uint8Array(32))).toString("base64");
30
+ }
31
+ }
32
+ return output;
33
+ }
@@ -1,2 +1,17 @@
1
1
  import FlagSetBuilder from "../../context/FlagSetBuilder.js";
2
- export const { flags: stackFlags, args: stackArgs, withId: withStackId, } = new FlagSetBuilder("stack", "s").build();
2
+ import { assertStatus } from "@mittwald/api-client";
3
+ export const { flags: stackFlags, args: stackArgs, withId: withStackId, } = new FlagSetBuilder("stack", "s", {
4
+ retrieveFunction: async (client, context) => {
5
+ const projectContext = await context.projectId();
6
+ if (!projectContext) {
7
+ return null;
8
+ }
9
+ const projectId = projectContext.value;
10
+ const stacks = await client.container.listStacks({ projectId });
11
+ assertStatus(stacks, 200);
12
+ if (stacks.data.length === 1) {
13
+ return stacks.data[0].id;
14
+ }
15
+ return null;
16
+ },
17
+ }).build();
@@ -0,0 +1,18 @@
1
+ export interface TemplateContent {
2
+ composeYaml: string;
3
+ envContent: string | null;
4
+ }
5
+ export declare class TemplateFileNotFoundError extends Error {
6
+ constructor(url: string);
7
+ }
8
+ export declare class GitHubRateLimitError extends Error {
9
+ constructor();
10
+ }
11
+ export declare class TemplateNetworkError extends Error {
12
+ constructor(message: string);
13
+ }
14
+ export declare function validateTemplateName(name: string): void;
15
+ export declare function templateNameToRepoName(templateName: string): string;
16
+ export declare function buildGitHubRawUrl(templateName: string, filename: string): string;
17
+ export declare function fetchTemplateFile(url: string): Promise<string>;
18
+ export declare function loadStackFromTemplate(templateName: string): Promise<TemplateContent>;
@@ -0,0 +1,94 @@
1
+ import axios from "axios";
2
+ export class TemplateFileNotFoundError extends Error {
3
+ constructor(url) {
4
+ super(`File not found at ${url}`);
5
+ this.name = "TemplateFileNotFoundError";
6
+ }
7
+ }
8
+ export class GitHubRateLimitError extends Error {
9
+ constructor() {
10
+ super("GitHub API rate limit exceeded. Please try again later.");
11
+ this.name = "GitHubRateLimitError";
12
+ }
13
+ }
14
+ export class TemplateNetworkError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "TemplateNetworkError";
18
+ }
19
+ }
20
+ export function validateTemplateName(name) {
21
+ const validFormat = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
22
+ if (!validFormat.test(name)) {
23
+ throw new Error(`Invalid template name format: '${name}'. Expected format: 'owner/name' (e.g., 'mittwald/n8n')`);
24
+ }
25
+ }
26
+ export function templateNameToRepoName(templateName) {
27
+ const [owner, name] = templateName.split("/");
28
+ return `${owner}/stack-template-${name}`;
29
+ }
30
+ export function buildGitHubRawUrl(templateName, filename) {
31
+ const repoName = templateNameToRepoName(templateName);
32
+ return `https://raw.githubusercontent.com/${repoName}/main/${filename}`;
33
+ }
34
+ function convertAxiosError(error, url) {
35
+ if (!axios.isAxiosError(error)) {
36
+ return error instanceof Error ? error : new Error(String(error));
37
+ }
38
+ const axiosError = error;
39
+ if (axiosError.response?.status === 404) {
40
+ return new TemplateFileNotFoundError(url);
41
+ }
42
+ if (axiosError.response?.status === 403) {
43
+ return new GitHubRateLimitError();
44
+ }
45
+ if (axiosError.code === "ETIMEDOUT") {
46
+ return new TemplateNetworkError("Request timed out while fetching template from GitHub");
47
+ }
48
+ if (axiosError.code === "ECONNREFUSED" || axiosError.code === "ENOTFOUND") {
49
+ return new TemplateNetworkError("Network error: Unable to connect to GitHub. Please check your internet connection.");
50
+ }
51
+ return error;
52
+ }
53
+ export async function fetchTemplateFile(url) {
54
+ try {
55
+ const response = await axios.get(url, {
56
+ responseType: "text",
57
+ timeout: 10000,
58
+ });
59
+ return response.data;
60
+ }
61
+ catch (error) {
62
+ throw convertAxiosError(error, url);
63
+ }
64
+ }
65
+ export async function loadStackFromTemplate(templateName) {
66
+ validateTemplateName(templateName);
67
+ const composeUrl = buildGitHubRawUrl(templateName, "docker-compose.yml");
68
+ const envUrl = buildGitHubRawUrl(templateName, ".env");
69
+ let composeYaml;
70
+ try {
71
+ composeYaml = await fetchTemplateFile(composeUrl);
72
+ }
73
+ catch (error) {
74
+ if (error instanceof TemplateFileNotFoundError) {
75
+ const repoName = templateNameToRepoName(templateName);
76
+ throw new Error(`Template '${templateName}' not found. Repository '${repoName}' does not exist or does not contain a docker-compose.yml file.`);
77
+ }
78
+ throw error;
79
+ }
80
+ let envContent = null;
81
+ try {
82
+ envContent = await fetchTemplateFile(envUrl);
83
+ }
84
+ catch (error) {
85
+ if (error instanceof TemplateFileNotFoundError) {
86
+ // .env file is optional, so we ignore 404 errors
87
+ envContent = null;
88
+ }
89
+ else {
90
+ throw error;
91
+ }
92
+ }
93
+ return { composeYaml, envContent };
94
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ import { afterEach, beforeEach, describe, expect, it, jest, } from "@jest/globals";
2
+ import { validateTemplateName, templateNameToRepoName, buildGitHubRawUrl, fetchTemplateFile, loadStackFromTemplate, TemplateFileNotFoundError, GitHubRateLimitError, TemplateNetworkError, } from "./template-loader.js";
3
+ import axios from "axios";
4
+ describe("validateTemplateName", () => {
5
+ it("should accept valid template names", () => {
6
+ expect(() => validateTemplateName("mittwald/n8n")).not.toThrow();
7
+ expect(() => validateTemplateName("owner/name")).not.toThrow();
8
+ expect(() => validateTemplateName("my-org/my-app")).not.toThrow();
9
+ expect(() => validateTemplateName("org_name/app_name")).not.toThrow();
10
+ });
11
+ it("should reject invalid template names", () => {
12
+ expect(() => validateTemplateName("")).toThrow(/Invalid template name/);
13
+ expect(() => validateTemplateName("noSlash")).toThrow(/Invalid template name/);
14
+ expect(() => validateTemplateName("too/many/slashes")).toThrow(/Invalid template name/);
15
+ expect(() => validateTemplateName("/missingOwner")).toThrow(/Invalid template name/);
16
+ expect(() => validateTemplateName("missingName/")).toThrow(/Invalid template name/);
17
+ });
18
+ });
19
+ describe("templateNameToRepoName", () => {
20
+ it("should convert template name to repository name", () => {
21
+ expect(templateNameToRepoName("mittwald/n8n")).toBe("mittwald/stack-template-n8n");
22
+ expect(templateNameToRepoName("owner/app")).toBe("owner/stack-template-app");
23
+ });
24
+ });
25
+ describe("buildGitHubRawUrl", () => {
26
+ it("should build correct GitHub raw URL", () => {
27
+ expect(buildGitHubRawUrl("mittwald/n8n", "docker-compose.yml")).toBe("https://raw.githubusercontent.com/mittwald/stack-template-n8n/main/docker-compose.yml");
28
+ expect(buildGitHubRawUrl("owner/app", ".env")).toBe("https://raw.githubusercontent.com/owner/stack-template-app/main/.env");
29
+ });
30
+ });
31
+ describe("fetchTemplateFile", () => {
32
+ let axiosGetSpy;
33
+ let axiosIsAxiosErrorSpy;
34
+ beforeEach(() => {
35
+ axiosGetSpy = jest.spyOn(axios, "get");
36
+ axiosIsAxiosErrorSpy = jest.spyOn(axios, "isAxiosError");
37
+ });
38
+ afterEach(() => {
39
+ jest.restoreAllMocks();
40
+ });
41
+ it("should fetch file successfully", async () => {
42
+ const mockData = "file content";
43
+ axiosGetSpy.mockResolvedValue({ data: mockData });
44
+ const result = await fetchTemplateFile("https://raw.githubusercontent.com/mittwald/stack-template-n8n/main/docker-compose.yml");
45
+ expect(result).toBe(mockData);
46
+ expect(axiosGetSpy).toHaveBeenCalledWith("https://raw.githubusercontent.com/mittwald/stack-template-n8n/main/docker-compose.yml", { responseType: "text", timeout: 10000 });
47
+ });
48
+ it("should handle 404 errors", async () => {
49
+ axiosGetSpy.mockRejectedValue({
50
+ isAxiosError: true,
51
+ response: { status: 404 },
52
+ });
53
+ axiosIsAxiosErrorSpy.mockReturnValue(true);
54
+ await expect(fetchTemplateFile("https://example.com/missing.yml")).rejects.toThrow(TemplateFileNotFoundError);
55
+ });
56
+ it("should handle 403 rate limit errors", async () => {
57
+ axiosGetSpy.mockRejectedValue({
58
+ isAxiosError: true,
59
+ response: { status: 403 },
60
+ });
61
+ axiosIsAxiosErrorSpy.mockReturnValue(true);
62
+ await expect(fetchTemplateFile("https://example.com/file.yml")).rejects.toThrow(GitHubRateLimitError);
63
+ });
64
+ it("should handle timeout errors", async () => {
65
+ axiosGetSpy.mockRejectedValue({
66
+ isAxiosError: true,
67
+ code: "ETIMEDOUT",
68
+ });
69
+ axiosIsAxiosErrorSpy.mockReturnValue(true);
70
+ await expect(fetchTemplateFile("https://example.com/file.yml")).rejects.toThrow(TemplateNetworkError);
71
+ });
72
+ it("should handle network errors", async () => {
73
+ axiosGetSpy.mockRejectedValue({
74
+ isAxiosError: true,
75
+ code: "ECONNREFUSED",
76
+ });
77
+ axiosIsAxiosErrorSpy.mockReturnValue(true);
78
+ await expect(fetchTemplateFile("https://example.com/file.yml")).rejects.toThrow(TemplateNetworkError);
79
+ });
80
+ });
81
+ describe("loadStackFromTemplate", () => {
82
+ let axiosGetSpy;
83
+ let axiosIsAxiosErrorSpy;
84
+ beforeEach(() => {
85
+ axiosGetSpy = jest.spyOn(axios, "get");
86
+ axiosIsAxiosErrorSpy = jest.spyOn(axios, "isAxiosError");
87
+ });
88
+ afterEach(() => {
89
+ jest.restoreAllMocks();
90
+ });
91
+ it("should load both docker-compose.yml and .env", async () => {
92
+ const composeContent = "version: '3'\nservices:\n app:\n image: nginx";
93
+ const envContent = "FOO=bar\nBAZ=qux";
94
+ axiosGetSpy
95
+ .mockResolvedValueOnce({ data: composeContent })
96
+ .mockResolvedValueOnce({ data: envContent });
97
+ const result = await loadStackFromTemplate("mittwald/n8n");
98
+ expect(result.composeYaml).toBe(composeContent);
99
+ expect(result.envContent).toBe(envContent);
100
+ });
101
+ it("should handle missing .env file gracefully", async () => {
102
+ const composeContent = "version: '3'\nservices:\n app:\n image: nginx";
103
+ axiosGetSpy.mockResolvedValueOnce({ data: composeContent });
104
+ axiosGetSpy.mockRejectedValueOnce({
105
+ isAxiosError: true,
106
+ response: { status: 404 },
107
+ });
108
+ axiosIsAxiosErrorSpy.mockReturnValue(true);
109
+ const result = await loadStackFromTemplate("mittwald/n8n");
110
+ expect(result.composeYaml).toBe(composeContent);
111
+ expect(result.envContent).toBeNull();
112
+ });
113
+ it("should throw error if docker-compose.yml is missing", async () => {
114
+ axiosGetSpy.mockRejectedValue({
115
+ isAxiosError: true,
116
+ response: { status: 404 },
117
+ });
118
+ axiosIsAxiosErrorSpy.mockReturnValue(true);
119
+ await expect(loadStackFromTemplate("mittwald/n8n")).rejects.toThrow(/Template 'mittwald\/n8n' not found/);
120
+ });
121
+ it("should validate template name before fetching", async () => {
122
+ await expect(loadStackFromTemplate("invalid")).rejects.toThrow(/Invalid template name/);
123
+ expect(axiosGetSpy).not.toHaveBeenCalled();
124
+ });
125
+ });
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.12.0",
3
+ "version": "1.13.1-beta.6",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "url": "https://github.com/mittwald/cli"
8
+ },
6
9
  "author": {
7
10
  "name": "Mittwald CM Service GmbH & Co. KG",
8
11
  "email": "opensource@mittwald.de"
@@ -16,12 +19,13 @@
16
19
  },
17
20
  "type": "module",
18
21
  "engines": {
19
- "node": ">=20.0.0"
22
+ "node": ">=20.7.0"
20
23
  },
21
24
  "scripts": {
22
25
  "clean": "(rimraf --glob dist tsconfig.*.tsbuildinfo) | true",
23
26
  "compile": "tsc --build tsconfig.json",
24
- "format": "prettier --write $@ '**/*.{ts,tsx,yaml,yml,json,md,mdx}'",
27
+ "format": "yarn format:prettier --write",
28
+ "format:prettier": "prettier $@ '**/*.{ts,tsx,yaml,yml,json,md,mdx}'",
25
29
  "generate:readme": "oclif readme --multi --output-dir=docs < /dev/null",
26
30
  "license-check": "yarn pnpify license-checker-rseidelsohn ",
27
31
  "lint": "eslint . --cache",
@@ -31,7 +35,7 @@
31
35
  "package:windows": "oclif pack win --targets=win32-x64,win32-x86",
32
36
  "post:generate": "yarn run -T compile && yarn run -T compile:cjs",
33
37
  "test": "yarn test:format && yarn test:licenses && yarn test:unit",
34
- "test:format": "yarn lint && yarn format --check",
38
+ "test:format": "yarn lint && yarn format:prettier --check",
35
39
  "test:licenses": "yarn license-check --summary --unknown --failOn 'UNLICENSED;UNKNOWN'",
36
40
  "test:readme": "yarn generate:readme && git diff --exit-code README.md docs/*.md",
37
41
  "test:unit": "NODE_NO_WARNINGS=1 yarn node --experimental-vm-modules $(yarn bin jest) ./src"
@@ -61,7 +65,7 @@
61
65
  "js-yaml": "^4.1.0",
62
66
  "marked": "^15.0.12",
63
67
  "marked-terminal": "^7.3.0",
64
- "open": "^10.0.3",
68
+ "open": "^11.0.0",
65
69
  "parse-duration": "^2.0.1",
66
70
  "pretty-bytes": "^7.0.0",
67
71
  "react": "^19.1.1",
@@ -70,14 +74,14 @@
70
74
  "shell-escape": "^0.2.0",
71
75
  "slice-ansi": "^7.1.0",
72
76
  "string-width": "^8.0.0",
73
- "tempfile": "^5.0.0",
77
+ "tempfile": "^6.0.1",
74
78
  "uuid": "^13.0.0"
75
79
  },
76
80
  "devDependencies": {
77
81
  "@jest/globals": "^30.0.4",
78
82
  "@oclif/test": "^4.0.4",
79
83
  "@types/js-yaml": "^4.0.9",
80
- "@types/node": "^24.0.10",
84
+ "@types/node": "^25.0.3",
81
85
  "@types/react": "^19",
82
86
  "@types/semver": "^7.5.0",
83
87
  "@types/shell-escape": "^0.2.3",
@@ -88,7 +92,7 @@
88
92
  "eslint-config-prettier": "^10.1.5",
89
93
  "eslint-plugin-json": "^4.0.1",
90
94
  "eslint-plugin-prettier": "^5.5.1",
91
- "globals": "^16.0.0",
95
+ "globals": "^17.0.0",
92
96
  "jest": "^30.0.4",
93
97
  "license-checker-rseidelsohn": "^4.2.6",
94
98
  "nock": "^14.0.0",
@@ -288,4 +292,4 @@
288
292
  }
289
293
  },
290
294
  "packageManager": "yarn@3.6.1"
291
- }
295
+ }
@@ -1,18 +0,0 @@
1
- import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js";
2
- import { ReactNode } from "react";
3
- type Result = {
4
- executionId: string;
5
- };
6
- export declare class Abort extends ExecRenderBaseCommand<typeof Abort, Result> {
7
- static summary: string;
8
- static flags: {
9
- quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- };
11
- static args: {
12
- "cronjob-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
13
- "execution-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
14
- };
15
- protected exec(): Promise<Result>;
16
- protected render({ executionId }: Result): ReactNode;
17
- }
18
- export {};
@@ -1,41 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js";
3
- import { makeProcessRenderer, processFlags, } from "../../../rendering/process/process_flags.js";
4
- import { Args } from "@oclif/core";
5
- import { Success } from "../../../rendering/react/components/Success.js";
6
- import { Value } from "../../../rendering/react/components/Value.js";
7
- import assertSuccess from "../../../lib/apiutil/assert_success.js";
8
- export class Abort extends ExecRenderBaseCommand {
9
- static summary = "Abort a running cron job execution.";
10
- static flags = {
11
- ...processFlags,
12
- };
13
- static args = {
14
- "cronjob-id": Args.string({
15
- description: "ID of the cronjob the execution belongs to",
16
- required: true,
17
- }),
18
- "execution-id": Args.string({
19
- required: true,
20
- description: "ID of the cron job execution to abort",
21
- }),
22
- };
23
- async exec() {
24
- const p = makeProcessRenderer(this.flags, "Aborting a cron job execution");
25
- const { "cronjob-id": cronjobId, "execution-id": executionId } = this.args;
26
- await p.runStep("aborting cron job execution", async () => {
27
- const r = await this.apiClient.cronjob.abortExecution({
28
- cronjobId,
29
- executionId,
30
- });
31
- assertSuccess(r);
32
- });
33
- p.complete(_jsxs(Success, { children: ["Execution ", _jsx(Value, { children: executionId }), " was successfully aborted."] }));
34
- return { executionId };
35
- }
36
- render({ executionId }) {
37
- if (this.flags.quiet) {
38
- return executionId;
39
- }
40
- }
41
- }