@mittwald/cli 1.4.4 → 1.5.1

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 (83) hide show
  1. package/README.md +3 -0
  2. package/dist/commands/container/delete.d.ts +15 -0
  3. package/dist/commands/container/delete.js +39 -0
  4. package/dist/commands/container/list.d.ts +23 -0
  5. package/dist/commands/container/list.js +53 -0
  6. package/dist/commands/container/logs.d.ts +14 -0
  7. package/dist/commands/container/logs.js +47 -0
  8. package/dist/commands/container/recreate.d.ts +20 -0
  9. package/dist/commands/container/recreate.js +72 -0
  10. package/dist/commands/container/restart.d.ts +18 -0
  11. package/dist/commands/container/restart.js +40 -0
  12. package/dist/commands/container/run.d.ts +41 -0
  13. package/dist/commands/container/run.js +167 -0
  14. package/dist/commands/container/start.d.ts +18 -0
  15. package/dist/commands/container/start.js +40 -0
  16. package/dist/commands/container/stop.d.ts +18 -0
  17. package/dist/commands/container/stop.js +40 -0
  18. package/dist/commands/container/update.d.ts +36 -0
  19. package/dist/commands/container/update.js +174 -0
  20. package/dist/commands/context/set.d.ts +1 -0
  21. package/dist/commands/context/set.js +8 -0
  22. package/dist/commands/cronjob/execution/logs.js +6 -24
  23. package/dist/commands/cronjob/list.d.ts +2 -3
  24. package/dist/commands/domain/virtualhost/create.d.ts +1 -0
  25. package/dist/commands/domain/virtualhost/create.js +12 -0
  26. package/dist/commands/extension/install.js +4 -3
  27. package/dist/commands/extension/list-installed.js +4 -3
  28. package/dist/commands/mail/address/create.d.ts +2 -1
  29. package/dist/commands/mail/address/create.js +2 -1
  30. package/dist/commands/org/membership/list-own.d.ts +3 -0
  31. package/dist/commands/org/membership/list.d.ts +3 -0
  32. package/dist/commands/registry/create.d.ts +20 -0
  33. package/dist/commands/registry/create.js +77 -0
  34. package/dist/commands/registry/delete.d.ts +13 -0
  35. package/dist/commands/registry/delete.js +21 -0
  36. package/dist/commands/registry/list.d.ts +23 -0
  37. package/dist/commands/registry/list.js +33 -0
  38. package/dist/commands/registry/update.d.ts +20 -0
  39. package/dist/commands/registry/update.js +73 -0
  40. package/dist/commands/stack/delete.d.ts +16 -0
  41. package/dist/commands/stack/delete.js +54 -0
  42. package/dist/commands/stack/deploy.d.ts +18 -0
  43. package/dist/commands/stack/deploy.js +75 -0
  44. package/dist/commands/stack/list.d.ts +24 -0
  45. package/dist/commands/stack/list.js +60 -0
  46. package/dist/commands/stack/ps.d.ts +23 -0
  47. package/dist/commands/stack/ps.js +51 -0
  48. package/dist/lib/basecommands/DeleteBaseCommand.js +1 -1
  49. package/dist/lib/basecommands/ExtendedBaseCommand.d.ts +1 -0
  50. package/dist/lib/basecommands/ExtendedBaseCommand.js +4 -0
  51. package/dist/lib/context/Context.d.ts +5 -2
  52. package/dist/lib/context/Context.js +10 -1
  53. package/dist/lib/context/FlagSetBuilder.d.ts +3 -4
  54. package/dist/lib/context/FlagSetBuilder.js +22 -15
  55. package/dist/lib/resources/container/containerconfig.d.ts +43 -0
  56. package/dist/lib/resources/container/containerconfig.js +82 -0
  57. package/dist/lib/resources/container/flags.d.ts +13 -0
  58. package/dist/lib/resources/container/flags.js +34 -0
  59. package/dist/lib/resources/org/flags.js +7 -1
  60. package/dist/lib/resources/server/flags.js +7 -1
  61. package/dist/lib/resources/stack/enrich.d.ts +4 -0
  62. package/dist/lib/resources/stack/enrich.js +55 -0
  63. package/dist/lib/resources/stack/env.d.ts +1 -0
  64. package/dist/lib/resources/stack/env.js +11 -0
  65. package/dist/lib/resources/stack/flags.d.ts +5 -0
  66. package/dist/lib/resources/stack/flags.js +2 -0
  67. package/dist/lib/resources/stack/loader.d.ts +5 -0
  68. package/dist/lib/resources/stack/loader.js +47 -0
  69. package/dist/lib/resources/stack/loader.test.d.ts +1 -0
  70. package/dist/lib/resources/stack/loader.test.js +51 -0
  71. package/dist/lib/resources/stack/sanitize.d.ts +10 -0
  72. package/dist/lib/resources/stack/sanitize.js +43 -0
  73. package/dist/lib/resources/stack/types.d.ts +12 -0
  74. package/dist/lib/resources/stack/types.js +1 -0
  75. package/dist/lib/util/pager.d.ts +7 -0
  76. package/dist/lib/util/pager.js +21 -0
  77. package/dist/rendering/process/process.d.ts +1 -1
  78. package/dist/rendering/process/process_fancy.d.ts +1 -1
  79. package/dist/rendering/process/process_fancy.js +2 -1
  80. package/dist/rendering/process/process_flags.js +1 -1
  81. package/dist/rendering/process/process_quiet.d.ts +1 -1
  82. package/dist/rendering/process/process_quiet.js +3 -0
  83. package/package.json +21 -17
@@ -0,0 +1,21 @@
1
+ import { DeleteBaseCommand } from "../../lib/basecommands/DeleteBaseCommand.js";
2
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
3
+ import { Args } from "@oclif/core";
4
+ export default class Delete extends DeleteBaseCommand {
5
+ static description = "Delete a container registry";
6
+ static resourceName = "container registry";
7
+ static flags = { ...DeleteBaseCommand.baseFlags };
8
+ static args = {
9
+ "registry-id": Args.string({
10
+ summary: "id of the container registry to delete",
11
+ required: true,
12
+ }),
13
+ };
14
+ async deleteResource() {
15
+ const registryId = this.args["registry-id"];
16
+ const response = await this.apiClient.container.deleteRegistry({
17
+ registryId,
18
+ });
19
+ assertSuccess(response);
20
+ }
21
+ }
@@ -0,0 +1,23 @@
1
+ import { Simplify } from "@mittwald/api-client-commons";
2
+ import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
3
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
4
+ import { ListColumns } from "../../rendering/formatter/Table.js";
5
+ type ContainerRegistry = MittwaldAPIV2.Components.Schemas.ContainerRegistry;
6
+ type ResponseItem = Simplify<ContainerRegistry>;
7
+ type Response = Awaited<ReturnType<MittwaldAPIV2Client["container"]["listRegistries"]>>;
8
+ export declare class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
9
+ static description: string;
10
+ static args: {};
11
+ static flags: {
12
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
13
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
14
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
19
+ };
20
+ getData(): Promise<Response>;
21
+ protected getColumns(data: ResponseItem[]): ListColumns<ResponseItem>;
22
+ }
23
+ export {};
@@ -0,0 +1,33 @@
1
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
2
+ import { projectFlags } from "../../lib/resources/project/flags.js";
3
+ export class List extends ListBaseCommand {
4
+ static description = "List container registries.";
5
+ static args = {};
6
+ static flags = {
7
+ ...ListBaseCommand.baseFlags,
8
+ ...projectFlags,
9
+ };
10
+ async getData() {
11
+ const projectId = await this.withProjectId(List);
12
+ return this.apiClient.container.listRegistries({ projectId });
13
+ }
14
+ getColumns(data) {
15
+ const { id } = super.getColumns(data);
16
+ return {
17
+ id,
18
+ description: {},
19
+ uri: {
20
+ header: "URI",
21
+ },
22
+ credentials: {
23
+ header: "Credentials",
24
+ get: (registry) => {
25
+ if (!registry.credentials) {
26
+ return "";
27
+ }
28
+ return `${registry.credentials.username} (${registry.credentials.valid ? "valid" : "invalid"})`;
29
+ },
30
+ },
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
2
+ import { ReactNode } from "react";
3
+ type UpdateResult = void;
4
+ export default class Update extends ExecRenderBaseCommand<typeof Update, UpdateResult> {
5
+ static description: string;
6
+ static args: {
7
+ "registry-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ uri: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ username: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ protected exec(): Promise<void>;
17
+ protected render(): ReactNode;
18
+ private getPassword;
19
+ }
20
+ export {};
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
3
+ import { Args, Flags } from "@oclif/core";
4
+ import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
5
+ import { Success } from "../../rendering/react/components/Success.js";
6
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
7
+ import { Text } from "ink";
8
+ export default class Update extends ExecRenderBaseCommand {
9
+ static description = "Update an existing container registry";
10
+ static args = {
11
+ "registry-id": Args.string({
12
+ summary: "id of the container registry to update",
13
+ required: true,
14
+ }),
15
+ };
16
+ static flags = {
17
+ ...processFlags,
18
+ description: Flags.string({
19
+ summary: "new description for the registry",
20
+ }),
21
+ uri: Flags.string({
22
+ summary: "new uri for the registry",
23
+ }),
24
+ username: Flags.string({
25
+ summary: "username for registry authentication",
26
+ }),
27
+ password: Flags.string({
28
+ summary: "password for registry authentication",
29
+ description: "If omitted but username is provided, the command will prompt interactively for a password.\n\nCAUTION: providing this flag may log your password in your shell history!",
30
+ }),
31
+ };
32
+ async exec() {
33
+ const registryId = this.args["registry-id"];
34
+ const process = makeProcessRenderer(this.flags, "Updating container registry");
35
+ const { description, uri, username } = this.flags;
36
+ const registryUpdatePayload = {};
37
+ if (description) {
38
+ registryUpdatePayload.description = description;
39
+ }
40
+ if (uri) {
41
+ registryUpdatePayload.uri = uri;
42
+ }
43
+ if (username) {
44
+ const password = await this.getPassword(process);
45
+ registryUpdatePayload.credentials = {
46
+ username,
47
+ password,
48
+ };
49
+ }
50
+ if (Object.keys(registryUpdatePayload).length == 0) {
51
+ await process.complete(_jsx(Success, { children: "Nothing to change. Have a good day!" }));
52
+ return;
53
+ }
54
+ await process.runStep("Updating container registry", async () => {
55
+ const response = await this.apiClient.container.updateRegistry({
56
+ registryId,
57
+ data: registryUpdatePayload,
58
+ });
59
+ assertSuccess(response);
60
+ });
61
+ await process.complete(_jsx(Success, { children: "Your container registry has successfully been updated." }));
62
+ return;
63
+ }
64
+ render() {
65
+ return true;
66
+ }
67
+ async getPassword(process) {
68
+ if (this.flags.password) {
69
+ return this.flags.password;
70
+ }
71
+ return await process.addInput(_jsx(Text, { children: "enter registry password" }), true);
72
+ }
73
+ }
@@ -0,0 +1,16 @@
1
+ import { DeleteBaseCommand } from "../../lib/basecommands/DeleteBaseCommand.js";
2
+ export default class Delete extends DeleteBaseCommand<typeof Delete> {
3
+ static description: string;
4
+ static resourceName: string;
5
+ static aliases: string[];
6
+ static flags: {
7
+ "with-volumes": import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ static args: {
12
+ "stack-id": import("@oclif/core/interfaces").Arg<string>;
13
+ };
14
+ protected deleteResource(): Promise<void>;
15
+ protected deleteVolumes(stackId: string, projectId: string): Promise<void>;
16
+ }
@@ -0,0 +1,54 @@
1
+ import { DeleteBaseCommand } from "../../lib/basecommands/DeleteBaseCommand.js";
2
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
3
+ import { stackArgs } from "../../lib/resources/stack/flags.js";
4
+ import { assertStatus } from "@mittwald/api-client";
5
+ import { Flags } from "@oclif/core";
6
+ export default class Delete extends DeleteBaseCommand {
7
+ static description = "Delete a container stack";
8
+ static resourceName = "container stack";
9
+ static aliases = ["stack:rm"];
10
+ static flags = {
11
+ ...DeleteBaseCommand.baseFlags,
12
+ "with-volumes": Flags.boolean({
13
+ summary: "also include remove volumes in removal",
14
+ default: false,
15
+ char: "v",
16
+ }),
17
+ };
18
+ static args = { ...stackArgs };
19
+ async deleteResource() {
20
+ const stackId = await this.withStackId(Delete);
21
+ const stackResponse = await this.apiClient.container.getStack({ stackId });
22
+ assertStatus(stackResponse, 200);
23
+ if (stackResponse.data.projectId !== stackResponse.data.id) {
24
+ throw new Error("not implemented");
25
+ }
26
+ const resp = await this.apiClient.container.declareStack({
27
+ stackId,
28
+ data: {
29
+ services: {},
30
+ volumes: {},
31
+ },
32
+ });
33
+ assertSuccess(resp);
34
+ if (this.flags["with-volumes"]) {
35
+ await this.deleteVolumes(stackId, stackResponse.data.projectId);
36
+ }
37
+ }
38
+ async deleteVolumes(stackId, projectId) {
39
+ const volumesResponse = await this.apiClient.container.listVolumes({
40
+ projectId,
41
+ });
42
+ assertStatus(volumesResponse, 200);
43
+ for (const volume of volumesResponse.data) {
44
+ if (volume.stackId !== stackId) {
45
+ continue;
46
+ }
47
+ const deleteVolumeResponse = await this.apiClient.container.deleteVolume({
48
+ stackId: stackId,
49
+ volumeId: volume.id,
50
+ });
51
+ assertSuccess(deleteVolumeResponse);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
2
+ import { ReactNode } from "react";
3
+ interface DeployResult {
4
+ restartedServices: string[];
5
+ }
6
+ export declare class Deploy extends ExecRenderBaseCommand<typeof Deploy, DeployResult> {
7
+ static description: string;
8
+ static aliases: string[];
9
+ static flags: {
10
+ "compose-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ "env-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ "stack-id": import("@oclif/core/interfaces").OptionFlag<string>;
14
+ };
15
+ protected exec(): Promise<DeployResult>;
16
+ protected render({ restartedServices }: DeployResult): ReactNode;
17
+ }
18
+ export {};
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
3
+ import { stackFlags, withStackId } from "../../lib/resources/stack/flags.js";
4
+ import { Flags } from "@oclif/core";
5
+ import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
6
+ import { loadStackFromFile } from "../../lib/resources/stack/loader.js";
7
+ import { assertStatus } from "@mittwald/api-client";
8
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
9
+ import { collectEnvironment } from "../../lib/resources/stack/env.js";
10
+ import { sanitizeStackDefinition } from "../../lib/resources/stack/sanitize.js";
11
+ import { enrichStackDefinition } from "../../lib/resources/stack/enrich.js";
12
+ import { Success } from "../../rendering/react/components/Success.js";
13
+ import { Value } from "../../rendering/react/components/Value.js";
14
+ export class Deploy extends ExecRenderBaseCommand {
15
+ static description = "Deploys a docker-compose compatible file to a mittwald container stack";
16
+ static aliases = ["stack:up"];
17
+ static flags = {
18
+ ...stackFlags,
19
+ ...processFlags,
20
+ "compose-file": Flags.string({
21
+ summary: 'path to a compose file, or "-" to read from stdin',
22
+ default: "./docker-compose.yml",
23
+ char: "c",
24
+ }),
25
+ "env-file": Flags.file({
26
+ summary: "alternative path to file with environment variables",
27
+ default: "./.env",
28
+ }),
29
+ };
30
+ async exec() {
31
+ const stackId = await withStackId(this.apiClient, Deploy, this.flags, this.args, this.config);
32
+ const { "compose-file": composeFile, "env-file": envFile } = this.flags;
33
+ const r = makeProcessRenderer(this.flags, "Deploying container stack");
34
+ const result = { restartedServices: [] };
35
+ const stack = await r.runStep("getting stack", async () => {
36
+ const resp = await this.apiClient.container.getStack({ stackId });
37
+ assertStatus(resp, 200);
38
+ return resp.data;
39
+ });
40
+ const env = await collectEnvironment(process.env, envFile);
41
+ let stackDefinition = await loadStackFromFile(composeFile, env);
42
+ stackDefinition = sanitizeStackDefinition(stackDefinition);
43
+ stackDefinition = await r.runStep("getting image configurations", async () => {
44
+ return enrichStackDefinition(this.apiClient, stack.projectId, stackDefinition);
45
+ });
46
+ this.debug("complete stack definition: %O", stackDefinition);
47
+ const declaredStack = await r.runStep("deploying stack", async () => {
48
+ const resp = await this.apiClient.container.declareStack({
49
+ stackId,
50
+ data: stackDefinition,
51
+ });
52
+ assertStatus(resp, 200);
53
+ return resp.data;
54
+ });
55
+ for (const service of declaredStack.services ?? []) {
56
+ if (service.requiresRecreate) {
57
+ await r.runStep(`recreating service ${service.serviceName}`, async () => {
58
+ const resp = await this.apiClient.container.recreateService({
59
+ stackId,
60
+ serviceId: service.id,
61
+ });
62
+ assertSuccess(resp);
63
+ result.restartedServices.push(service.serviceName);
64
+ });
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ render({ restartedServices }) {
70
+ if (restartedServices.length === 0) {
71
+ return (_jsx(Success, { children: "Deployment successful. No services were restarted." }));
72
+ }
73
+ return (_jsxs(Success, { children: ["Deployment successful. The following services were restarted:", " ", _jsx(Value, { children: restartedServices.join(", ") })] }));
74
+ }
75
+ }
@@ -0,0 +1,24 @@
1
+ import { Response, Simplify } from "@mittwald/api-client-commons";
2
+ import { type MittwaldAPIV2 } from "@mittwald/api-client";
3
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
4
+ import { ListColumns } from "../../rendering/formatter/Table.js";
5
+ type Stack = MittwaldAPIV2.Components.Schemas.ContainerStackResponse;
6
+ type ListResponse = Response<Stack[]>;
7
+ type ListItem = Simplify<Stack>;
8
+ export declare class List extends ListBaseCommand<typeof List, ListItem, ListResponse> {
9
+ static description: string;
10
+ static aliases: string[];
11
+ static args: {};
12
+ static flags: {
13
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
14
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
15
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
20
+ };
21
+ getData(): Promise<ListResponse>;
22
+ protected getColumns(data: ListItem[]): ListColumns<ListItem>;
23
+ }
24
+ export {};
@@ -0,0 +1,60 @@
1
+ import { assertStatus } from "@mittwald/api-client";
2
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
3
+ import { projectFlags } from "../../lib/resources/project/flags.js";
4
+ export class List extends ListBaseCommand {
5
+ static description = "List container stacks for a given project.";
6
+ static aliases = ["stack:ls"];
7
+ static args = {};
8
+ static flags = {
9
+ ...ListBaseCommand.baseFlags,
10
+ ...projectFlags,
11
+ };
12
+ async getData() {
13
+ const projectId = await this.withProjectId(List);
14
+ const response = await this.apiClient.container.listStacks({
15
+ projectId,
16
+ });
17
+ assertStatus(response, 200);
18
+ return response;
19
+ }
20
+ getColumns(data) {
21
+ const { id } = super.getColumns(data);
22
+ return {
23
+ id,
24
+ description: {},
25
+ services: {
26
+ get(stack) {
27
+ if (stack.services === undefined || stack.services.length === 0) {
28
+ return "no services";
29
+ }
30
+ const countByStatus = {};
31
+ let requireRecreate = 0;
32
+ for (const service of stack.services || []) {
33
+ countByStatus[service.status] =
34
+ (countByStatus[service.status] || 0) + 1;
35
+ if (service.requiresRecreate) {
36
+ requireRecreate++;
37
+ }
38
+ }
39
+ const summaryItems = [];
40
+ for (const status of Object.keys(countByStatus)) {
41
+ summaryItems.push(countByStatus[status] + " " + status);
42
+ }
43
+ let summary = summaryItems.join(", ");
44
+ if (requireRecreate > 0) {
45
+ summary += ` (${requireRecreate} require recreation)`;
46
+ }
47
+ return summary;
48
+ },
49
+ },
50
+ volumes: {
51
+ get(stack) {
52
+ if (stack.volumes === undefined || stack.volumes.length === 0) {
53
+ return "no volumes";
54
+ }
55
+ return stack.volumes.length + " volumes";
56
+ },
57
+ },
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,23 @@
1
+ import { Response, Simplify } from "@mittwald/api-client-commons";
2
+ import { type MittwaldAPIV2 } from "@mittwald/api-client";
3
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
4
+ import { ListColumns } from "../../rendering/formatter/Table.js";
5
+ type ContainerServiceResponse = MittwaldAPIV2.Components.Schemas.ContainerServiceResponse;
6
+ type ListResponse = Response<ContainerServiceResponse[]>;
7
+ type ListItem = Simplify<ContainerServiceResponse>;
8
+ export declare class ListContainers extends ListBaseCommand<typeof ListContainers, ListItem, ListResponse> {
9
+ static description: string;
10
+ static args: {};
11
+ static flags: {
12
+ "stack-id": import("@oclif/core/interfaces").OptionFlag<string>;
13
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
14
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
19
+ };
20
+ getData(): Promise<ListItem[]>;
21
+ protected getColumns(data: ListItem[]): ListColumns<ListItem>;
22
+ }
23
+ export {};
@@ -0,0 +1,51 @@
1
+ import { assertStatus } from "@mittwald/api-client";
2
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
3
+ import { stackFlags } from "../../lib/resources/stack/flags.js";
4
+ import { makeDateRendererForFormat } from "../../rendering/textformat/formatDate.js";
5
+ export class ListContainers extends ListBaseCommand {
6
+ static description = "List all services within a given container stack.";
7
+ static args = {};
8
+ static flags = {
9
+ ...ListBaseCommand.baseFlags,
10
+ ...stackFlags,
11
+ };
12
+ async getData() {
13
+ const stackId = await this.withStackId(ListContainers);
14
+ const response = await this.apiClient.container.getStack({
15
+ stackId,
16
+ });
17
+ assertStatus(response, 200);
18
+ return response.data.services || [];
19
+ }
20
+ getColumns(data) {
21
+ const { id } = super.getColumns(data);
22
+ const dateFormatter = makeDateRendererForFormat(this.flags.output, !this.flags["no-relative-dates"]);
23
+ return {
24
+ id,
25
+ name: {
26
+ get: (svc) => svc.serviceName,
27
+ },
28
+ image: {
29
+ get: (svc) => svc.deployedState.image,
30
+ },
31
+ command: {
32
+ get: (svc) => svc.deployedState.command?.join(" "),
33
+ },
34
+ description: {},
35
+ ports: {
36
+ get: (svc) => {
37
+ if (svc.deployedState.ports === undefined ||
38
+ svc.deployedState.ports.length === 0) {
39
+ return "no ports";
40
+ }
41
+ return svc.deployedState.ports.join(", ");
42
+ },
43
+ },
44
+ status: {
45
+ get(svc) {
46
+ return svc.status + " (" + dateFormatter(svc.statusSetAt) + ")";
47
+ },
48
+ },
49
+ };
50
+ }
51
+ }
@@ -10,7 +10,7 @@ export class DeleteBaseCommand extends ExecRenderBaseCommand {
10
10
  ...processFlags,
11
11
  force: Flags.boolean({
12
12
  char: "f",
13
- description: "Do not ask for confirmation",
13
+ description: "do not ask for confirmation",
14
14
  }),
15
15
  };
16
16
  async exec() {
@@ -8,4 +8,5 @@ export declare abstract class ExtendedBaseCommand<T extends typeof BaseCommand>
8
8
  withAppInstallationId(command: CommandType<"installation"> | "flag" | "arg"): Promise<string>;
9
9
  withProjectId(command: CommandType<"project"> | "flag" | "arg"): Promise<string>;
10
10
  withServerId(command: CommandType<"server"> | "flag" | "arg"): Promise<string>;
11
+ withStackId(command: CommandType<"stack"> | "flag" | "arg"): Promise<string>;
11
12
  }
@@ -2,6 +2,7 @@ import { BaseCommand } from "./BaseCommand.js";
2
2
  import { withAppInstallationId } from "../resources/app/flags.js";
3
3
  import { withProjectId } from "../resources/project/flags.js";
4
4
  import { withServerId } from "../resources/server/flags.js";
5
+ import { withStackId } from "../resources/stack/flags.js";
5
6
  export class ExtendedBaseCommand extends BaseCommand {
6
7
  flags;
7
8
  args;
@@ -25,4 +26,7 @@ export class ExtendedBaseCommand extends BaseCommand {
25
26
  async withServerId(command) {
26
27
  return withServerId(this.apiClient, command, this.flags, this.args, this.config);
27
28
  }
29
+ async withStackId(command) {
30
+ return withStackId(this.apiClient, command, this.flags, this.args, this.config);
31
+ }
28
32
  }
@@ -1,7 +1,7 @@
1
1
  import { Config } from "@oclif/core";
2
2
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
3
3
  import ContextProvider from "./ContextProvider.js";
4
- export type ContextNames = "project" | "server" | "org" | "installation" | "domain" | "dnszone" | "mailaddress" | "maildeliverybox" | "conversation" | "backup";
4
+ export type ContextNames = "project" | "server" | "org" | "installation" | "domain" | "dnszone" | "mailaddress" | "maildeliverybox" | "conversation" | "backup" | "stack" | "container";
5
5
  export type ContextKey<N extends ContextNames = ContextNames> = `${N}-id`;
6
6
  export type ContextMap = Partial<Record<ContextKey, ContextValue>>;
7
7
  export type ContextMapUpdate = Partial<Record<ContextKey, string>>;
@@ -14,7 +14,7 @@ export type ContextValue = {
14
14
  source: ContextValueSource;
15
15
  };
16
16
  export declare const contextIDNormalizers: {
17
- [k in ContextKey]?: (apiClient: MittwaldAPIV2Client, id: string) => Promise<string>;
17
+ [k in ContextKey]?: (apiClient: MittwaldAPIV2Client, id: string, ctx: Context) => Promise<string>;
18
18
  };
19
19
  export interface ContextOptions {
20
20
  onInitError: (err: unknown) => void;
@@ -29,13 +29,16 @@ export default class Context {
29
29
  reset(): Promise<void>;
30
30
  private persist;
31
31
  private setContextValue;
32
+ mustGetContextValue(key: ContextKey): Promise<ContextValue>;
32
33
  getContextValue(key: ContextKey): Promise<ContextValue | undefined>;
33
34
  setProjectId: (id: string) => Promise<string>;
34
35
  setServerId: (id: string) => Promise<string>;
35
36
  setOrgId: (id: string) => Promise<string>;
36
37
  setAppInstallationId: (id: string) => Promise<string>;
38
+ setStackId: (id: string) => Promise<string>;
37
39
  projectId: () => Promise<ContextValue | undefined>;
38
40
  serverId: () => Promise<ContextValue | undefined>;
39
41
  orgId: () => Promise<ContextValue | undefined>;
40
42
  appInstallationId: () => Promise<ContextValue | undefined>;
43
+ stackId: () => Promise<ContextValue | undefined>;
41
44
  }
@@ -57,11 +57,18 @@ export default class Context {
57
57
  }
58
58
  async setContextValue(key, value) {
59
59
  if (key in contextIDNormalizers) {
60
- value = await contextIDNormalizers[key](this.apiClient, value);
60
+ value = await contextIDNormalizers[key](this.apiClient, value, this);
61
61
  }
62
62
  await this.persist({ [key]: value });
63
63
  return value;
64
64
  }
65
+ async mustGetContextValue(key) {
66
+ const value = await this.getContextValue(key);
67
+ if (!value) {
68
+ throw new InvalidContextError(`Context value for "${key}" is not set.`);
69
+ }
70
+ return value;
71
+ }
65
72
  async getContextValue(key) {
66
73
  const data = await this.contextData;
67
74
  if (key in data) {
@@ -73,8 +80,10 @@ export default class Context {
73
80
  setServerId = (id) => this.setContextValue("server-id", id);
74
81
  setOrgId = (id) => this.setContextValue("org-id", id);
75
82
  setAppInstallationId = (id) => this.setContextValue("installation-id", id);
83
+ setStackId = (id) => this.setContextValue("stack-id", id);
76
84
  projectId = () => this.getContextValue("project-id");
77
85
  serverId = () => this.getContextValue("server-id");
78
86
  orgId = () => this.getContextValue("org-id");
79
87
  appInstallationId = () => this.getContextValue("installation-id");
88
+ stackId = () => this.getContextValue("stack-id");
80
89
  }
@@ -1,7 +1,6 @@
1
- import { Arg, OptionFlag } from "@oclif/core/interfaces";
1
+ import { AlphabetLowercase, Arg, OptionFlag } from "@oclif/core/interfaces";
2
2
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
3
- import { AlphabetLowercase } from "@oclif/core/interfaces";
4
- import { ContextKey, ContextNames } from "./Context.js";
3
+ import Context, { ContextKey, ContextNames } from "./Context.js";
5
4
  import FlagSet from "./FlagSet.js";
6
5
  export type ContextFlags<N extends ContextNames, TID extends string = ContextKey<N>> = {
7
6
  [k in TID]: OptionFlag<string>;
@@ -33,7 +32,7 @@ export type FlagSetOptions = {
33
32
  display: string;
34
33
  };
35
34
  };
36
- export type NormalizeFn = (apiClient: MittwaldAPIV2Client, id: string) => string | Promise<string>;
35
+ export type NormalizeFn = (apiClient: MittwaldAPIV2Client, id: string, ctx: Context) => string | Promise<string>;
37
36
  export declare function makeMissingContextInputError<TName extends ContextNames>(commandType: {
38
37
  flags: {
39
38
  [k in ContextKey<TName>]: OptionFlag<string>;