@mittwald/cli 1.2.6 → 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 +1 -0
- package/dist/commands/database/list.d.ts +30 -0
- package/dist/commands/database/list.js +68 -0
- package/dist/commands/database/mysql/dump.d.ts +1 -0
- package/dist/commands/database/mysql/dump.js +10 -1
- package/dist/commands/database/mysql/import.d.ts +1 -0
- package/dist/commands/database/mysql/list.d.ts +1 -1
- package/dist/commands/database/mysql/port-forward.js +1 -1
- package/dist/commands/database/mysql/shell.d.ts +1 -0
- package/dist/commands/extension/install.d.ts +20 -0
- package/dist/commands/extension/install.js +79 -0
- package/dist/commands/extension/list-installed.d.ts +30 -0
- package/dist/commands/extension/list-installed.js +80 -0
- package/dist/commands/extension/list.d.ts +21 -0
- package/dist/commands/extension/list.js +22 -0
- package/dist/commands/extension/uninstall.d.ts +13 -0
- package/dist/commands/extension/uninstall.js +32 -0
- package/dist/lib/basecommands/ListBaseCommand.d.ts +1 -1
- package/dist/lib/basecommands/ListBaseCommand.js +8 -2
- package/dist/lib/resources/database/mysql/connect.d.ts +3 -1
- package/dist/lib/resources/database/mysql/connect.js +4 -2
- package/dist/lib/resources/database/mysql/flags.d.ts +2 -0
- package/dist/lib/resources/database/mysql/flags.js +4 -0
- package/dist/lib/resources/database/mysql/temp_user.js +1 -1
- package/package.json +4 -1
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,30 @@
|
|
|
1
|
+
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
|
+
import { ListColumns } from "../../rendering/formatter/Table.js";
|
|
3
|
+
import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
|
|
4
|
+
type ResponseItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: "mysql" | "redis";
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
description: string;
|
|
10
|
+
hostname: string;
|
|
11
|
+
isReady: boolean;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
};
|
|
14
|
+
type Response = Awaited<ReturnType<MittwaldAPIV2Client["database"]["listMysqlDatabases"]>>;
|
|
15
|
+
export declare class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
|
|
16
|
+
static description: string;
|
|
17
|
+
static args: {};
|
|
18
|
+
static flags: {
|
|
19
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
20
|
+
output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
|
|
21
|
+
extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
+
"no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
"no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
+
"no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
|
+
"csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
|
|
26
|
+
};
|
|
27
|
+
getData(): Promise<ResponseItem[]>;
|
|
28
|
+
protected getColumns(ignoredData: ResponseItem[]): ListColumns<ResponseItem>;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { assertStatus } from "@mittwald/api-client-commons";
|
|
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 all kinds of databases belonging to a project.";
|
|
6
|
+
static args = {};
|
|
7
|
+
static flags = {
|
|
8
|
+
...ListBaseCommand.baseFlags,
|
|
9
|
+
...projectFlags,
|
|
10
|
+
};
|
|
11
|
+
async getData() {
|
|
12
|
+
const projectId = await this.withProjectId(List);
|
|
13
|
+
const databases = [];
|
|
14
|
+
const mysqlResponse = await this.apiClient.database.listMysqlDatabases({
|
|
15
|
+
projectId,
|
|
16
|
+
});
|
|
17
|
+
assertStatus(mysqlResponse, 200);
|
|
18
|
+
const redisResponse = await this.apiClient.database.listRedisDatabases({
|
|
19
|
+
projectId,
|
|
20
|
+
});
|
|
21
|
+
assertStatus(redisResponse, 200);
|
|
22
|
+
databases.push(...mysqlResponse.data.map((d) => ({ ...d, kind: "mysql" })), ...redisResponse.data.map((d) => ({
|
|
23
|
+
...d,
|
|
24
|
+
kind: "redis",
|
|
25
|
+
isReady: true,
|
|
26
|
+
})));
|
|
27
|
+
return databases;
|
|
28
|
+
}
|
|
29
|
+
getColumns(ignoredData) {
|
|
30
|
+
const { id, name, createdAt } = super.getColumns(ignoredData, {
|
|
31
|
+
shortIdKey: "name",
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
name,
|
|
36
|
+
version: {
|
|
37
|
+
header: "Version",
|
|
38
|
+
get(row) {
|
|
39
|
+
if (row.kind === "mysql") {
|
|
40
|
+
return `MySQL ${row.version}`;
|
|
41
|
+
}
|
|
42
|
+
else if (row.kind === "redis") {
|
|
43
|
+
return `Redis ${row.version}`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return "Unknown";
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
description: {
|
|
51
|
+
header: "Description",
|
|
52
|
+
},
|
|
53
|
+
hostname: {
|
|
54
|
+
header: "Hostname",
|
|
55
|
+
},
|
|
56
|
+
status: {
|
|
57
|
+
header: "Status",
|
|
58
|
+
get: (row) => {
|
|
59
|
+
if (!row.isReady) {
|
|
60
|
+
return "pending";
|
|
61
|
+
}
|
|
62
|
+
return "ready";
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
createdAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -10,6 +10,7 @@ export declare class Dump extends ExecRenderBaseCommand<typeof Dump, Record<stri
|
|
|
10
10
|
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
"temporary-user": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
"mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
"mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
};
|
|
15
16
|
static args: {
|
|
@@ -68,5 +68,14 @@ function DumpSuccess({ database, output, }) {
|
|
|
68
68
|
return (_jsxs(Success, { children: ["Dump of MySQL database ", _jsx(Value, { children: database }), " written to", " ", _jsx(Value, { children: output })] }));
|
|
69
69
|
}
|
|
70
70
|
function buildMySqlDumpArgs(d) {
|
|
71
|
-
return [
|
|
71
|
+
return [
|
|
72
|
+
"-h",
|
|
73
|
+
d.hostname,
|
|
74
|
+
"-u",
|
|
75
|
+
d.user,
|
|
76
|
+
`-p${d.password}`,
|
|
77
|
+
"--default-character-set",
|
|
78
|
+
d.charset,
|
|
79
|
+
d.database,
|
|
80
|
+
];
|
|
72
81
|
}
|
|
@@ -10,6 +10,7 @@ export declare class Import extends ExecRenderBaseCommand<typeof Import, Record<
|
|
|
10
10
|
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
"temporary-user": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
"mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
"mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
};
|
|
15
16
|
static args: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Simplify } from "@mittwald/api-client-commons";
|
|
2
2
|
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
3
|
-
import { ListColumns } from "../../../rendering/formatter/
|
|
3
|
+
import { ListColumns } from "../../../rendering/formatter/Table.js";
|
|
4
4
|
import { ListBaseCommand } from "../../../lib/basecommands/ListBaseCommand.js";
|
|
5
5
|
type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2ProjectsProjectIdMysqlDatabases.Get.Responses.$200.Content.ApplicationJson[number]>;
|
|
6
6
|
type Response = Awaited<ReturnType<MittwaldAPIV2Client["database"]["listMysqlDatabases"]>>;
|
|
@@ -26,7 +26,7 @@ export class PortForward extends ExecRenderBaseCommand {
|
|
|
26
26
|
async exec() {
|
|
27
27
|
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
|
|
28
28
|
const p = makeProcessRenderer(this.flags, "Port-forwarding a MySQL database");
|
|
29
|
-
const { sshUser, sshHost, hostname, database } = await getConnectionDetails(this.apiClient, databaseId, this.flags["ssh-user"], p);
|
|
29
|
+
const { sshUser, sshHost, hostname, database } = await getConnectionDetails(this.apiClient, databaseId, this.flags["ssh-user"], undefined, p);
|
|
30
30
|
const { port } = this.flags;
|
|
31
31
|
p.complete(_jsxs(Text, { children: ["Forwarding MySQL database ", _jsx(Value, { children: database }), " to local port", " ", _jsx(Value, { children: port }), ". Use CTRL+C to cancel."] }));
|
|
32
32
|
const sshArgs = buildSSHClientFlags(sshUser, sshHost, this.flags, {
|
|
@@ -5,6 +5,7 @@ export declare class Shell extends ExecRenderBaseCommand<typeof Shell, Record<st
|
|
|
5
5
|
static description: string;
|
|
6
6
|
static flags: {
|
|
7
7
|
"mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
"mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
"ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -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
|
+
}
|
|
@@ -24,7 +24,7 @@ export declare abstract class ListBaseCommand<T extends typeof BaseCommand, TIte
|
|
|
24
24
|
protected sorter?: SorterFunction<TItem>;
|
|
25
25
|
init(): Promise<void>;
|
|
26
26
|
run(): Promise<void>;
|
|
27
|
-
protected abstract getData(): Promise<TAPIResponse>;
|
|
27
|
+
protected abstract getData(): Promise<TAPIResponse | TItem[]>;
|
|
28
28
|
protected mapData(data: SuccessfulResponse<TAPIResponse, 200>["data"]): TItem[] | Promise<TItem[]>;
|
|
29
29
|
protected getColumns(data: TItem[], opts?: ColumnOpts<TItem>): ListColumns<TItem>;
|
|
30
30
|
}
|
|
@@ -21,8 +21,14 @@ export class ListBaseCommand extends ExtendedBaseCommand {
|
|
|
21
21
|
}
|
|
22
22
|
async run() {
|
|
23
23
|
const response = await this.getData();
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
let data;
|
|
25
|
+
if (!Array.isArray(response)) {
|
|
26
|
+
assertStatus(response, 200);
|
|
27
|
+
data = await this.mapData(response.data);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
data = response;
|
|
31
|
+
}
|
|
26
32
|
this.formatter.log(data, this.getColumns(data), this.flags);
|
|
27
33
|
}
|
|
28
34
|
mapData(data) {
|
|
@@ -4,6 +4,7 @@ import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
|
4
4
|
type Project = MittwaldAPIV2.Components.Schemas.ProjectProject;
|
|
5
5
|
export interface MySQLConnectionFlags {
|
|
6
6
|
"mysql-password": string | undefined;
|
|
7
|
+
"mysql-charset": string | undefined;
|
|
7
8
|
"temporary-user"?: boolean;
|
|
8
9
|
"ssh-user"?: string;
|
|
9
10
|
}
|
|
@@ -14,6 +15,7 @@ export interface MySQLConnectionDetails {
|
|
|
14
15
|
sshHost: string;
|
|
15
16
|
sshUser: string;
|
|
16
17
|
project: Project;
|
|
18
|
+
charset: string;
|
|
17
19
|
}
|
|
18
20
|
export type MySQLConnectionDetailsWithPassword = MySQLConnectionDetails & {
|
|
19
21
|
password: string;
|
|
@@ -27,5 +29,5 @@ export type MySQLConnectionDetailsWithPassword = MySQLConnectionDetails & {
|
|
|
27
29
|
*/
|
|
28
30
|
export declare function runWithConnectionDetails<TRes>(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: MySQLConnectionFlags, cb: (connectionDetails: MySQLConnectionDetailsWithPassword) => Promise<TRes>): Promise<TRes>;
|
|
29
31
|
export declare function getConnectionDetailsWithPassword(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: MySQLConnectionFlags): Promise<MySQLConnectionDetailsWithPassword>;
|
|
30
|
-
export declare function getConnectionDetails(apiClient: MittwaldAPIV2Client, databaseId: string, sshUser: string | undefined, p: ProcessRenderer): Promise<MySQLConnectionDetails>;
|
|
32
|
+
export declare function getConnectionDetails(apiClient: MittwaldAPIV2Client, databaseId: string, sshUser: string | undefined, characterSet: string | undefined, p: ProcessRenderer): Promise<MySQLConnectionDetails>;
|
|
31
33
|
export {};
|
|
@@ -19,12 +19,13 @@ export async function runWithConnectionDetails(apiClient, databaseId, p, flags,
|
|
|
19
19
|
export async function getConnectionDetailsWithPassword(apiClient, databaseId, p, flags) {
|
|
20
20
|
const password = flags["temporary-user"] ? "" : await getPassword(p, flags);
|
|
21
21
|
const sshUser = flags["ssh-user"];
|
|
22
|
+
const characterSet = flags["mysql-charset"];
|
|
22
23
|
return {
|
|
23
|
-
...(await getConnectionDetails(apiClient, databaseId, sshUser, p)),
|
|
24
|
+
...(await getConnectionDetails(apiClient, databaseId, sshUser, characterSet, p)),
|
|
24
25
|
password,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
export async function getConnectionDetails(apiClient, databaseId, sshUser, p) {
|
|
28
|
+
export async function getConnectionDetails(apiClient, databaseId, sshUser, characterSet, p) {
|
|
28
29
|
const database = await getDatabase(apiClient, p, databaseId);
|
|
29
30
|
const databaseUser = await getDatabaseUser(apiClient, p, databaseId);
|
|
30
31
|
const project = await getProject(apiClient, p, database);
|
|
@@ -35,6 +36,7 @@ export async function getConnectionDetails(apiClient, databaseId, sshUser, p) {
|
|
|
35
36
|
user: databaseUser.name,
|
|
36
37
|
sshHost: sshConnectionData.host,
|
|
37
38
|
sshUser: sshConnectionData.user,
|
|
39
|
+
charset: characterSet ?? database.characterSettings.characterSet,
|
|
38
40
|
project,
|
|
39
41
|
};
|
|
40
42
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
export declare const mysqlConnectionFlags: {
|
|
3
3
|
"mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
4
|
+
"mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
4
5
|
};
|
|
5
6
|
export declare const mysqlConnectionFlagsWithTempUser: {
|
|
6
7
|
"temporary-user": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
"mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
"mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
};
|
|
9
11
|
export declare const mysqlArgs: {
|
|
10
12
|
"database-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
@@ -12,6 +12,10 @@ NOTE: This is a security risk, as the password will be visible in the process li
|
|
|
12
12
|
required: false,
|
|
13
13
|
env: "MYSQL_PWD",
|
|
14
14
|
}),
|
|
15
|
+
"mysql-charset": Flags.string({
|
|
16
|
+
summary: "the character set to use for the MySQL connection",
|
|
17
|
+
description: "The character set that should be used for the MySQL connection. If omitted, the database's default character set will be used (for newer databases, this should be utf8mb4 in most cases, but really might be anything).",
|
|
18
|
+
}),
|
|
15
19
|
};
|
|
16
20
|
export const mysqlConnectionFlagsWithTempUser = {
|
|
17
21
|
...mysqlConnectionFlags,
|
|
@@ -36,7 +36,7 @@ export function generateRandomPassword(length = 32) {
|
|
|
36
36
|
const lowercase = "abcdefghijklmnopqrstuvwxyz";
|
|
37
37
|
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
38
38
|
const digits = "0123456789";
|
|
39
|
-
const specialChars = "
|
|
39
|
+
const specialChars = "_";
|
|
40
40
|
const allChars = lowercase + uppercase + digits + specialChars;
|
|
41
41
|
// Ensure the password includes at least one of each required character type
|
|
42
42
|
const passwordArray = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/cli",
|
|
3
|
-
"version": "1.
|
|
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
|
},
|