@mittwald/cli 1.4.3 → 1.5.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 (77) hide show
  1. package/README.md +3 -0
  2. package/dist/commands/app/dependency/versions.d.ts +1 -1
  3. package/dist/commands/container/delete.d.ts +15 -0
  4. package/dist/commands/container/delete.js +39 -0
  5. package/dist/commands/container/list.d.ts +23 -0
  6. package/dist/commands/container/list.js +53 -0
  7. package/dist/commands/container/logs.d.ts +14 -0
  8. package/dist/commands/container/logs.js +47 -0
  9. package/dist/commands/container/recreate.d.ts +20 -0
  10. package/dist/commands/container/recreate.js +72 -0
  11. package/dist/commands/container/restart.d.ts +18 -0
  12. package/dist/commands/container/restart.js +40 -0
  13. package/dist/commands/container/run.d.ts +57 -0
  14. package/dist/commands/container/run.js +224 -0
  15. package/dist/commands/container/start.d.ts +18 -0
  16. package/dist/commands/container/start.js +40 -0
  17. package/dist/commands/container/stop.d.ts +18 -0
  18. package/dist/commands/container/stop.js +40 -0
  19. package/dist/commands/context/set.d.ts +1 -0
  20. package/dist/commands/context/set.js +8 -0
  21. package/dist/commands/cronjob/execution/logs.js +6 -24
  22. package/dist/commands/cronjob/list.d.ts +2 -3
  23. package/dist/commands/domain/virtualhost/create.d.ts +1 -0
  24. package/dist/commands/domain/virtualhost/create.js +12 -0
  25. package/dist/commands/extension/install.js +4 -3
  26. package/dist/commands/extension/list-installed.js +4 -3
  27. package/dist/commands/registry/create.d.ts +20 -0
  28. package/dist/commands/registry/create.js +77 -0
  29. package/dist/commands/registry/delete.d.ts +13 -0
  30. package/dist/commands/registry/delete.js +21 -0
  31. package/dist/commands/registry/list.d.ts +23 -0
  32. package/dist/commands/registry/list.js +33 -0
  33. package/dist/commands/registry/update.d.ts +20 -0
  34. package/dist/commands/registry/update.js +73 -0
  35. package/dist/commands/stack/delete.d.ts +16 -0
  36. package/dist/commands/stack/delete.js +54 -0
  37. package/dist/commands/stack/deploy.d.ts +18 -0
  38. package/dist/commands/stack/deploy.js +75 -0
  39. package/dist/commands/stack/list.d.ts +24 -0
  40. package/dist/commands/stack/list.js +60 -0
  41. package/dist/commands/stack/ps.d.ts +23 -0
  42. package/dist/commands/stack/ps.js +51 -0
  43. package/dist/lib/basecommands/DeleteBaseCommand.js +1 -1
  44. package/dist/lib/basecommands/ExtendedBaseCommand.d.ts +1 -0
  45. package/dist/lib/basecommands/ExtendedBaseCommand.js +4 -0
  46. package/dist/lib/context/Context.d.ts +5 -2
  47. package/dist/lib/context/Context.js +10 -1
  48. package/dist/lib/context/FlagSetBuilder.d.ts +3 -4
  49. package/dist/lib/context/FlagSetBuilder.js +22 -15
  50. package/dist/lib/resources/container/flags.d.ts +13 -0
  51. package/dist/lib/resources/container/flags.js +34 -0
  52. package/dist/lib/resources/org/flags.js +7 -1
  53. package/dist/lib/resources/server/flags.js +7 -1
  54. package/dist/lib/resources/stack/enrich.d.ts +4 -0
  55. package/dist/lib/resources/stack/enrich.js +55 -0
  56. package/dist/lib/resources/stack/env.d.ts +1 -0
  57. package/dist/lib/resources/stack/env.js +11 -0
  58. package/dist/lib/resources/stack/flags.d.ts +5 -0
  59. package/dist/lib/resources/stack/flags.js +2 -0
  60. package/dist/lib/resources/stack/loader.d.ts +5 -0
  61. package/dist/lib/resources/stack/loader.js +47 -0
  62. package/dist/lib/resources/stack/loader.test.d.ts +1 -0
  63. package/dist/lib/resources/stack/loader.test.js +51 -0
  64. package/dist/lib/resources/stack/sanitize.d.ts +10 -0
  65. package/dist/lib/resources/stack/sanitize.js +43 -0
  66. package/dist/lib/resources/stack/types.d.ts +12 -0
  67. package/dist/lib/resources/stack/types.js +1 -0
  68. package/dist/lib/util/pager.d.ts +7 -0
  69. package/dist/lib/util/pager.js +21 -0
  70. package/dist/rendering/process/process.d.ts +1 -1
  71. package/dist/rendering/process/process_fancy.d.ts +1 -1
  72. package/dist/rendering/process/process_fancy.js +2 -1
  73. package/dist/rendering/process/process_flags.js +1 -1
  74. package/dist/rendering/process/process_quiet.d.ts +1 -1
  75. package/dist/rendering/process/process_quiet.js +3 -0
  76. package/dist/rendering/setup/FlagSupportedSetup.d.ts +1 -1
  77. package/package.json +21 -17
@@ -0,0 +1,11 @@
1
+ import * as fs from "fs/promises";
2
+ import { parse } from "envfile";
3
+ import { pathExists } from "../../util/fs/pathExists.js";
4
+ export async function collectEnvironment(base, envFile) {
5
+ if (!(await pathExists(envFile))) {
6
+ return base;
7
+ }
8
+ const defs = await fs.readFile(envFile, { encoding: "utf-8" });
9
+ const parsed = parse(defs);
10
+ return { ...base, ...parsed };
11
+ }
@@ -0,0 +1,5 @@
1
+ export declare const stackFlags: import("../../context/FlagSetBuilder.js").ContextFlags<"stack">, stackArgs: import("../../context/FlagSetBuilder.js").ContextArgs<"stack">, withStackId: (apiClient: import("@mittwald/api-client").MittwaldAPIV2Client, command: "flag" | "arg" | import("../../context/FlagSetBuilder.js").CommandType<"stack">, flags: {
2
+ [k: string]: unknown;
3
+ }, args: {
4
+ [k: string]: unknown;
5
+ }, cfg: import("@oclif/core").Config) => Promise<string>;
@@ -0,0 +1,2 @@
1
+ import FlagSetBuilder from "../../context/FlagSetBuilder.js";
2
+ export const { flags: stackFlags, args: stackArgs, withId: withStackId, } = new FlagSetBuilder("stack", "s").build();
@@ -0,0 +1,5 @@
1
+ import type { MittwaldAPIV2 } from "@mittwald/api-client";
2
+ type StackRequest = MittwaldAPIV2.Paths.V2StacksStackId.Put.Parameters.RequestBody;
3
+ export declare function loadStackFromFile(file: string, environment: Record<string, string | undefined>): Promise<StackRequest>;
4
+ export declare function loadStackFromStr(input: string, environment: Record<string, string | undefined>): Promise<StackRequest>;
5
+ export {};
@@ -0,0 +1,47 @@
1
+ import yaml from "js-yaml";
2
+ import * as fs from "fs";
3
+ function loadStackYAMLFromFile(file) {
4
+ if (file === "-") {
5
+ return fs.readFileSync(process.stdin.fd, { encoding: "utf-8" });
6
+ }
7
+ return fs.readFileSync(file, { encoding: "utf-8" });
8
+ }
9
+ export async function loadStackFromFile(file, environment) {
10
+ const input = loadStackYAMLFromFile(file);
11
+ return loadStackFromStr(input, environment);
12
+ }
13
+ export async function loadStackFromStr(input, environment) {
14
+ const stack = yaml.load(input);
15
+ const stackWithSubstitutedEnvironment = substituteEnvironmentVariables(stack, environment);
16
+ return stackWithSubstitutedEnvironment;
17
+ }
18
+ /**
19
+ * This function aims to be a (more-or-less) complete implementation of Docker
20
+ * Compose's variable interpolation feature.
21
+ *
22
+ * @param input Input object; may be anything coming out of a YAML file.
23
+ * @param environment A record of environment variables
24
+ * @see https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
25
+ */
26
+ function substituteEnvironmentVariables(input, environment) {
27
+ if (typeof input === "string") {
28
+ return input.replace(/\$\{(\w+)(:?-(.*))?}/g, (_, key, defExpr, def = "") => {
29
+ const useDefaultIfNonEmpty = defExpr && defExpr.startsWith(":");
30
+ if (useDefaultIfNonEmpty) {
31
+ return environment[key] || def;
32
+ }
33
+ return key in environment ? environment[key] : def;
34
+ });
35
+ }
36
+ if (Array.isArray(input)) {
37
+ return input.map((item) => substituteEnvironmentVariables(item, environment));
38
+ }
39
+ if (input && typeof input === "object") {
40
+ const result = {};
41
+ for (const [key, value] of Object.entries(input)) {
42
+ result[key] = substituteEnvironmentVariables(value, environment);
43
+ }
44
+ return result;
45
+ }
46
+ return input;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { loadStackFromStr } from "./loader.js";
3
+ describe("loadStackFromStr", () => {
4
+ it("should convert a stack YAML input to a stack definition", async () => {
5
+ const input = `services:
6
+ nginx:
7
+ image: nginx
8
+ ports:
9
+ - 80:80
10
+ environment:
11
+ FOO: bar
12
+ volumes:
13
+ data:/var/www
14
+ volumes:
15
+ data:`;
16
+ const stack = await loadStackFromStr(input, {});
17
+ expect(stack).toHaveProperty("services.nginx.image", "nginx");
18
+ });
19
+ it("interpolates environment variables in a stack definition", async () => {
20
+ const input = `services:
21
+ nginx:
22
+ image: nginx:\${NGINX_VERSION}`;
23
+ const stack = await loadStackFromStr(input, { NGINX_VERSION: "0.1.0" });
24
+ expect(stack).toHaveProperty("services.nginx.image", "nginx:0.1.0");
25
+ });
26
+ it("interpolates environment variables with default values", async () => {
27
+ const input = `services:
28
+ nginx:
29
+ image: nginx:\${NGINX_VERSION:-latest}`;
30
+ const stack = await loadStackFromStr(input, {});
31
+ expect(stack).toHaveProperty("services.nginx.image", "nginx:latest");
32
+ });
33
+ it("interpolates environment variables with default values for non-empty variables", async () => {
34
+ const input = `services:
35
+ nginx:
36
+ image: nginx
37
+ environment:
38
+ FOO: \${FOO:-foo}`;
39
+ const stack = await loadStackFromStr(input, { FOO: "" });
40
+ expect(stack).toHaveProperty("services.nginx.environment.FOO", "foo");
41
+ });
42
+ it("interpolates environment variables with default values for empty variables", async () => {
43
+ const input = `services:
44
+ nginx:
45
+ image: nginx
46
+ environment:
47
+ FOO: \${FOO-foo}`;
48
+ const stack = await loadStackFromStr(input, { FOO: "" });
49
+ expect(stack).toHaveProperty("services.nginx.environment.FOO", "");
50
+ });
51
+ });
@@ -0,0 +1,10 @@
1
+ import type { MittwaldAPIV2 } from "@mittwald/api-client";
2
+ type StackRequest = MittwaldAPIV2.Paths.V2StacksStackId.Put.Parameters.RequestBody;
3
+ /**
4
+ * This function is needed to work around the mStudios supposedly docker-compose
5
+ * compatible container stack API.
6
+ *
7
+ * @param stack
8
+ */
9
+ export declare function sanitizeStackDefinition(stack: StackRequest): StackRequest;
10
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * This function is needed to work around the mStudios supposedly docker-compose
3
+ * compatible container stack API.
4
+ *
5
+ * @param stack
6
+ */
7
+ export function sanitizeStackDefinition(stack) {
8
+ const sanitized = {
9
+ services: structuredClone(stack.services),
10
+ volumes: structuredClone(stack.volumes),
11
+ };
12
+ /* eslint-disable @typescript-eslint/no-explicit-any */
13
+ for (const serviceKey of Object.keys(sanitized.services ?? {})) {
14
+ const service = sanitized.services[serviceKey];
15
+ // mStudio requires "command" to be a string[]; in a docker-compose file, it
16
+ // can also be a regular string.
17
+ if (typeof service.command === "string") {
18
+ service.command = [service.command];
19
+ }
20
+ // descriptions are required for mStudio containers; docker compose has no
21
+ // equivalent field.
22
+ if (!service.description) {
23
+ service.description = serviceKey;
24
+ }
25
+ // mStudio calls it "envs", docker compose calls it "environment" 🫠
26
+ const serviceWithEnvironment = service;
27
+ if (serviceWithEnvironment.environment) {
28
+ service.envs = serviceWithEnvironment.environment;
29
+ delete serviceWithEnvironment.environment;
30
+ }
31
+ if (!service.envs) {
32
+ service.envs = {};
33
+ }
34
+ }
35
+ // For mStudio, volume definitions must be empty objects, null is apparently
36
+ // not allowed.
37
+ for (const volumeKey of Object.keys(sanitized.volumes ?? {})) {
38
+ if (sanitized.volumes[volumeKey] === null) {
39
+ sanitized.volumes[volumeKey] = { name: volumeKey };
40
+ }
41
+ }
42
+ return sanitized;
43
+ }
@@ -0,0 +1,12 @@
1
+ import type { MittwaldAPIV2 } from "@mittwald/api-client";
2
+ type ContainerServiceDeclareRequest = MittwaldAPIV2.Components.Schemas.ContainerServiceDeclareRequest;
3
+ export type ContainerServiceInput = ContainerServiceDeclareRequest & {
4
+ command?: string[] | string;
5
+ entrypoint?: string[] | string;
6
+ env_file?: string;
7
+ environment?: {
8
+ [k: string]: string;
9
+ };
10
+ ports?: string[];
11
+ };
12
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Prints the given content to a pager, such as `less`. This function respects
3
+ * the `PAGER` environment variable.
4
+ *
5
+ * @param content
6
+ */
7
+ export declare function printToPager(content: string): void;
@@ -0,0 +1,21 @@
1
+ import tempfile from "tempfile";
2
+ import fs from "fs";
3
+ import cp from "child_process";
4
+ /**
5
+ * Prints the given content to a pager, such as `less`. This function respects
6
+ * the `PAGER` environment variable.
7
+ *
8
+ * @param content
9
+ */
10
+ export function printToPager(content) {
11
+ const t = tempfile();
12
+ try {
13
+ fs.writeFileSync(t, content, { encoding: "utf8" });
14
+ cp.spawnSync(process.env.PAGER || "less", [t], {
15
+ stdio: "inherit",
16
+ });
17
+ }
18
+ finally {
19
+ fs.unlinkSync(t);
20
+ }
21
+ }
@@ -54,7 +54,7 @@ export declare class RunnableHandler {
54
54
  export interface ProcessRenderer {
55
55
  start(): void;
56
56
  addStep(title: ReactNode): RunnableHandler;
57
- runStep<TRes>(title: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
57
+ runStep<TRes>(title: ReactNode, fn: (() => Promise<TRes>) | Promise<TRes>): Promise<TRes>;
58
58
  addInfo(title: ReactNode): void;
59
59
  addConfirmation(question: ReactNode): Promise<boolean>;
60
60
  addInput(question: ReactNode, mask?: boolean): Promise<string>;
@@ -8,7 +8,7 @@ export declare class FancyProcessRenderer implements ProcessRenderer {
8
8
  constructor(title: string);
9
9
  start(): void;
10
10
  addStep(title: ReactNode): RunnableHandler;
11
- runStep<TRes>(title: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
11
+ runStep<TRes>(title: ReactNode, fn: (() => Promise<TRes>) | Promise<TRes>): Promise<TRes>;
12
12
  addInfo(title: ReactNode): void;
13
13
  addInput(question: React.ReactElement, mask?: boolean): Promise<string>;
14
14
  addSelect<TVal>(question: React.ReactNode, options: {
@@ -40,7 +40,8 @@ export class FancyProcessRenderer {
40
40
  async runStep(title, fn) {
41
41
  const step = this.addStep(title);
42
42
  try {
43
- const result = await fn();
43
+ const promise = typeof fn === "function" ? fn() : fn;
44
+ const result = await promise;
44
45
  step.complete();
45
46
  return result;
46
47
  }
@@ -4,7 +4,7 @@ import { SilentProcessRenderer } from "./process_quiet.js";
4
4
  export const processFlags = {
5
5
  quiet: Flags.boolean({
6
6
  char: "q",
7
- summary: "suppress process output and only display a machine-readable summary.",
7
+ summary: "suppress process output and only display a machine-readable summary",
8
8
  description: "This flag controls if you want to see the process output or only a summary. When using <%= config.bin %> non-interactively (e.g. in scripts), you can use this flag to easily get the IDs of created resources for further processing.",
9
9
  }),
10
10
  };
@@ -4,7 +4,7 @@ export declare class SilentProcessRenderer implements ProcessRenderer {
4
4
  private cleanupFns;
5
5
  start(): void;
6
6
  addStep(title: ReactNode): RunnableHandler;
7
- runStep<TRes>(unusedTitle: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
7
+ runStep<TRes>(unusedTitle: ReactNode, fn: (() => Promise<TRes>) | Promise<TRes>): Promise<TRes>;
8
8
  addInfo(): void;
9
9
  complete(): Promise<void>;
10
10
  error(err: unknown): Promise<void>;
@@ -11,6 +11,9 @@ export class SilentProcessRenderer {
11
11
  });
12
12
  }
13
13
  runStep(unusedTitle, fn) {
14
+ if (fn instanceof Promise) {
15
+ return fn;
16
+ }
14
17
  return fn();
15
18
  }
16
19
  addInfo() {
@@ -8,5 +8,5 @@ export declare abstract class FlagSupportedSetup<TFlags extends FlagInput, TSett
8
8
  protected constructor(flagsInput: TFlags, settings: TSettings);
9
9
  getSetup(flags: InferredFlags<TFlags>): TSettings & TSetupObject;
10
10
  protected abstract getFlagsOutput(flags: InferredFlags<TFlags>): TSetupObject;
11
- static build: <TFlags extends FlagInput, TSettings, TOutput>(flags: TFlags_1, defaultSettings: TSettings_1, buildFlagsOutput: (flags: InferredFlags<TFlags_1>, settings: TSettings_1) => TOutput) => Class<FlagSupportedSetup<TFlags_1, TSettings_1, TOutput>, [Partial<TSettings_1>] | []>;
11
+ static build: <TFlags_1 extends FlagInput, TSettings_1, TOutput>(flags: TFlags_1, defaultSettings: TSettings_1, buildFlagsOutput: (flags: InferredFlags<TFlags_1>, settings: TSettings_1) => TOutput) => Class<FlagSupportedSetup<TFlags_1, TSettings_1, TOutput>, [Partial<TSettings_1>] | []>;
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@mittwald/api-client": "^4.131.0",
46
- "@mittwald/react-use-promise": "^3.0.4",
46
+ "@mittwald/react-use-promise": "^2.6.0",
47
47
  "@oclif/core": "^4.0.18",
48
48
  "@oclif/plugin-autocomplete": "^3.0.3",
49
49
  "@oclif/plugin-help": "^6.0.5",
@@ -52,12 +52,14 @@
52
52
  "axios-retry": "^4.0.0",
53
53
  "chalk": "^5.3.0",
54
54
  "date-fns": "^4.0.0",
55
+ "docker-names": "^1.2.1",
56
+ "envfile": "^7.1.0",
55
57
  "ink": "^5.0.1",
56
58
  "ink-link": "^4.0.0",
57
59
  "ink-text-input": "^6.0.0",
58
60
  "js-yaml": "^4.1.0",
59
- "marked": "^12.0.0",
60
- "marked-terminal": "^6.0.0",
61
+ "marked": "^15.0.12",
62
+ "marked-terminal": "^7.3.0",
61
63
  "open": "^10.0.3",
62
64
  "parse-duration": "^2.0.1",
63
65
  "pretty-bytes": "^6.1.0",
@@ -73,31 +75,27 @@
73
75
  "devDependencies": {
74
76
  "@jest/globals": "^29.7.0",
75
77
  "@oclif/test": "^4.0.4",
76
- "@types/chalk": "^2.2.0",
77
78
  "@types/js-yaml": "^4.0.9",
78
- "@types/marked-terminal": "^3.1.3",
79
79
  "@types/node": "^22.7.5",
80
- "@types/parse-duration": "^0.3.0",
81
- "@types/pretty-bytes": "^5.2.0",
82
80
  "@types/react": "^18",
83
81
  "@types/semver": "^7.5.0",
84
82
  "@types/shell-escape": "^0.2.3",
85
- "@typescript-eslint/eslint-plugin": "^8.3.0",
86
- "@typescript-eslint/parser": "^8.3.0",
83
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
84
+ "@typescript-eslint/parser": "^8.35.0",
87
85
  "@yarnpkg/pnpify": "^4.0.0-rc.48",
88
- "eslint": "^9.9.1",
89
- "eslint-config-prettier": "^10.0.1",
90
- "eslint-plugin-json": "^4.0.0",
91
- "eslint-plugin-prettier": "^5.1.3",
86
+ "eslint": "^9.29.0",
87
+ "eslint-config-prettier": "^10.1.5",
88
+ "eslint-plugin-json": "^4.0.1",
89
+ "eslint-plugin-prettier": "^5.5.1",
92
90
  "globals": "^16.0.0",
93
91
  "jest": "^29.7.0",
94
92
  "license-checker-rseidelsohn": "^4.2.6",
95
93
  "nock": "^14.0.0",
96
94
  "oclif": "^4.14.31",
97
- "prettier": "^3.2.5",
98
- "prettier-plugin-jsdoc": "^1.3.0",
95
+ "prettier": "~3.5.3",
96
+ "prettier-plugin-jsdoc": "^1.3.2",
99
97
  "prettier-plugin-package": "^1.4.0",
100
- "prettier-plugin-sort-json": "^4.0.0",
98
+ "prettier-plugin-sort-json": "^4.1.1",
101
99
  "rimraf": "^5.0.1",
102
100
  "ts-jest": "^29.2.5",
103
101
  "ts-node": "^10.9.2",
@@ -127,6 +125,9 @@
127
125
  }
128
126
  }
129
127
  },
128
+ "registry": {
129
+ "description": "Manage container registries"
130
+ },
130
131
  "article": {
131
132
  "description": "Query available hosting articles"
132
133
  },
@@ -202,6 +203,9 @@
202
203
  "ssh-user": {
203
204
  "description": "Manage SSH users of your projects"
204
205
  },
206
+ "stack": {
207
+ "description": "Manage container stacks"
208
+ },
205
209
  "user": {
206
210
  "description": "Manage your own user account",
207
211
  "subtopics": {