@mittwald/cli 1.11.2 → 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 (64) hide show
  1. package/README.md +5 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/app/copy.d.ts +1 -0
  4. package/dist/commands/app/copy.js +6 -1
  5. package/dist/commands/app/create/node.d.ts +1 -1
  6. package/dist/commands/app/create/php-worker.d.ts +1 -1
  7. package/dist/commands/app/create/php.d.ts +1 -1
  8. package/dist/commands/app/create/python.d.ts +1 -1
  9. package/dist/commands/app/create/static.d.ts +1 -1
  10. package/dist/commands/app/dependency/update.js +4 -6
  11. package/dist/commands/app/dependency/versions.js +3 -3
  12. package/dist/commands/app/install/contao.d.ts +1 -1
  13. package/dist/commands/app/install/joomla.d.ts +1 -1
  14. package/dist/commands/app/install/matomo.d.ts +1 -1
  15. package/dist/commands/app/install/nextcloud.d.ts +1 -1
  16. package/dist/commands/app/install/shopware5.d.ts +1 -1
  17. package/dist/commands/app/install/shopware6.d.ts +1 -1
  18. package/dist/commands/app/install/typo3.d.ts +1 -1
  19. package/dist/commands/app/install/wordpress.d.ts +1 -1
  20. package/dist/commands/app/open.d.ts +3 -0
  21. package/dist/commands/app/open.js +35 -9
  22. package/dist/commands/container/logs.d.ts +1 -0
  23. package/dist/commands/container/logs.js +8 -0
  24. package/dist/commands/container/port-forward.js +2 -2
  25. package/dist/commands/container/run.d.ts +9 -0
  26. package/dist/commands/container/run.js +42 -4
  27. package/dist/commands/container/update.js +2 -2
  28. package/dist/commands/context/get.js +1 -0
  29. package/dist/commands/database/mysql/create.d.ts +1 -1
  30. package/dist/commands/database/mysql/create.js +3 -2
  31. package/dist/commands/ddev/init.js +2 -1
  32. package/dist/commands/org/invite/list-own.d.ts +1 -0
  33. package/dist/commands/org/membership/list-own.d.ts +1 -0
  34. package/dist/commands/project/create.js +12 -3
  35. package/dist/commands/stack/deploy.d.ts +2 -0
  36. package/dist/commands/stack/deploy.js +49 -6
  37. package/dist/lib/context/FlagSetBuilder.d.ts +1 -0
  38. package/dist/lib/context/FlagSetBuilder.js +7 -1
  39. package/dist/lib/intellij/config.test.js +9 -2
  40. package/dist/lib/resources/app/Installer.d.ts +1 -1
  41. package/dist/lib/resources/app/Installer.js +14 -1
  42. package/dist/lib/resources/app/flags.d.ts +3 -1
  43. package/dist/lib/resources/app/flags.js +12 -0
  44. package/dist/lib/resources/app/install.d.ts +1 -0
  45. package/dist/lib/resources/app/install.js +1 -0
  46. package/dist/lib/resources/app/versions.d.ts +7 -1
  47. package/dist/lib/resources/app/versions.js +35 -2
  48. package/dist/lib/resources/container/containerconfig.js +7 -1
  49. package/dist/lib/resources/container/containerconfig.test.d.ts +1 -0
  50. package/dist/lib/resources/container/containerconfig.test.js +25 -0
  51. package/dist/lib/resources/database/mysql/flags.js +1 -1
  52. package/dist/lib/resources/stack/env.d.ts +10 -0
  53. package/dist/lib/resources/stack/env.js +22 -0
  54. package/dist/lib/resources/stack/flags.js +16 -1
  55. package/dist/lib/resources/stack/template-loader.d.ts +18 -0
  56. package/dist/lib/resources/stack/template-loader.js +94 -0
  57. package/dist/lib/resources/stack/template-loader.test.d.ts +1 -0
  58. package/dist/lib/resources/stack/template-loader.test.js +125 -0
  59. package/dist/lib/units/PortMapping.d.ts +2 -0
  60. package/dist/lib/units/PortMapping.js +21 -6
  61. package/dist/lib/units/PortMapping.test.js +10 -0
  62. package/package.json +13 -9
  63. package/dist/commands/cronjob/execution/abort.d.ts +0 -18
  64. package/dist/commands/cronjob/execution/abort.js +0 -41
@@ -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
+ });
@@ -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,8 +1,11 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.11.2",
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
- }