@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.
Files changed (36) hide show
  1. package/README.md +42 -18
  2. package/dist/BaseCommand.js +4 -0
  3. package/dist/commands/database/mysql/create.d.ts +1 -0
  4. package/dist/commands/database/mysql/create.js +34 -29
  5. package/dist/commands/database/mysql/create.test.d.ts +1 -0
  6. package/dist/commands/database/mysql/create.test.js +100 -0
  7. package/dist/commands/database/mysql/delete.js +1 -1
  8. package/dist/commands/database/mysql/dump.js +1 -1
  9. package/dist/commands/database/mysql/get.js +1 -1
  10. package/dist/commands/database/mysql/phpmyadmin.js +1 -1
  11. package/dist/commands/database/mysql/port-forward.js +1 -1
  12. package/dist/commands/database/mysql/shell.js +1 -1
  13. package/dist/commands/ddev/init.d.ts +1 -0
  14. package/dist/commands/ddev/init.js +22 -4
  15. package/dist/commands/project/create.js +0 -2
  16. package/dist/commands/user/ssh-key/create.d.ts +2 -1
  17. package/dist/commands/user/ssh-key/create.js +27 -35
  18. package/dist/commands/user/ssh-key/import.d.ts +11 -0
  19. package/dist/commands/user/ssh-key/import.js +56 -0
  20. package/dist/lib/api_consistency.d.ts +2 -0
  21. package/dist/lib/api_consistency.js +21 -0
  22. package/dist/lib/api_retry.d.ts +2 -0
  23. package/dist/lib/api_retry.js +29 -0
  24. package/dist/lib/app/Installer.js +2 -2
  25. package/dist/lib/app/install.d.ts +1 -1
  26. package/dist/lib/app/install.js +3 -4
  27. package/dist/lib/app/wait.d.ts +1 -1
  28. package/dist/lib/app/wait.js +1 -3
  29. package/dist/lib/database/mysql/flags.d.ts +1 -2
  30. package/dist/lib/database/mysql/flags.js +7 -17
  31. package/dist/lib/ddev/config.d.ts +12 -0
  32. package/dist/lib/ddev/config_builder.d.ts +1 -0
  33. package/dist/lib/ddev/config_builder.js +21 -1
  34. package/dist/rendering/process/components/ProcessStateIcon.js +2 -2
  35. package/dist/rendering/react/hooks/useIncreaseInkStdoutColumns.js +1 -1
  36. 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 [COMMANDS]`](#mw-help-commands)
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 (when a project context is set, you can also use the name)
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 (when a project context is set, you can also use the name)
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 (when a project context is set, you can also use the name)
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 (when a project context is set, you can also use the name)
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 (when a project context is set, you can also use the name)
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 (when a project context is set, you can also use the name)
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 [COMMANDS]`
3109
+ ## `mw help [COMMAND]`
3109
3110
 
3110
3111
  Display help for mw.
3111
3112
 
3112
3113
  ```
3113
3114
  USAGE
3114
- $ mw help [COMMANDS] [-n]
3115
+ $ mw help [COMMAND] [-n]
3115
3116
 
3116
3117
  ARGUMENTS
3117
- COMMANDS Command to show help for.
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.14/src/commands/help.ts)_
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.14/src/commands/update.ts)_
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>] [--expiresAt <value>]
4683
+ $ mw user ssh-key create [-q] [--expires <value>] [--output <value>] [--no-passphrase] [--comment <value>]
4683
4684
 
4684
4685
  FLAGS
4685
- -q, --quiet suppress process output and only display a machine-readable summary.
4686
- --comment=<value> A comment for the SSH key.
4687
- --expiresAt=<value> Duration after which the SSH key should expire (example: '1y').
4688
- --no-passphrase Use this flag to not set a passphrase for the SSH key.
4689
- --output=<value> [default: mstudio-cli] A filename in your ~/.ssh directory to write the SSH key to.
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
@@ -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 p.runStep("creating MySQL database", async () => {
53
- const r = await this.apiClient.database.createMysqlDatabase({
54
- projectId,
55
- data: {
56
- database: {
57
- projectId,
58
- description,
59
- version,
60
- characterSettings: {
61
- collation,
62
- characterSet,
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 r = await this.apiClient.database.getMysqlDatabase({
65
+ const response = await this.apiClient.database.getMysqlDatabase({
77
66
  mysqlDatabaseId: db.id,
78
67
  });
79
- assertStatus(r, 200);
80
- return r.data;
68
+ assertStatus(response, 200);
69
+ return response.data;
81
70
  });
82
71
  const user = await p.runStep("fetching user", async () => {
83
- const r = await this.apiClient.database.getMysqlUser({
72
+ const response = await this.apiClient.database.getMysqlUser({
84
73
  mysqlUserId: db.userId,
85
74
  });
86
- assertStatus(r, 200);
87
- return r.data;
75
+ assertStatus(response, 200);
76
+ return response.data;
88
77
  });
89
- p.complete(_jsxs(Success, { children: ["The database ", _jsx(Value, { children: database.name }), " and the user", " ", _jsx(Value, { children: user.name }), " were successfully created."] }));
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, this.config);
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, this.config);
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, this.config);
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, this.config);
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, this.config);
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, this.config);
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
- await spawnInProcess(r, "initializing DDEV project", "ddev", [
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
- expiresAt: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
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 args = ["-t", "rsa", "-f", outputFile];
37
- const process = makeProcessRenderer(flags, "Creating a new SSH key");
38
- let expiresAt;
39
- if (flags["expiresAt"]) {
40
- const parsedDuration = parseDuration(flags["expiresAt"]);
41
- if (!parsedDuration) {
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
- if (flags.comment) {
55
- args.push("-C", flags.comment);
56
- }
57
- const publicKey = await process.runStep("generating SSH key using ssh-keygen", async () => {
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
- process.complete(_jsx(Success, { children: "Your SSH key was successfully created and imported to your user profile." }));
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,2 @@
1
+ import { AxiosInstance } from "@mittwald/api-client-commons";
2
+ export declare function configureConsistencyHandling(axios: AxiosInstance): void;
@@ -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,2 @@
1
+ import { AxiosInstance } from "@mittwald/api-client-commons";
2
+ export declare function configureAxiosRetry(axios: AxiosInstance): void;
@@ -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 [appInstallationId, eventId] = await triggerAppInstallation(apiClient, process, projectId, flags, appVersion);
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, eventId);
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 {};
@@ -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 [appInstallationId, eventId] = await process.runStep("starting installation", async () => {
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 [result.data.id, result.headers["etag"]];
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 [appInstallationId, eventId];
41
+ return appInstallationId;
43
42
  }
@@ -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, eventId: string): Promise<void>;
3
+ export declare function waitUntilAppIsInstalled(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, appInstallationId: string): Promise<void>;
@@ -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, eventId) {
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, cfg: Config): Promise<string>;
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 (when a project context is set, you can also use the name)",
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, cfg) {
34
- const candidate = getIdCandidate(flags, args);
35
- if (isUuid(candidate)) {
36
- return candidate;
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(databases, 200);
43
- const database = databases.data.find((db) => db.name === candidate);
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: await this.determineProjectType(appInstallation, 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 { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
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 _jsxs(Text, { children: ["\u2139\uFE0F", " "] });
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" });
@@ -4,6 +4,6 @@ export const useIncreaseInkStdoutColumns = () => {
4
4
  const stdout = useStdout().stdout;
5
5
  if (!stdout.isTTY) {
6
6
  // Not using Number.MAX_SAFE_INTEGER here because Ink will fail otherwise
7
- stdout.columns = 100000;
7
+ stdout.columns = 100_000;
8
8
  }
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.0.0-alpha.37",
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
- "dist/**/*.{js,d.ts}",
42
- "bin"
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": "^1.5.0",
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
  },