@mittwald/cli 1.3.0 → 1.4.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.
package/README.md CHANGED
@@ -113,6 +113,7 @@ USAGE
113
113
  * [`mw database`](docs/database.md) - Manage databases (like MySQL and Redis) in your projects
114
114
  * [`mw ddev`](docs/ddev.md) - Integrate your mittwald projects with DDEV
115
115
  * [`mw domain`](docs/domain.md) - Manage domains, virtual hosts and DNS settings in your projects
116
+ * [`mw extension`](docs/extension.md) - Install and manage extensions in your organisations and projects
116
117
  * [`mw help`](docs/help.md) - Display help for mw.
117
118
  * [`mw login`](docs/login.md) - Manage your client authentication
118
119
  * [`mw mail`](docs/mail.md) - Manage mailboxes and mail addresses in your projects
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
3
+ type InstallResult = {
4
+ extensionInstanceId: string;
5
+ };
6
+ export default class Install extends ExecRenderBaseCommand<typeof Install, InstallResult> {
7
+ static description: string;
8
+ static flags: {
9
+ "org-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ consent: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ static args: {
15
+ "extension-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
16
+ };
17
+ protected exec(): Promise<InstallResult>;
18
+ protected render(executionResult: InstallResult): React.ReactNode;
19
+ }
20
+ export {};
@@ -0,0 +1,79 @@
1
+ import { 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, Flags } from "@oclif/core";
5
+ import { assertStatus } from "@mittwald/api-client";
6
+ import { Text } from "ink";
7
+ import { contextIDNormalizers } from "../../lib/context/Context.js";
8
+ export default class Install extends ExecRenderBaseCommand {
9
+ static description = "Install an extension in a project or organization";
10
+ static flags = {
11
+ ...processFlags,
12
+ "org-id": Flags.string({
13
+ description: "the ID of the organization to install the extension in",
14
+ exactlyOne: ["org-id", "project-id"],
15
+ }),
16
+ "project-id": Flags.string({
17
+ description: "the ID of the project to install the extension in",
18
+ exactlyOne: ["org-id", "project-id"],
19
+ }),
20
+ consent: Flags.boolean({
21
+ description: "consent to the extension having access to the requested scopes",
22
+ }),
23
+ };
24
+ static args = {
25
+ "extension-id": Args.string({
26
+ description: "the ID of the extension to install",
27
+ required: true,
28
+ }),
29
+ };
30
+ async exec() {
31
+ const { "extension-id": extensionId } = this.args;
32
+ const { consent } = this.flags;
33
+ let { "org-id": orgId, "project-id": projectId } = this.flags;
34
+ const p = makeProcessRenderer(this.flags, "Installing extension");
35
+ const ext = await p.runStep("Loading extension", async () => {
36
+ const response = await this.apiClient.marketplace.extensionGetExtension({
37
+ extensionId,
38
+ });
39
+ assertStatus(response, 200);
40
+ return response.data;
41
+ });
42
+ if (orgId !== undefined) {
43
+ const normalizer = contextIDNormalizers["org-id"];
44
+ orgId = await normalizer(this.apiClient, orgId);
45
+ }
46
+ if (projectId !== undefined) {
47
+ const normalizer = contextIDNormalizers["project-id"];
48
+ projectId = await normalizer(this.apiClient, projectId);
49
+ }
50
+ if (!consent) {
51
+ p.addInfo(_jsxs(Text, { children: ["This extension requires access to the following scopes:", " ", ext.scopes.join(", "), ". Please confirm your consent, or run the command with the --consent flag."] }));
52
+ const consentedInteractively = await p.addConfirmation("Consent to requested scopes?");
53
+ if (!consentedInteractively) {
54
+ throw new Error("Consent was not given; skipping extension installation");
55
+ }
56
+ }
57
+ const result = await p.runStep("installing extension", async () => {
58
+ const resp = await this.apiClient.marketplace.extensionCreateExtensionInstance({
59
+ data: {
60
+ extensionId,
61
+ context: orgId ? "customer" : "project",
62
+ contextId: (orgId ?? projectId),
63
+ consentedScopes: ext.scopes,
64
+ },
65
+ });
66
+ assertStatus(resp, 201);
67
+ return resp;
68
+ });
69
+ return {
70
+ extensionInstanceId: result.data.id,
71
+ };
72
+ }
73
+ render(executionResult) {
74
+ if (this.flags.quiet) {
75
+ return executionResult.extensionInstanceId;
76
+ }
77
+ return undefined;
78
+ }
79
+ }
@@ -0,0 +1,30 @@
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
+ import { SuccessfulResponse } from "../../lib/apiutil/SuccessfulResponse.js";
6
+ type Extension = MittwaldAPIV2.Components.Schemas.MarketplaceExtension;
7
+ type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2ExtensionInstances.Get.Responses.$200.Content.ApplicationJson[number]>;
8
+ type Response = Awaited<ReturnType<MittwaldAPIV2Client["marketplace"]["extensionListExtensionInstances"]>>;
9
+ type ExtendedResponseItem = ResponseItem & {
10
+ extension: Extension;
11
+ };
12
+ export declare class ListInstalled extends ListBaseCommand<typeof ListInstalled, ResponseItem, Response> {
13
+ static description: string;
14
+ static args: {};
15
+ static flags: {
16
+ "org-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
19
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
24
+ };
25
+ getData(): Promise<Response>;
26
+ protected mapData(data: SuccessfulResponse<Response, 200>["data"]): Promise<ExtendedResponseItem[]>;
27
+ protected getColumns(data: ExtendedResponseItem[]): ListColumns<ResponseItem>;
28
+ protected getColumnsExtended(data: ExtendedResponseItem[]): ListColumns<ExtendedResponseItem>;
29
+ }
30
+ export {};
@@ -0,0 +1,80 @@
1
+ import { assertStatus } from "@mittwald/api-client-commons";
2
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
3
+ import { Flags } from "@oclif/core";
4
+ import { contextIDNormalizers } from "../../lib/context/Context.js";
5
+ export class ListInstalled extends ListBaseCommand {
6
+ static description = "List installed extensions in an organization or project.";
7
+ static args = {};
8
+ static flags = {
9
+ ...ListBaseCommand.baseFlags,
10
+ "org-id": Flags.string({
11
+ description: "the ID of the organization to install the extension in",
12
+ exactlyOne: ["org-id", "project-id"],
13
+ }),
14
+ "project-id": Flags.string({
15
+ description: "the ID of the project to install the extension in",
16
+ exactlyOne: ["org-id", "project-id"],
17
+ }),
18
+ };
19
+ async getData() {
20
+ let { "org-id": orgId, "project-id": projectId } = this.flags;
21
+ if (orgId) {
22
+ const normalizer = contextIDNormalizers["org-id"];
23
+ orgId = await normalizer(this.apiClient, orgId);
24
+ }
25
+ if (projectId) {
26
+ const normalizer = contextIDNormalizers["project-id"];
27
+ projectId = await normalizer(this.apiClient, projectId);
28
+ }
29
+ return await this.apiClient.marketplace.extensionListExtensionInstances({
30
+ queryParameters: {
31
+ context: orgId ? "customer" : "project",
32
+ contextId: (orgId ?? projectId),
33
+ },
34
+ });
35
+ }
36
+ mapData(data) {
37
+ return Promise.all(data.map(async (item) => {
38
+ const resp = await this.apiClient.marketplace.extensionGetExtension({
39
+ extensionId: item.extensionId,
40
+ });
41
+ assertStatus(resp, 200);
42
+ const extension = resp.data;
43
+ return {
44
+ ...item,
45
+ extension,
46
+ };
47
+ }));
48
+ }
49
+ getColumns(data) {
50
+ return this.getColumnsExtended(data);
51
+ }
52
+ getColumnsExtended(data) {
53
+ const { id } = super.getColumns(data, {});
54
+ return {
55
+ id,
56
+ extension: {
57
+ header: "Extension",
58
+ get: (row) => row.extension.name,
59
+ },
60
+ state: {
61
+ header: "State",
62
+ get: (row) => {
63
+ // Temporary "as any" cast, because the API response is not typed correctly
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ if (row.pendingInstallation) {
66
+ return "installing";
67
+ }
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ if (row.pendingRemoval) {
70
+ return "removing";
71
+ }
72
+ if (row.disabled) {
73
+ return "disabled";
74
+ }
75
+ return "enabled";
76
+ },
77
+ },
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,21 @@
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 ResponseItem = Simplify<MittwaldAPIV2.Paths.V2Extensions.Get.Responses.$200.Content.ApplicationJson[number]>;
6
+ type Response = Awaited<ReturnType<MittwaldAPIV2Client["marketplace"]["extensionListExtensions"]>>;
7
+ export declare class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
8
+ static description: string;
9
+ static args: {};
10
+ static flags: {
11
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
12
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
17
+ };
18
+ getData(): Promise<Response>;
19
+ protected getColumns(data: ResponseItem[]): ListColumns<ResponseItem>;
20
+ }
21
+ export {};
@@ -0,0 +1,22 @@
1
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
2
+ export class List extends ListBaseCommand {
3
+ static description = "Get all available extensions.";
4
+ static args = {};
5
+ static flags = {
6
+ ...ListBaseCommand.baseFlags,
7
+ };
8
+ async getData() {
9
+ return await this.apiClient.marketplace.extensionListExtensions();
10
+ }
11
+ getColumns(data) {
12
+ const { id } = super.getColumns(data, {});
13
+ return {
14
+ id,
15
+ name: {},
16
+ context: {},
17
+ subTitle: {
18
+ get: (ext) => ext.subTitle?.en ?? ext.subTitle?.de,
19
+ },
20
+ };
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
3
+ export default class Uninstall extends ExecRenderBaseCommand<typeof Uninstall, void> {
4
+ static description: string;
5
+ static flags: {
6
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ static args: {
9
+ "extension-instance-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
10
+ };
11
+ protected exec(): Promise<void>;
12
+ protected render(): React.ReactNode;
13
+ }
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } 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 assertSuccess from "../../lib/apiutil/assert_success.js";
6
+ import { Success } from "../../rendering/react/components/Success.js";
7
+ export default class Uninstall extends ExecRenderBaseCommand {
8
+ static description = "Remove an extension from an organization";
9
+ static flags = {
10
+ ...processFlags,
11
+ };
12
+ static args = {
13
+ "extension-instance-id": Args.string({
14
+ description: "the ID of the extension instance to uninstall",
15
+ required: true,
16
+ }),
17
+ };
18
+ async exec() {
19
+ const { "extension-instance-id": extensionInstanceId } = this.args;
20
+ const p = makeProcessRenderer(this.flags, "Uninstalling extension");
21
+ await p.runStep("removing extension instance", async () => {
22
+ const resp = await this.apiClient.marketplace.extensionDeleteExtensionInstance({
23
+ extensionInstanceId,
24
+ });
25
+ assertSuccess(resp);
26
+ });
27
+ await p.complete(_jsx(Success, { children: "Extension successfully uninstalled" }));
28
+ }
29
+ render() {
30
+ return undefined;
31
+ }
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -159,6 +159,9 @@
159
159
  "domain": {
160
160
  "description": "Manage domains, virtual hosts and DNS settings in your projects"
161
161
  },
162
+ "extension": {
163
+ "description": "Install and manage extensions in your organisations and projects"
164
+ },
162
165
  "login": {
163
166
  "description": "Manage your client authentication"
164
167
  },