@mittwald/cli 1.0.0-alpha.37 → 1.0.0-alpha.39
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 +42 -18
- package/dist/BaseCommand.js +4 -0
- package/dist/commands/database/mysql/create.d.ts +1 -0
- package/dist/commands/database/mysql/create.js +34 -29
- package/dist/commands/database/mysql/create.test.d.ts +1 -0
- package/dist/commands/database/mysql/create.test.js +100 -0
- package/dist/commands/database/mysql/delete.js +1 -1
- package/dist/commands/database/mysql/dump.js +1 -1
- package/dist/commands/database/mysql/get.js +1 -1
- package/dist/commands/database/mysql/phpmyadmin.js +1 -1
- package/dist/commands/database/mysql/port-forward.js +1 -1
- package/dist/commands/database/mysql/shell.js +1 -1
- package/dist/commands/ddev/init.d.ts +1 -0
- package/dist/commands/ddev/init.js +22 -4
- package/dist/commands/project/create.js +0 -2
- package/dist/commands/user/ssh-key/create.d.ts +2 -1
- package/dist/commands/user/ssh-key/create.js +27 -35
- package/dist/commands/user/ssh-key/import.d.ts +11 -0
- package/dist/commands/user/ssh-key/import.js +56 -0
- package/dist/lib/api_consistency.d.ts +2 -0
- package/dist/lib/api_consistency.js +21 -0
- package/dist/lib/api_retry.d.ts +2 -0
- package/dist/lib/api_retry.js +29 -0
- package/dist/lib/app/Installer.js +2 -2
- package/dist/lib/app/install.d.ts +1 -1
- package/dist/lib/app/install.js +3 -4
- package/dist/lib/app/wait.d.ts +1 -1
- package/dist/lib/app/wait.js +1 -3
- package/dist/lib/database/mysql/flags.d.ts +1 -2
- package/dist/lib/database/mysql/flags.js +7 -17
- package/dist/lib/ddev/config.d.ts +12 -0
- package/dist/lib/ddev/config_builder.d.ts +1 -0
- package/dist/lib/ddev/config_builder.js +21 -1
- package/dist/rendering/process/components/ProcessStateIcon.js +2 -2
- package/dist/rendering/react/hooks/useIncreaseInkStdoutColumns.js +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -191,7 +191,7 @@ USAGE
|
|
|
191
191
|
* [`mw domain virtualhost delete VIRTUAL-HOST-ID`](#mw-domain-virtualhost-delete-virtual-host-id)
|
|
192
192
|
* [`mw domain virtualhost get INGRESS-ID`](#mw-domain-virtualhost-get-ingress-id)
|
|
193
193
|
* [`mw domain virtualhost list`](#mw-domain-virtualhost-list)
|
|
194
|
-
* [`mw help [
|
|
194
|
+
* [`mw help [COMMAND]`](#mw-help-command)
|
|
195
195
|
* [`mw login reset`](#mw-login-reset)
|
|
196
196
|
* [`mw login status`](#mw-login-status)
|
|
197
197
|
* [`mw login token`](#mw-login-token)
|
|
@@ -254,6 +254,7 @@ USAGE
|
|
|
254
254
|
* [`mw user ssh-key create`](#mw-user-ssh-key-create)
|
|
255
255
|
* [`mw user ssh-key delete ID`](#mw-user-ssh-key-delete-id)
|
|
256
256
|
* [`mw user ssh-key get KEY-ID`](#mw-user-ssh-key-get-key-id)
|
|
257
|
+
* [`mw user ssh-key import`](#mw-user-ssh-key-import)
|
|
257
258
|
* [`mw user ssh-key list`](#mw-user-ssh-key-list)
|
|
258
259
|
|
|
259
260
|
## `mw app copy [INSTALLATION-ID]`
|
|
@@ -2280,7 +2281,7 @@ USAGE
|
|
|
2280
2281
|
$ mw database mysql delete DATABASE-ID [-q] [-f]
|
|
2281
2282
|
|
|
2282
2283
|
ARGUMENTS
|
|
2283
|
-
DATABASE-ID The ID of the database
|
|
2284
|
+
DATABASE-ID The ID or name of the database
|
|
2284
2285
|
|
|
2285
2286
|
FLAGS
|
|
2286
2287
|
-f, --force Do not ask for confirmation
|
|
@@ -2305,7 +2306,7 @@ USAGE
|
|
|
2305
2306
|
$ mw database mysql dump DATABASE-ID -o <value> [-q] [-p <value>] [--ssh-user <value>] [--temporary-user] [--gzip]
|
|
2306
2307
|
|
|
2307
2308
|
ARGUMENTS
|
|
2308
|
-
DATABASE-ID The ID of the database
|
|
2309
|
+
DATABASE-ID The ID or name of the database
|
|
2309
2310
|
|
|
2310
2311
|
FLAGS
|
|
2311
2312
|
-o, --output=<value> (required) the output file to write the dump to ("-" for stdout)
|
|
@@ -2365,7 +2366,7 @@ USAGE
|
|
|
2365
2366
|
$ mw database mysql get DATABASE-ID [-o json|yaml | | ]
|
|
2366
2367
|
|
|
2367
2368
|
ARGUMENTS
|
|
2368
|
-
DATABASE-ID The ID of the database
|
|
2369
|
+
DATABASE-ID The ID or name of the database
|
|
2369
2370
|
|
|
2370
2371
|
FLAGS
|
|
2371
2372
|
-o, --output=<option> output in a more machine friendly format
|
|
@@ -2415,7 +2416,7 @@ USAGE
|
|
|
2415
2416
|
$ mw database mysql phpmyadmin DATABASE-ID
|
|
2416
2417
|
|
|
2417
2418
|
ARGUMENTS
|
|
2418
|
-
DATABASE-ID The ID of the database
|
|
2419
|
+
DATABASE-ID The ID or name of the database
|
|
2419
2420
|
```
|
|
2420
2421
|
|
|
2421
2422
|
## `mw database mysql port-forward DATABASE-ID`
|
|
@@ -2427,7 +2428,7 @@ USAGE
|
|
|
2427
2428
|
$ mw database mysql port-forward DATABASE-ID [-q] [--ssh-user <value>] [--port <value>]
|
|
2428
2429
|
|
|
2429
2430
|
ARGUMENTS
|
|
2430
|
-
DATABASE-ID The ID of the database
|
|
2431
|
+
DATABASE-ID The ID or name of the database
|
|
2431
2432
|
|
|
2432
2433
|
FLAGS
|
|
2433
2434
|
-q, --quiet suppress process output and only display a machine-readable summary.
|
|
@@ -2457,7 +2458,7 @@ USAGE
|
|
|
2457
2458
|
$ mw database mysql shell DATABASE-ID [-q] [-p <value>]
|
|
2458
2459
|
|
|
2459
2460
|
ARGUMENTS
|
|
2460
|
-
DATABASE-ID The ID of the database
|
|
2461
|
+
DATABASE-ID The ID or name of the database
|
|
2461
2462
|
|
|
2462
2463
|
FLAGS
|
|
2463
2464
|
-p, --mysql-password=<value> the password to use for the MySQL user (env: MYSQL_PWD)
|
|
@@ -3105,16 +3106,16 @@ FLAG DESCRIPTIONS
|
|
|
3105
3106
|
to persistently set a default project for all commands that accept this flag.
|
|
3106
3107
|
```
|
|
3107
3108
|
|
|
3108
|
-
## `mw help [
|
|
3109
|
+
## `mw help [COMMAND]`
|
|
3109
3110
|
|
|
3110
3111
|
Display help for mw.
|
|
3111
3112
|
|
|
3112
3113
|
```
|
|
3113
3114
|
USAGE
|
|
3114
|
-
$ mw help [
|
|
3115
|
+
$ mw help [COMMAND] [-n]
|
|
3115
3116
|
|
|
3116
3117
|
ARGUMENTS
|
|
3117
|
-
|
|
3118
|
+
COMMAND Command to show help for.
|
|
3118
3119
|
|
|
3119
3120
|
FLAGS
|
|
3120
3121
|
-n, --nested-commands Include all nested commands in the output.
|
|
@@ -3123,7 +3124,7 @@ DESCRIPTION
|
|
|
3123
3124
|
Display help for mw.
|
|
3124
3125
|
```
|
|
3125
3126
|
|
|
3126
|
-
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.
|
|
3127
|
+
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.16/src/commands/help.ts)_
|
|
3127
3128
|
|
|
3128
3129
|
## `mw login reset`
|
|
3129
3130
|
|
|
@@ -4517,7 +4518,7 @@ EXAMPLES
|
|
|
4517
4518
|
$ mw update --available
|
|
4518
4519
|
```
|
|
4519
4520
|
|
|
4520
|
-
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.1.
|
|
4521
|
+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.1.16/src/commands/update.ts)_
|
|
4521
4522
|
|
|
4522
4523
|
## `mw user api-token create`
|
|
4523
4524
|
|
|
@@ -4679,14 +4680,14 @@ Create and import a new SSH key
|
|
|
4679
4680
|
|
|
4680
4681
|
```
|
|
4681
4682
|
USAGE
|
|
4682
|
-
$ mw user ssh-key create [-q] [--output <value>] [--no-passphrase] [--comment <value>]
|
|
4683
|
+
$ mw user ssh-key create [-q] [--expires <value>] [--output <value>] [--no-passphrase] [--comment <value>]
|
|
4683
4684
|
|
|
4684
4685
|
FLAGS
|
|
4685
|
-
-q, --quiet
|
|
4686
|
-
--comment=<value>
|
|
4687
|
-
--
|
|
4688
|
-
--no-passphrase
|
|
4689
|
-
--output=<value>
|
|
4686
|
+
-q, --quiet suppress process output and only display a machine-readable summary.
|
|
4687
|
+
--comment=<value> A comment for the SSH key.
|
|
4688
|
+
--expires=<value> An interval after which the SSH key expires (examples: 30m, 30d, 1y).
|
|
4689
|
+
--no-passphrase Use this flag to not set a passphrase for the SSH key.
|
|
4690
|
+
--output=<value> [default: mstudio-cli] A filename in your ~/.ssh directory to write the SSH key to.
|
|
4690
4691
|
|
|
4691
4692
|
DESCRIPTION
|
|
4692
4693
|
Create and import a new SSH key
|
|
@@ -4742,6 +4743,29 @@ DESCRIPTION
|
|
|
4742
4743
|
Get a specific SSH key
|
|
4743
4744
|
```
|
|
4744
4745
|
|
|
4746
|
+
## `mw user ssh-key import`
|
|
4747
|
+
|
|
4748
|
+
Import an existing (local) SSH key
|
|
4749
|
+
|
|
4750
|
+
```
|
|
4751
|
+
USAGE
|
|
4752
|
+
$ mw user ssh-key import [-q] [--expires <value>] [--input <value>]
|
|
4753
|
+
|
|
4754
|
+
FLAGS
|
|
4755
|
+
-q, --quiet suppress process output and only display a machine-readable summary.
|
|
4756
|
+
--expires=<value> An interval after which the SSH key expires (examples: 30m, 30d, 1y).
|
|
4757
|
+
--input=<value> [default: id_rsa.pub] A filename in your ~/.ssh directory containing the key to import.
|
|
4758
|
+
|
|
4759
|
+
DESCRIPTION
|
|
4760
|
+
Import an existing (local) SSH key
|
|
4761
|
+
|
|
4762
|
+
FLAG DESCRIPTIONS
|
|
4763
|
+
-q, --quiet suppress process output and only display a machine-readable summary.
|
|
4764
|
+
|
|
4765
|
+
This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
|
|
4766
|
+
scripts), you can use this flag to easily get the IDs of created resources for further processing.
|
|
4767
|
+
```
|
|
4768
|
+
|
|
4745
4769
|
## `mw user ssh-key list`
|
|
4746
4770
|
|
|
4747
4771
|
Get your stored ssh keys
|
package/dist/BaseCommand.js
CHANGED
|
@@ -2,6 +2,8 @@ import { Command } from "@oclif/core";
|
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
5
|
+
import { configureAxiosRetry } from "./lib/api_retry.js";
|
|
6
|
+
import { configureConsistencyHandling } from "./lib/api_consistency.js";
|
|
5
7
|
export class BaseCommand extends Command {
|
|
6
8
|
authenticationRequired = true;
|
|
7
9
|
apiClient = MittwaldAPIV2Client.newUnauthenticated();
|
|
@@ -15,6 +17,8 @@ export class BaseCommand extends Command {
|
|
|
15
17
|
this.apiClient = MittwaldAPIV2Client.newWithToken(token);
|
|
16
18
|
this.apiClient.axios.defaults.headers["User-Agent"] =
|
|
17
19
|
`mittwald-cli/${this.config.version}`;
|
|
20
|
+
configureAxiosRetry(this.apiClient.axios);
|
|
21
|
+
configureConsistencyHandling(this.apiClient.axios);
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
getTokenFilename() {
|
|
@@ -18,6 +18,7 @@ export declare class Create extends ExecRenderBaseCommand<typeof Create, Result>
|
|
|
18
18
|
"project-id": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string>;
|
|
19
19
|
};
|
|
20
20
|
protected exec(): Promise<Result>;
|
|
21
|
+
private createMySQLDatabase;
|
|
21
22
|
private getPassword;
|
|
22
23
|
protected render({ databaseId }: Result): ReactNode;
|
|
23
24
|
}
|
|
@@ -49,46 +49,48 @@ export class Create extends ExecRenderBaseCommand {
|
|
|
49
49
|
const projectId = await this.withProjectId(Create);
|
|
50
50
|
const { description, version, collation, "character-set": characterSet, "user-external": externalAccess, "user-access-level": accessLevel, } = this.flags;
|
|
51
51
|
const password = await this.getPassword(p);
|
|
52
|
-
const db = await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
user: {
|
|
66
|
-
password,
|
|
67
|
-
externalAccess,
|
|
68
|
-
accessLevel: accessLevel,
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
assertStatus(r, 201);
|
|
73
|
-
return r.data;
|
|
52
|
+
const db = await this.createMySQLDatabase(p, projectId, {
|
|
53
|
+
description,
|
|
54
|
+
version,
|
|
55
|
+
characterSettings: {
|
|
56
|
+
collation,
|
|
57
|
+
characterSet,
|
|
58
|
+
},
|
|
59
|
+
}, {
|
|
60
|
+
password,
|
|
61
|
+
externalAccess,
|
|
62
|
+
accessLevel: accessLevel,
|
|
74
63
|
});
|
|
75
64
|
const database = await p.runStep("fetching database", async () => {
|
|
76
|
-
const
|
|
65
|
+
const response = await this.apiClient.database.getMysqlDatabase({
|
|
77
66
|
mysqlDatabaseId: db.id,
|
|
78
67
|
});
|
|
79
|
-
assertStatus(
|
|
80
|
-
return
|
|
68
|
+
assertStatus(response, 200);
|
|
69
|
+
return response.data;
|
|
81
70
|
});
|
|
82
71
|
const user = await p.runStep("fetching user", async () => {
|
|
83
|
-
const
|
|
72
|
+
const response = await this.apiClient.database.getMysqlUser({
|
|
84
73
|
mysqlUserId: db.userId,
|
|
85
74
|
});
|
|
86
|
-
assertStatus(
|
|
87
|
-
return
|
|
75
|
+
assertStatus(response, 200);
|
|
76
|
+
return response.data;
|
|
88
77
|
});
|
|
89
|
-
p.complete(
|
|
78
|
+
await p.complete(_jsx(DatabaseCreateSuccess, { database: database, user: user }));
|
|
90
79
|
return { databaseId: db.id, userId: db.userId };
|
|
91
80
|
}
|
|
81
|
+
async createMySQLDatabase(p, projectId, database, user) {
|
|
82
|
+
return await p.runStep("creating MySQL database", async () => {
|
|
83
|
+
const r = await this.apiClient.database.createMysqlDatabase({
|
|
84
|
+
projectId,
|
|
85
|
+
data: {
|
|
86
|
+
database: { projectId, ...database },
|
|
87
|
+
user,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
assertStatus(r, 201);
|
|
91
|
+
return r.data;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
92
94
|
async getPassword(p) {
|
|
93
95
|
if (this.flags["user-password"]) {
|
|
94
96
|
return this.flags["user-password"];
|
|
@@ -101,3 +103,6 @@ export class Create extends ExecRenderBaseCommand {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
}
|
|
106
|
+
function DatabaseCreateSuccess({ database, user, }) {
|
|
107
|
+
return (_jsxs(Success, { children: ["The database ", _jsx(Value, { children: database.name }), " and the user", " ", _jsx(Value, { children: user.name }), " were successfully created."] }));
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test } from "@oclif/test";
|
|
2
|
+
describe("database:mysql:create", () => {
|
|
3
|
+
const projectId = "339d6458-839f-4809-a03d-78700069690c";
|
|
4
|
+
const databaseId = "83e0cb85-dcf7-4968-8646-87a63980ae91";
|
|
5
|
+
const userId = "a8c1eb2a-aa4d-4daf-8e21-9d91d56559ca";
|
|
6
|
+
const password = "secret";
|
|
7
|
+
const description = "Test";
|
|
8
|
+
const createFlags = [
|
|
9
|
+
"database mysql create",
|
|
10
|
+
"--project-id",
|
|
11
|
+
projectId,
|
|
12
|
+
"--version",
|
|
13
|
+
"8.0",
|
|
14
|
+
"--description",
|
|
15
|
+
description,
|
|
16
|
+
"--user-password",
|
|
17
|
+
password,
|
|
18
|
+
];
|
|
19
|
+
test
|
|
20
|
+
.nock("https://api.mittwald.de", (api) => {
|
|
21
|
+
api.get(`/v2/projects/${projectId}`).reply(200, {
|
|
22
|
+
id: projectId,
|
|
23
|
+
});
|
|
24
|
+
api
|
|
25
|
+
.post(`/v2/projects/${projectId}/mysql-databases`, {
|
|
26
|
+
database: {
|
|
27
|
+
projectId,
|
|
28
|
+
description,
|
|
29
|
+
version: "8.0",
|
|
30
|
+
characterSettings: {
|
|
31
|
+
collation: "utf8mb4_unicode_ci",
|
|
32
|
+
characterSet: "utf8mb4",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
user: {
|
|
36
|
+
password,
|
|
37
|
+
externalAccess: false,
|
|
38
|
+
accessLevel: "full",
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
.reply(201, { id: databaseId, userId });
|
|
42
|
+
api.get(`/v2/mysql-databases/${databaseId}`).reply(200, {
|
|
43
|
+
id: databaseId,
|
|
44
|
+
name: "mysql_xxxxxx",
|
|
45
|
+
});
|
|
46
|
+
api.get(`/v2/mysql-users/${userId}`).reply(200, {
|
|
47
|
+
id: userId,
|
|
48
|
+
name: "dbu_xxxxxx",
|
|
49
|
+
});
|
|
50
|
+
})
|
|
51
|
+
.env({ MITTWALD_API_TOKEN: "foo" })
|
|
52
|
+
.stdout()
|
|
53
|
+
.command(createFlags)
|
|
54
|
+
.it("creates a database and prints database and user name", (ctx) => {
|
|
55
|
+
// cannot match on exact output, because linebreaks
|
|
56
|
+
expect(ctx.stdout).to.contain("The database mysql_xxxxxx");
|
|
57
|
+
expect(ctx.stdout).to.contain("the user dbu_xxxxxx");
|
|
58
|
+
});
|
|
59
|
+
test
|
|
60
|
+
.nock("https://api.mittwald.de", (api) => {
|
|
61
|
+
api.get(`/v2/projects/${projectId}`).reply(200, {
|
|
62
|
+
id: projectId,
|
|
63
|
+
});
|
|
64
|
+
api
|
|
65
|
+
.post(`/v2/projects/${projectId}/mysql-databases`, {
|
|
66
|
+
database: {
|
|
67
|
+
projectId,
|
|
68
|
+
description: "Test",
|
|
69
|
+
version: "8.0",
|
|
70
|
+
characterSettings: {
|
|
71
|
+
collation: "utf8mb4_unicode_ci",
|
|
72
|
+
characterSet: "utf8mb4",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
user: {
|
|
76
|
+
password: "secret",
|
|
77
|
+
externalAccess: false,
|
|
78
|
+
accessLevel: "full",
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
.reply(201, { id: databaseId, userId });
|
|
82
|
+
api.get(`/v2/mysql-databases/${databaseId}`).reply(200, {
|
|
83
|
+
id: databaseId,
|
|
84
|
+
name: "mysql_xxxxxx",
|
|
85
|
+
});
|
|
86
|
+
api.get(`/v2/mysql-users/${userId}`).times(3).reply(403);
|
|
87
|
+
api.get(`/v2/mysql-users/${userId}`).reply(200, {
|
|
88
|
+
id: userId,
|
|
89
|
+
name: "dbu_xxxxxx",
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
.env({ MITTWALD_API_TOKEN: "foo" })
|
|
93
|
+
.stdout()
|
|
94
|
+
.command(createFlags)
|
|
95
|
+
.it("retries fetching user until successful", (ctx) => {
|
|
96
|
+
// cannot match on exact output, because linebreaks
|
|
97
|
+
expect(ctx.stdout).to.contain("The database mysql_xxxxxx");
|
|
98
|
+
expect(ctx.stdout).to.contain("the user dbu_xxxxxx");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -8,7 +8,7 @@ export default class Delete extends DeleteBaseCommand {
|
|
|
8
8
|
static flags = { ...DeleteBaseCommand.baseFlags };
|
|
9
9
|
static args = { ...mysqlArgs };
|
|
10
10
|
async deleteResource() {
|
|
11
|
-
const mysqlDatabaseId = await withMySQLId(this.apiClient, this.flags, this.args
|
|
11
|
+
const mysqlDatabaseId = await withMySQLId(this.apiClient, this.flags, this.args);
|
|
12
12
|
const response = await this.apiClient.database.deleteMysqlDatabase({
|
|
13
13
|
mysqlDatabaseId,
|
|
14
14
|
});
|
|
@@ -43,7 +43,7 @@ export class Dump extends ExecRenderBaseCommand {
|
|
|
43
43
|
};
|
|
44
44
|
static args = { ...mysqlArgs };
|
|
45
45
|
async exec() {
|
|
46
|
-
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args
|
|
46
|
+
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
|
|
47
47
|
const p = makeProcessRenderer(this.flags, "Dumping a MySQL database");
|
|
48
48
|
const connectionDetails = await getConnectionDetailsWithPassword(this.apiClient, databaseId, p, this.flags);
|
|
49
49
|
if (this.flags["temporary-user"]) {
|
|
@@ -7,7 +7,7 @@ export class Get extends GetBaseCommand {
|
|
|
7
7
|
};
|
|
8
8
|
static args = { ...mysqlArgs };
|
|
9
9
|
async getData() {
|
|
10
|
-
const mysqlDatabaseId = await withMySQLId(this.apiClient, this.flags, this.args
|
|
10
|
+
const mysqlDatabaseId = await withMySQLId(this.apiClient, this.flags, this.args);
|
|
11
11
|
return await this.apiClient.database.getMysqlDatabase({
|
|
12
12
|
mysqlDatabaseId,
|
|
13
13
|
});
|
|
@@ -7,7 +7,7 @@ export class PhpMyAdmin extends BaseCommand {
|
|
|
7
7
|
static args = { ...mysqlArgs };
|
|
8
8
|
async run() {
|
|
9
9
|
const { flags, args } = await this.parse(PhpMyAdmin);
|
|
10
|
-
const databaseId = await withMySQLId(this.apiClient, flags, args
|
|
10
|
+
const databaseId = await withMySQLId(this.apiClient, flags, args);
|
|
11
11
|
const users = await this.apiClient.database.listMysqlUsers({
|
|
12
12
|
mysqlDatabaseId: databaseId,
|
|
13
13
|
});
|
|
@@ -20,7 +20,7 @@ export class PortForward extends ExecRenderBaseCommand {
|
|
|
20
20
|
};
|
|
21
21
|
static args = { ...mysqlArgs };
|
|
22
22
|
async exec() {
|
|
23
|
-
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args
|
|
23
|
+
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
|
|
24
24
|
const p = makeProcessRenderer(this.flags, "Port-forwarding a MySQL database");
|
|
25
25
|
const { sshUser, sshHost, hostname, database } = await getConnectionDetails(this.apiClient, databaseId, this.flags["ssh-user"], p);
|
|
26
26
|
const { port } = this.flags;
|
|
@@ -13,7 +13,7 @@ export class Shell extends ExecRenderBaseCommand {
|
|
|
13
13
|
};
|
|
14
14
|
static args = { ...mysqlArgs };
|
|
15
15
|
async exec() {
|
|
16
|
-
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args
|
|
16
|
+
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
|
|
17
17
|
const p = makeProcessRenderer(this.flags, "Starting a MySQL shell");
|
|
18
18
|
const { sshUser, sshHost, user, hostname, database, password } = await getConnectionDetailsWithPassword(this.apiClient, databaseId, p, this.flags);
|
|
19
19
|
p.complete(_jsx(Text, { children: "Starting MySQL shell -- get ready..." }));
|
|
@@ -15,6 +15,7 @@ export declare class Init extends ExecRenderBaseCommand<typeof Init, void> {
|
|
|
15
15
|
protected exec(): Promise<void>;
|
|
16
16
|
protected render(): React.ReactNode;
|
|
17
17
|
private addSSHCredentials;
|
|
18
|
+
private determineDDEVVersion;
|
|
18
19
|
private installMittwaldPlugin;
|
|
19
20
|
private initializeDDEVProject;
|
|
20
21
|
private determineProjectName;
|
|
@@ -14,6 +14,10 @@ import { renderDDEVConfig } from "../../lib/ddev/config_render.js";
|
|
|
14
14
|
import { loadDDEVConfig } from "../../lib/ddev/config_loader.js";
|
|
15
15
|
import { Value } from "../../rendering/react/components/Value.js";
|
|
16
16
|
import { ddevFlags } from "../../lib/ddev/flags.js";
|
|
17
|
+
import { exec } from "child_process";
|
|
18
|
+
import { promisify } from "util";
|
|
19
|
+
import { compareSemVer } from "semver-parser";
|
|
20
|
+
const execAsync = promisify(exec);
|
|
17
21
|
export class Init extends ExecRenderBaseCommand {
|
|
18
22
|
static summary = "Initialize a new ddev project in the current directory.";
|
|
19
23
|
static description = "This command initializes a new ddev configuration for an existing app installation in the current directory.\n" +
|
|
@@ -50,9 +54,10 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
50
54
|
const appInstallationId = await this.withAppInstallationId(Init);
|
|
51
55
|
const r = makeProcessRenderer(this.flags, "Initializing DDEV project");
|
|
52
56
|
await assertDDEVIsInstalled(r);
|
|
57
|
+
const ddevVersion = await this.determineDDEVVersion(r);
|
|
53
58
|
const config = await this.writeMittwaldConfiguration(r, appInstallationId);
|
|
54
59
|
const projectName = await this.determineProjectName(r);
|
|
55
|
-
await this.initializeDDEVProject(r, config, projectName);
|
|
60
|
+
await this.initializeDDEVProject(r, config, projectName, ddevVersion);
|
|
56
61
|
await this.installMittwaldPlugin(r);
|
|
57
62
|
await this.addSSHCredentials(r);
|
|
58
63
|
await r.complete(_jsx(DDEVInitSuccess, {}));
|
|
@@ -66,6 +71,12 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
66
71
|
"ssh",
|
|
67
72
|
]);
|
|
68
73
|
}
|
|
74
|
+
async determineDDEVVersion(r) {
|
|
75
|
+
const { stdout } = await execAsync("ddev --version");
|
|
76
|
+
const version = stdout.trim().replace(/^ddev version +/, "");
|
|
77
|
+
r.addInfo(_jsx(InfoDDEVVersion, { version: version }));
|
|
78
|
+
return version;
|
|
79
|
+
}
|
|
69
80
|
async installMittwaldPlugin(r) {
|
|
70
81
|
const { "override-mittwald-plugin": mittwaldPlugin } = this.flags;
|
|
71
82
|
await spawnInProcess(r, "installing mittwald plugin", "ddev", [
|
|
@@ -73,13 +84,17 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
73
84
|
mittwaldPlugin,
|
|
74
85
|
]);
|
|
75
86
|
}
|
|
76
|
-
async initializeDDEVProject(r, config, projectName) {
|
|
77
|
-
|
|
87
|
+
async initializeDDEVProject(r, config, projectName, ddevVersion) {
|
|
88
|
+
const ddevFlags = [
|
|
78
89
|
"config",
|
|
79
90
|
"--project-name",
|
|
80
91
|
projectName,
|
|
81
92
|
...ddevConfigToFlags(config),
|
|
82
|
-
]
|
|
93
|
+
];
|
|
94
|
+
if (compareSemVer(ddevVersion, "1.22.7") < 0) {
|
|
95
|
+
ddevFlags.push("--create-docroot");
|
|
96
|
+
}
|
|
97
|
+
await spawnInProcess(r, "initializing DDEV project", "ddev", ddevFlags);
|
|
83
98
|
}
|
|
84
99
|
async determineProjectName(r) {
|
|
85
100
|
const { "project-name": projectName } = this.flags;
|
|
@@ -118,3 +133,6 @@ async function writeContentsToFile(filename, data) {
|
|
|
118
133
|
function InfoUsingExistingName({ name }) {
|
|
119
134
|
return (_jsxs(_Fragment, { children: ["using existing project name: ", _jsx(Value, { children: name })] }));
|
|
120
135
|
}
|
|
136
|
+
function InfoDDEVVersion({ version }) {
|
|
137
|
+
return (_jsxs(_Fragment, { children: ["detected DDEV version: ", _jsx(Value, { children: version })] }));
|
|
138
|
+
}
|
|
@@ -35,7 +35,6 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
35
35
|
data: { description },
|
|
36
36
|
});
|
|
37
37
|
assertStatus(result, 201);
|
|
38
|
-
const eventId = result.headers["etag"];
|
|
39
38
|
stepCreating.complete();
|
|
40
39
|
process.addInfo(_jsxs(Text, { children: ["project ID: ", _jsx(Value, { children: result.data.id })] }));
|
|
41
40
|
if (flags.wait) {
|
|
@@ -43,7 +42,6 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
43
42
|
await waitUntil(async () => {
|
|
44
43
|
const projectResponse = await this.apiClient.project.getProject({
|
|
45
44
|
projectId: result.data.id,
|
|
46
|
-
headers: { "if-event-reached": eventId },
|
|
47
45
|
});
|
|
48
46
|
if (projectResponse.status === 200 &&
|
|
49
47
|
projectResponse.data.readiness === "ready") {
|
|
@@ -5,9 +5,10 @@ export default class Create extends ExecRenderBaseCommand<typeof Create, undefin
|
|
|
5
5
|
output: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
6
6
|
"no-passphrase": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
7
7
|
comment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
-
|
|
8
|
+
expires: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
9
|
quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
10
|
};
|
|
11
11
|
protected exec(): Promise<undefined>;
|
|
12
12
|
protected render(): null;
|
|
13
|
+
private getPassphrase;
|
|
13
14
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Flags } from "@oclif/core";
|
|
3
3
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
4
|
-
import * as cp from "child_process";
|
|
5
4
|
import * as path from "path";
|
|
6
5
|
import * as os from "os";
|
|
7
6
|
import * as fs from "fs/promises";
|
|
8
|
-
import parseDuration from "parse-duration";
|
|
9
7
|
import { ExecRenderBaseCommand } from "../../../rendering/react/ExecRenderBaseCommand.js";
|
|
10
8
|
import { makeProcessRenderer, processFlags, } from "../../../rendering/process/process_flags.js";
|
|
11
9
|
import { Success } from "../../../rendering/react/components/Success.js";
|
|
12
10
|
import { Filename } from "../../../rendering/react/components/Filename.js";
|
|
13
11
|
import { Text } from "ink";
|
|
12
|
+
import { expirationDateFromFlagsOptional, expireFlags, } from "../../../lib/expires.js";
|
|
13
|
+
import { spawnInProcess } from "../../../rendering/process/process_exec.js";
|
|
14
14
|
export default class Create extends ExecRenderBaseCommand {
|
|
15
15
|
static description = "Create and import a new SSH key";
|
|
16
16
|
static flags = {
|
|
17
17
|
...processFlags,
|
|
18
|
+
...expireFlags("SSH key"),
|
|
18
19
|
output: Flags.string({
|
|
19
20
|
description: "A filename in your ~/.ssh directory to write the SSH key to.",
|
|
20
21
|
default: "mstudio-cli",
|
|
@@ -25,41 +26,21 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
25
26
|
comment: Flags.string({
|
|
26
27
|
description: "A comment for the SSH key.",
|
|
27
28
|
}),
|
|
28
|
-
expiresAt: Flags.string({
|
|
29
|
-
description: "Duration after which the SSH key should expire (example: '1y').",
|
|
30
|
-
}),
|
|
31
29
|
};
|
|
32
30
|
async exec() {
|
|
33
|
-
const { flags } = await this.parse(Create);
|
|
34
31
|
const cmd = "ssh-keygen";
|
|
35
|
-
const outputFile = path.join(os.homedir(), ".ssh", flags.output);
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
throw new Error("Invalid duration");
|
|
43
|
-
}
|
|
44
|
-
expiresAt = new Date();
|
|
45
|
-
expiresAt.setTime(new Date().getTime() + parsedDuration);
|
|
46
|
-
}
|
|
47
|
-
if (flags["no-passphrase"]) {
|
|
48
|
-
args.push("-N", "");
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const passphrase = await process.addInput(_jsx(Text, { children: "enter passphrase for SSH key" }), true);
|
|
52
|
-
args.push("-N", passphrase);
|
|
32
|
+
const outputFile = path.join(os.homedir(), ".ssh", this.flags.output);
|
|
33
|
+
const r = makeProcessRenderer(this.flags, "Creating a new SSH key");
|
|
34
|
+
const expiresAt = expirationDateFromFlagsOptional(this.flags);
|
|
35
|
+
const passphrase = await this.getPassphrase(r);
|
|
36
|
+
const args = ["-t", "rsa", "-f", outputFile, "-N", passphrase];
|
|
37
|
+
if (this.flags.comment) {
|
|
38
|
+
args.push("-C", this.flags.comment);
|
|
53
39
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
cp.spawnSync(cmd, args, { stdio: "ignore" });
|
|
59
|
-
return await fs.readFile(outputFile + ".pub", "utf-8");
|
|
60
|
-
});
|
|
61
|
-
process.addInfo(_jsxs(Text, { children: ["ssh key saved to ", _jsx(Filename, { filename: outputFile }), "."] }));
|
|
62
|
-
await process.runStep("importing SSH key", async () => {
|
|
40
|
+
await spawnInProcess(r, "generating SSH key using ssh-keygen", cmd, args);
|
|
41
|
+
const publicKey = await fs.readFile(outputFile + ".pub", "utf-8");
|
|
42
|
+
r.addInfo(_jsx(InfoSSHKeySaved, { filename: outputFile }));
|
|
43
|
+
await r.runStep("importing SSH key", async () => {
|
|
63
44
|
const response = await this.apiClient.user.createSshKey({
|
|
64
45
|
data: {
|
|
65
46
|
publicKey,
|
|
@@ -67,11 +48,22 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
67
48
|
},
|
|
68
49
|
});
|
|
69
50
|
assertStatus(response, 201);
|
|
70
|
-
return response;
|
|
71
51
|
});
|
|
72
|
-
|
|
52
|
+
await r.complete(_jsx(SSHKeySuccess, {}));
|
|
73
53
|
}
|
|
74
54
|
render() {
|
|
75
55
|
return null;
|
|
76
56
|
}
|
|
57
|
+
async getPassphrase(r) {
|
|
58
|
+
if (this.flags["no-passphrase"]) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
return await r.addInput(_jsx(Text, { children: "enter passphrase for SSH key" }), true);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function SSHKeySuccess() {
|
|
65
|
+
return (_jsx(Success, { children: "Your SSH key was successfully created and imported to your user profile." }));
|
|
66
|
+
}
|
|
67
|
+
function InfoSSHKeySaved({ filename }) {
|
|
68
|
+
return (_jsxs(Text, { children: ["ssh key saved to ", _jsx(Filename, { filename: filename }), "."] }));
|
|
77
69
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ExecRenderBaseCommand } from "../../../rendering/react/ExecRenderBaseCommand.js";
|
|
2
|
+
export default class Import extends ExecRenderBaseCommand<typeof Import, undefined> {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
input: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
6
|
+
expires: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
9
|
+
protected exec(): Promise<undefined>;
|
|
10
|
+
protected render(): null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Flags } from "@oclif/core";
|
|
3
|
+
import { assertStatus } from "@mittwald/api-client-commons";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as fs from "fs/promises";
|
|
7
|
+
import { ExecRenderBaseCommand } from "../../../rendering/react/ExecRenderBaseCommand.js";
|
|
8
|
+
import { makeProcessRenderer, processFlags, } from "../../../rendering/process/process_flags.js";
|
|
9
|
+
import { Success } from "../../../rendering/react/components/Success.js";
|
|
10
|
+
import { Filename } from "../../../rendering/react/components/Filename.js";
|
|
11
|
+
import { expirationDateFromFlagsOptional, expireFlags, } from "../../../lib/expires.js";
|
|
12
|
+
export default class Import extends ExecRenderBaseCommand {
|
|
13
|
+
static description = "Import an existing (local) SSH key";
|
|
14
|
+
static flags = {
|
|
15
|
+
...processFlags,
|
|
16
|
+
...expireFlags("SSH key"),
|
|
17
|
+
input: Flags.string({
|
|
18
|
+
description: "A filename in your ~/.ssh directory containing the key to import.",
|
|
19
|
+
default: "id_rsa.pub",
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async exec() {
|
|
23
|
+
const inputFile = path.join(os.homedir(), ".ssh", this.flags.input);
|
|
24
|
+
const r = makeProcessRenderer(this.flags, "Importing an SSH key");
|
|
25
|
+
const expiresAt = expirationDateFromFlagsOptional(this.flags);
|
|
26
|
+
const publicKey = await fs.readFile(inputFile, "utf-8");
|
|
27
|
+
const publicKeyParts = publicKey.split(" ");
|
|
28
|
+
const keys = await r.runStep("retrieving existing SSH keys", async () => {
|
|
29
|
+
const response = await this.apiClient.user.listSshKeys();
|
|
30
|
+
assertStatus(response, 200);
|
|
31
|
+
return response.data;
|
|
32
|
+
});
|
|
33
|
+
const keyAlreadyExists = (keys.sshKeys ?? []).some(({ key }) => publicKeyParts.includes(key));
|
|
34
|
+
if (keyAlreadyExists) {
|
|
35
|
+
r.addInfo(_jsxs(_Fragment, { children: ["the SSH key ", _jsx(Filename, { filename: inputFile }), " is already imported."] }));
|
|
36
|
+
await r.complete(_jsx(SSHKeySuccess, {}));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await r.runStep("importing SSH key", async () => {
|
|
40
|
+
const response = await this.apiClient.user.createSshKey({
|
|
41
|
+
data: {
|
|
42
|
+
publicKey,
|
|
43
|
+
expiresAt: expiresAt?.toJSON(),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
assertStatus(response, 201);
|
|
47
|
+
});
|
|
48
|
+
await r.complete(_jsx(SSHKeySuccess, {}));
|
|
49
|
+
}
|
|
50
|
+
render() {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function SSHKeySuccess() {
|
|
55
|
+
return (_jsx(Success, { children: "Your SSH key was successfully read and imported to your user profile." }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
const d = debug("mw:api-consistency");
|
|
3
|
+
export function configureConsistencyHandling(axios) {
|
|
4
|
+
let lastEventId = undefined;
|
|
5
|
+
axios.interceptors.request.use((config) => {
|
|
6
|
+
if (lastEventId !== undefined) {
|
|
7
|
+
d("setting if-event-reached to %o", lastEventId);
|
|
8
|
+
config.headers["if-event-reached"] = lastEventId;
|
|
9
|
+
}
|
|
10
|
+
return config;
|
|
11
|
+
});
|
|
12
|
+
axios.interceptors.response.use((response) => {
|
|
13
|
+
const isMutatingRequest = ["post", "put", "delete", "patch"].indexOf(response.config?.method?.toLowerCase() ?? "") >= 0;
|
|
14
|
+
const headers = response.headers;
|
|
15
|
+
if (headers.has("etag") && isMutatingRequest) {
|
|
16
|
+
d("setting last event id to %o after mutating request", headers.get("etag"));
|
|
17
|
+
lastEventId = headers.get("etag");
|
|
18
|
+
}
|
|
19
|
+
return response;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
import axiosRetry from "axios-retry";
|
|
3
|
+
const d = debug("mw:api-retry");
|
|
4
|
+
export function configureAxiosRetry(axios) {
|
|
5
|
+
axios.interceptors.request.use((config) => {
|
|
6
|
+
return {
|
|
7
|
+
...config,
|
|
8
|
+
validateStatus: (status) => status < 300,
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
axiosRetry(axios, {
|
|
12
|
+
retries: 10,
|
|
13
|
+
retryDelay: axiosRetry.exponentialDelay,
|
|
14
|
+
onRetry(count, error) {
|
|
15
|
+
d("retrying request after %d attempts; error: %o", count, error.message);
|
|
16
|
+
},
|
|
17
|
+
retryCondition(error) {
|
|
18
|
+
if (error.code === "ERR_FR_TOO_MANY_REDIRECTS") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (axiosRetry.isNetworkOrIdempotentRequestError(error)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const isSafeRequest = error.config?.method?.toLowerCase() === "get";
|
|
25
|
+
const isAccessDenied = error.response?.status === 403;
|
|
26
|
+
return isSafeRequest && isAccessDenied;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -33,10 +33,10 @@ export class AppInstaller {
|
|
|
33
33
|
const projectId = await withProjectId(apiClient, "flag", flags, args, config);
|
|
34
34
|
await autofillFlags(apiClient, process, this.appSupportedFlags, flags, projectId, this.appName);
|
|
35
35
|
const appVersion = await normalizeToAppVersionUuid(apiClient, "version" in flags ? flags.version : "latest", process, this.appId);
|
|
36
|
-
const
|
|
36
|
+
const appInstallationId = await triggerAppInstallation(apiClient, process, projectId, flags, appVersion);
|
|
37
37
|
let successText;
|
|
38
38
|
if (flags.wait) {
|
|
39
|
-
await waitUntilAppIsInstalled(apiClient, process, appInstallationId
|
|
39
|
+
await waitUntilAppIsInstalled(apiClient, process, appInstallationId);
|
|
40
40
|
successText = `Your ${this.appName} installation is now complete. Have fun! 🎉`;
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { ProcessRenderer } from "../../rendering/process/process.js";
|
|
3
3
|
type AppAppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
|
|
4
|
-
export declare function triggerAppInstallation(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, projectId: string, flags: Record<string, string>, appVersion: AppAppVersion): Promise<string
|
|
4
|
+
export declare function triggerAppInstallation(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, projectId: string, flags: Record<string, string>, appVersion: AppAppVersion): Promise<string>;
|
|
5
5
|
export {};
|
package/dist/lib/app/install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
2
2
|
export async function triggerAppInstallation(apiClient, process, projectId, flags, appVersion) {
|
|
3
|
-
const
|
|
3
|
+
const appInstallationId = await process.runStep("starting installation", async () => {
|
|
4
4
|
const result = await apiClient.app.requestAppinstallation({
|
|
5
5
|
projectId,
|
|
6
6
|
data: {
|
|
@@ -14,7 +14,7 @@ export async function triggerAppInstallation(apiClient, process, projectId, flag
|
|
|
14
14
|
},
|
|
15
15
|
});
|
|
16
16
|
assertStatus(result, 201);
|
|
17
|
-
return
|
|
17
|
+
return result.data.id;
|
|
18
18
|
});
|
|
19
19
|
await process.runStep("waiting for installation to be retrievable", async () => {
|
|
20
20
|
for (let attempts = 0; attempts < 10; attempts++) {
|
|
@@ -31,7 +31,6 @@ export async function triggerAppInstallation(apiClient, process, projectId, flag
|
|
|
31
31
|
await process.runStep("setting document root", async () => {
|
|
32
32
|
const result = await apiClient.app.patchAppinstallation({
|
|
33
33
|
appInstallationId,
|
|
34
|
-
headers: { "if-event-reached": eventId },
|
|
35
34
|
data: {
|
|
36
35
|
customDocumentRoot: flags["document-root"],
|
|
37
36
|
},
|
|
@@ -39,5 +38,5 @@ export async function triggerAppInstallation(apiClient, process, projectId, flag
|
|
|
39
38
|
assertStatus(result, 204);
|
|
40
39
|
});
|
|
41
40
|
}
|
|
42
|
-
return
|
|
41
|
+
return appInstallationId;
|
|
43
42
|
}
|
package/dist/lib/app/wait.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { ProcessRenderer } from "../../rendering/process/process.js";
|
|
3
|
-
export declare function waitUntilAppIsInstalled(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, appInstallationId: string
|
|
3
|
+
export declare function waitUntilAppIsInstalled(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, appInstallationId: string): Promise<void>;
|
package/dist/lib/app/wait.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { waitUntil } from "../wait.js";
|
|
3
3
|
import { Text } from "ink";
|
|
4
|
-
export async function waitUntilAppIsInstalled(apiClient, process, appInstallationId
|
|
4
|
+
export async function waitUntilAppIsInstalled(apiClient, process, appInstallationId) {
|
|
5
5
|
const stepWaiting = process.addStep(_jsx(Text, { children: "waiting for app installation to be ready" }));
|
|
6
6
|
await waitUntil(async () => {
|
|
7
7
|
const installationResponse = await apiClient.app.getAppinstallation({
|
|
8
8
|
appInstallationId,
|
|
9
|
-
// TODO: Remove once @mittwald/api-client supports this
|
|
10
|
-
headers: { "if-event-reached": eventId }, // eslint-disable-line
|
|
11
9
|
});
|
|
12
10
|
if (installationResponse.status === 200 &&
|
|
13
11
|
installationResponse.data.appVersion.current ==
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Config } from "@oclif/core";
|
|
2
1
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
3
2
|
import { ArgOutput, FlagOutput } from "@oclif/core/lib/interfaces/parser.js";
|
|
4
3
|
export declare const mysqlConnectionFlags: {
|
|
@@ -7,4 +6,4 @@ export declare const mysqlConnectionFlags: {
|
|
|
7
6
|
export declare const mysqlArgs: {
|
|
8
7
|
"database-id": import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
9
8
|
};
|
|
10
|
-
export declare function withMySQLId(apiClient: MittwaldAPIV2Client, flags: FlagOutput, args: ArgOutput
|
|
9
|
+
export declare function withMySQLId(apiClient: MittwaldAPIV2Client, flags: FlagOutput, args: ArgOutput): Promise<string>;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { Args, Flags } from "@oclif/core";
|
|
2
|
-
import { isUuid } from "../../../normalize_id.js";
|
|
3
|
-
import { withProjectId } from "../../project/flags.js";
|
|
4
2
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
5
3
|
export const mysqlConnectionFlags = {
|
|
6
4
|
"mysql-password": Flags.string({
|
|
@@ -17,7 +15,7 @@ NOTE: This is a security risk, as the password will be visible in the process li
|
|
|
17
15
|
};
|
|
18
16
|
export const mysqlArgs = {
|
|
19
17
|
"database-id": Args.string({
|
|
20
|
-
description: "The ID of the database
|
|
18
|
+
description: "The ID or name of the database",
|
|
21
19
|
required: true,
|
|
22
20
|
}),
|
|
23
21
|
};
|
|
@@ -30,19 +28,11 @@ function getIdCandidate(flags, args) {
|
|
|
30
28
|
}
|
|
31
29
|
throw new Error("No ID given");
|
|
32
30
|
}
|
|
33
|
-
export async function withMySQLId(apiClient, flags, args
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
const projectId = await withProjectId(apiClient, "flag", flags, args, cfg);
|
|
39
|
-
const databases = await apiClient.database.listMysqlDatabases({
|
|
40
|
-
projectId,
|
|
31
|
+
export async function withMySQLId(apiClient, flags, args) {
|
|
32
|
+
const mysqlDatabaseId = getIdCandidate(flags, args);
|
|
33
|
+
const response = await apiClient.database.getMysqlDatabase({
|
|
34
|
+
mysqlDatabaseId,
|
|
41
35
|
});
|
|
42
|
-
assertStatus(
|
|
43
|
-
|
|
44
|
-
if (!database) {
|
|
45
|
-
throw new Error(`No database with name "${candidate}" found`);
|
|
46
|
-
}
|
|
47
|
-
return database.id;
|
|
36
|
+
assertStatus(response, 200);
|
|
37
|
+
return response.data.id;
|
|
48
38
|
}
|
|
@@ -15,7 +15,19 @@ export interface DDEVConfig {
|
|
|
15
15
|
web_environment: string[];
|
|
16
16
|
docroot: string;
|
|
17
17
|
database: DDEVDatabaseConfig;
|
|
18
|
+
hooks: DDEVHooks;
|
|
18
19
|
}
|
|
20
|
+
export type DDEVHookEvent = "start" | "import-db" | "import-files" | "composer" | "stop" | "config" | "exec" | "pull" | "push" | "snapshot" | "restore-snapshot";
|
|
21
|
+
export type DDEVHooks = {
|
|
22
|
+
[k in `pre-${DDEVHookEvent}`]?: DDEVHook[];
|
|
23
|
+
} & {
|
|
24
|
+
[k in `post-${DDEVHookEvent}`]?: DDEVHook[];
|
|
25
|
+
};
|
|
26
|
+
export type DDEVHook = {
|
|
27
|
+
exec: string;
|
|
28
|
+
} | {
|
|
29
|
+
"exec-host": string;
|
|
30
|
+
};
|
|
19
31
|
export interface DDEVDatabaseConfig {
|
|
20
32
|
type: string;
|
|
21
33
|
version: string;
|
|
@@ -4,6 +4,7 @@ export declare class DDEVConfigBuilder {
|
|
|
4
4
|
private apiClient;
|
|
5
5
|
constructor(apiClient: MittwaldAPIV2Client);
|
|
6
6
|
build(appInstallationId: string, type: string): Promise<Partial<DDEVConfig>>;
|
|
7
|
+
private buildHooks;
|
|
7
8
|
private determineDocumentRoot;
|
|
8
9
|
private determineProjectType;
|
|
9
10
|
private determineDatabaseVersion;
|
|
@@ -11,9 +11,10 @@ export class DDEVConfigBuilder {
|
|
|
11
11
|
async build(appInstallationId, type) {
|
|
12
12
|
const appInstallation = await this.getAppInstallation(appInstallationId);
|
|
13
13
|
const systemSoftwares = await this.buildSystemSoftwareVersionMap(appInstallation);
|
|
14
|
+
type = await this.determineProjectType(appInstallation, type);
|
|
14
15
|
return {
|
|
15
16
|
override_config: true,
|
|
16
|
-
type
|
|
17
|
+
type,
|
|
17
18
|
webserver_type: "apache-fpm",
|
|
18
19
|
php_version: this.determinePHPVersion(systemSoftwares),
|
|
19
20
|
database: await this.determineDatabaseVersion(appInstallation),
|
|
@@ -21,6 +22,25 @@ export class DDEVConfigBuilder {
|
|
|
21
22
|
web_environment: [
|
|
22
23
|
`MITTWALD_APP_INSTALLATION_ID=${appInstallation.shortId}`,
|
|
23
24
|
],
|
|
25
|
+
hooks: this.buildHooks(type),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
buildHooks(type) {
|
|
29
|
+
const postPull = [
|
|
30
|
+
{ "exec-host": "ddev config --project-name $DDEV_PROJECT" },
|
|
31
|
+
{ "exec-host": "ddev restart" },
|
|
32
|
+
];
|
|
33
|
+
if (type === "typo3") {
|
|
34
|
+
postPull.push({ exec: "typo3 cache:flush" }, { exec: "typo3 cache:warmup" });
|
|
35
|
+
}
|
|
36
|
+
if (type === "wordpress") {
|
|
37
|
+
postPull.push({ exec: "wp cache flush" });
|
|
38
|
+
}
|
|
39
|
+
if (type === "shopware6") {
|
|
40
|
+
postPull.push({ exec: "bin/console cache:clear" });
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
"post-pull": postPull,
|
|
24
44
|
};
|
|
25
45
|
}
|
|
26
46
|
async determineDocumentRoot(inst) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from "ink";
|
|
3
3
|
export const ProcessStateIcon = ({ step }) => {
|
|
4
4
|
if (step.type === "info") {
|
|
5
|
-
return
|
|
5
|
+
return _jsx(Text, { children: "\uD83D\uDCA1 " });
|
|
6
6
|
}
|
|
7
7
|
else if (step.type === "confirm" || step.type === "input") {
|
|
8
8
|
return _jsx(Text, { children: "\u2753" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.39",
|
|
4
4
|
"description": "Hand-crafted CLI for the mittwald API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -32,25 +32,24 @@
|
|
|
32
32
|
"post:generate": "yarn run -T compile && yarn run -T compile:cjs",
|
|
33
33
|
"test": "yarn test:format && yarn test:licenses && yarn test:unit",
|
|
34
34
|
"test:format": "yarn lint && yarn format --check",
|
|
35
|
-
"test:unit": "mocha --forbid-only \"src/**/*.test.ts\"",
|
|
36
35
|
"test:licenses": "yarn license-check --summary --unknown --failOn 'UNLICENSED;UNKNOWN'",
|
|
37
|
-
"test:readme": "yarn generate:readme && git diff --exit-code README.md"
|
|
36
|
+
"test:readme": "yarn generate:readme && git diff --exit-code README.md",
|
|
37
|
+
"test:unit": "mocha --forbid-only \"src/**/*.test.ts\""
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
".deps",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"bin",
|
|
42
|
+
"dist/**/*.{js,d.ts}"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@mittwald/api-client": "^4.9.0",
|
|
46
|
-
"@mittwald/api-client-commons": "^4.2.2",
|
|
47
46
|
"@mittwald/react-use-promise": "^2.1.2",
|
|
48
47
|
"@oclif/core": "^3.18.1",
|
|
49
48
|
"@oclif/plugin-autocomplete": "^3.0.3",
|
|
50
49
|
"@oclif/plugin-help": "^6.0.5",
|
|
51
50
|
"@oclif/plugin-update": "^4.1.3",
|
|
52
51
|
"@oclif/plugin-warn-if-update-available": "^3.0.2",
|
|
53
|
-
"axios": "^
|
|
52
|
+
"axios-retry": "^4.0.0",
|
|
54
53
|
"chalk": "^5.3.0",
|
|
55
54
|
"date-fns": "^3.2.0",
|
|
56
55
|
"humanize-string": "^3.0.0",
|
|
@@ -66,6 +65,7 @@
|
|
|
66
65
|
"pretty-bytes": "^6.1.0",
|
|
67
66
|
"react": "^18.2.0",
|
|
68
67
|
"semver": "^7.5.4",
|
|
68
|
+
"semver-parser": "^4.1.6",
|
|
69
69
|
"shell-escape": "^0.2.0",
|
|
70
70
|
"tempfile": "^5.0.0"
|
|
71
71
|
},
|