@mittwald/cli 1.0.0-alpha.39 → 1.0.0-alpha.40
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 +56 -26
- package/dist/BaseCommand.d.ts +0 -4
- package/dist/BaseCommand.js +3 -30
- package/dist/commands/app/install/nextcloud.js +1 -1
- package/dist/commands/ddev/init.d.ts +12 -1
- package/dist/commands/ddev/init.js +64 -25
- package/dist/commands/ddev/render-config.d.ts +2 -0
- package/dist/commands/ddev/render-config.js +4 -1
- package/dist/commands/login/reset.js +11 -8
- package/dist/commands/login/token.js +7 -4
- package/dist/lib/api_consistency.js +8 -2
- package/dist/lib/api_retry.js +16 -2
- package/dist/lib/auth/token.d.ts +13 -0
- package/dist/lib/auth/token.js +44 -0
- package/dist/lib/ddev/config_builder.d.ts +3 -1
- package/dist/lib/ddev/config_builder.js +27 -12
- package/dist/lib/ddev/flags.d.ts +9 -0
- package/dist/lib/ddev/flags.js +17 -0
- package/dist/lib/ddev/init_assert.d.ts +3 -0
- package/dist/lib/ddev/init_assert.js +22 -0
- package/dist/lib/ddev/init_database.d.ts +19 -0
- package/dist/lib/ddev/init_database.js +59 -0
- package/dist/lib/error/InteractiveInputRequiredError.d.ts +7 -0
- package/dist/lib/error/InteractiveInputRequiredError.js +11 -0
- package/dist/rendering/process/components/InteractiveInputDisabled.d.ts +1 -0
- package/dist/rendering/process/components/InteractiveInputDisabled.js +3 -0
- package/dist/rendering/process/components/ProcessInput.js +1 -1
- package/dist/rendering/process/components/ProcessSelect.d.ts +6 -0
- package/dist/rendering/process/components/ProcessSelect.js +61 -0
- package/dist/rendering/process/components/ProcessSelectStateSummary.d.ts +4 -0
- package/dist/rendering/process/components/ProcessSelectStateSummary.js +10 -0
- package/dist/rendering/process/components/ProcessStateIcon.js +3 -1
- package/dist/rendering/process/components/ProcessStateSummary.js +4 -0
- package/dist/rendering/process/process.d.ts +14 -1
- package/dist/rendering/process/process_fancy.d.ts +4 -0
- package/dist/rendering/process/process_fancy.js +30 -0
- package/dist/rendering/process/process_quiet.d.ts +1 -0
- package/dist/rendering/process/process_quiet.js +3 -0
- package/dist/rendering/react/components/ErrorBox.js +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1010,7 +1010,7 @@ FLAG DESCRIPTIONS
|
|
|
1010
1010
|
|
|
1011
1011
|
## `mw app install nextcloud`
|
|
1012
1012
|
|
|
1013
|
-
Creates new
|
|
1013
|
+
Creates new Nextcloud installation.
|
|
1014
1014
|
|
|
1015
1015
|
```
|
|
1016
1016
|
USAGE
|
|
@@ -1021,17 +1021,17 @@ FLAGS
|
|
|
1021
1021
|
-p, --project-id=<value> ID or short ID of a project; this flag is optional if a default project is set in the
|
|
1022
1022
|
context
|
|
1023
1023
|
-q, --quiet suppress process output and only display a machine-readable summary.
|
|
1024
|
-
-w, --wait wait for your
|
|
1024
|
+
-w, --wait wait for your Nextcloud to be ready.
|
|
1025
1025
|
--admin-email=<value> email address of your administrator user.
|
|
1026
1026
|
--admin-pass=<value> password of your administrator user.
|
|
1027
1027
|
--admin-user=<value> Username for your administrator user.
|
|
1028
|
-
--host=<value> host to initially configure your
|
|
1028
|
+
--host=<value> host to initially configure your Nextcloud installation with; needs to be created
|
|
1029
1029
|
separately.
|
|
1030
|
-
--site-title=<value> site title for your
|
|
1031
|
-
--version=<value> (required) [default: latest] version of
|
|
1030
|
+
--site-title=<value> site title for your Nextcloud installation.
|
|
1031
|
+
--version=<value> (required) [default: latest] version of Nextcloud to be installed.
|
|
1032
1032
|
|
|
1033
1033
|
DESCRIPTION
|
|
1034
|
-
Creates new
|
|
1034
|
+
Creates new Nextcloud installation.
|
|
1035
1035
|
|
|
1036
1036
|
FLAG DESCRIPTIONS
|
|
1037
1037
|
-p, --project-id=<value> ID or short ID of a project; this flag is optional if a default project is set in the context
|
|
@@ -1046,40 +1046,40 @@ FLAG DESCRIPTIONS
|
|
|
1046
1046
|
|
|
1047
1047
|
--admin-email=<value> email address of your administrator user.
|
|
1048
1048
|
|
|
1049
|
-
email address that will be used for the first administrator user that is created during the
|
|
1049
|
+
email address that will be used for the first administrator user that is created during the Nextcloud installation.
|
|
1050
1050
|
If unspecified, email address of your mStudio account will be used. This email address can be changed after the
|
|
1051
1051
|
installation is finished.
|
|
1052
1052
|
|
|
1053
1053
|
--admin-pass=<value> password of your administrator user.
|
|
1054
1054
|
|
|
1055
|
-
The password that will be used for the first administrator user that is created during the
|
|
1055
|
+
The password that will be used for the first administrator user that is created during the Nextcloud installation.
|
|
1056
1056
|
If unspecified, a random secure password will be generated and printed to stdout. This password can be changed after
|
|
1057
1057
|
the installation is finished
|
|
1058
1058
|
|
|
1059
1059
|
--admin-user=<value> Username for your administrator user.
|
|
1060
1060
|
|
|
1061
|
-
Username of the first administrator user which will be created during the
|
|
1061
|
+
Username of the first administrator user which will be created during the Nextcloud installation.
|
|
1062
1062
|
If unspecified, an adequate username will be generated.
|
|
1063
1063
|
After the installation is finished, the username can be changed and additional administrator users can be created.
|
|
1064
1064
|
|
|
1065
|
-
--host=<value> host to initially configure your
|
|
1065
|
+
--host=<value> host to initially configure your Nextcloud installation with; needs to be created separately.
|
|
1066
1066
|
|
|
1067
|
-
Specify a host which will be used during the installation and as an initial host for the
|
|
1067
|
+
Specify a host which will be used during the installation and as an initial host for the Nextcloud configuration.
|
|
1068
1068
|
If unspecified, the default host for the given project will be used.
|
|
1069
|
-
This does not change the target of the used host and can be changed later by configuring the host and your
|
|
1070
|
-
|
|
1069
|
+
This does not change the target of the used host and can be changed later by configuring the host and your Nextcloud
|
|
1070
|
+
installation.
|
|
1071
1071
|
|
|
1072
|
-
--site-title=<value> site title for your
|
|
1072
|
+
--site-title=<value> site title for your Nextcloud installation.
|
|
1073
1073
|
|
|
1074
|
-
The site title for this
|
|
1074
|
+
The site title for this Nextcloud installation. It is also the title shown in the app overview in the mStudio and
|
|
1075
1075
|
the CLI.
|
|
1076
1076
|
If unspecified, the application name and the given project ID will be used. The title can be changed after the
|
|
1077
1077
|
installation is finished
|
|
1078
1078
|
|
|
1079
|
-
--version=<value> version of
|
|
1079
|
+
--version=<value> version of Nextcloud to be installed.
|
|
1080
1080
|
|
|
1081
|
-
Specify the version in which your
|
|
1082
|
-
If unspecified, the
|
|
1081
|
+
Specify the version in which your Nextcloud will be installed.
|
|
1082
|
+
If unspecified, the Nextcloud will be installed in the latest available version.
|
|
1083
1083
|
```
|
|
1084
1084
|
|
|
1085
1085
|
## `mw app install shopware5`
|
|
@@ -1588,7 +1588,7 @@ EXAMPLES
|
|
|
1588
1588
|
$ mw autocomplete --refresh-cache
|
|
1589
1589
|
```
|
|
1590
1590
|
|
|
1591
|
-
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.
|
|
1591
|
+
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.13/src/commands/autocomplete/index.ts)_
|
|
1592
1592
|
|
|
1593
1593
|
## `mw backup create`
|
|
1594
1594
|
|
|
@@ -2727,8 +2727,8 @@ Initialize a new ddev project in the current directory.
|
|
|
2727
2727
|
|
|
2728
2728
|
```
|
|
2729
2729
|
USAGE
|
|
2730
|
-
$ mw ddev init [INSTALLATION-ID] [-q] [--override-type <value>] [--
|
|
2731
|
-
[--override-mittwald-plugin <value>]
|
|
2730
|
+
$ mw ddev init [INSTALLATION-ID] [-q] [--override-type <value>] [--without-database | --database-id <value>]
|
|
2731
|
+
[--project-name <value>] [--override-mittwald-plugin <value>]
|
|
2732
2732
|
|
|
2733
2733
|
ARGUMENTS
|
|
2734
2734
|
INSTALLATION-ID ID or short ID of an app installation; this argument is optional if a default app installation is set
|
|
@@ -2736,8 +2736,10 @@ ARGUMENTS
|
|
|
2736
2736
|
|
|
2737
2737
|
FLAGS
|
|
2738
2738
|
-q, --quiet suppress process output and only display a machine-readable summary.
|
|
2739
|
+
--database-id=<value> ID of the application database
|
|
2739
2740
|
--override-type=<value> [default: auto] Override the type of the generated DDEV configuration
|
|
2740
2741
|
--project-name=<value> DDEV project name
|
|
2742
|
+
--without-database Create a DDEV project without a database
|
|
2741
2743
|
|
|
2742
2744
|
DEVELOPMENT FLAGS
|
|
2743
2745
|
--override-mittwald-plugin=<value> [default: mittwald/ddev] override the mittwald plugin
|
|
@@ -2764,6 +2766,14 @@ FLAG DESCRIPTIONS
|
|
|
2764
2766
|
This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
|
|
2765
2767
|
scripts), you can use this flag to easily get the IDs of created resources for further processing.
|
|
2766
2768
|
|
|
2769
|
+
--database-id=<value> ID of the application database
|
|
2770
|
+
|
|
2771
|
+
The ID of the database to use for the DDEV project; if set to 'auto', the command will use the database linked to
|
|
2772
|
+
the app installation.
|
|
2773
|
+
|
|
2774
|
+
Setting a database ID (either automatically or manually) is required. To create a DDEV project without a database,
|
|
2775
|
+
set the --without-database flag.
|
|
2776
|
+
|
|
2767
2777
|
--override-mittwald-plugin=<value> override the mittwald plugin
|
|
2768
2778
|
|
|
2769
2779
|
This flag allows you to override the mittwald plugin that should be installed by default; this is useful for testing
|
|
@@ -2779,6 +2789,11 @@ FLAG DESCRIPTIONS
|
|
|
2779
2789
|
--project-name=<value> DDEV project name
|
|
2780
2790
|
|
|
2781
2791
|
The name of the DDEV project
|
|
2792
|
+
|
|
2793
|
+
--without-database Create a DDEV project without a database
|
|
2794
|
+
|
|
2795
|
+
Use this flag to create a DDEV project without a database; this is useful for projects that do not require a
|
|
2796
|
+
database.
|
|
2782
2797
|
```
|
|
2783
2798
|
|
|
2784
2799
|
## `mw ddev render-config [INSTALLATION-ID]`
|
|
@@ -2787,14 +2802,16 @@ Generate a DDEV configuration YAML file for the current app.
|
|
|
2787
2802
|
|
|
2788
2803
|
```
|
|
2789
2804
|
USAGE
|
|
2790
|
-
$ mw ddev render-config [INSTALLATION-ID] [--override-type <value>]
|
|
2805
|
+
$ mw ddev render-config [INSTALLATION-ID] [--override-type <value>] [--without-database | --database-id <value>]
|
|
2791
2806
|
|
|
2792
2807
|
ARGUMENTS
|
|
2793
2808
|
INSTALLATION-ID ID or short ID of an app installation; this argument is optional if a default app installation is set
|
|
2794
2809
|
in the context
|
|
2795
2810
|
|
|
2796
2811
|
FLAGS
|
|
2812
|
+
--database-id=<value> ID of the application database
|
|
2797
2813
|
--override-type=<value> [default: auto] Override the type of the generated DDEV configuration
|
|
2814
|
+
--without-database Create a DDEV project without a database
|
|
2798
2815
|
|
|
2799
2816
|
DESCRIPTION
|
|
2800
2817
|
Generate a DDEV configuration YAML file for the current app.
|
|
@@ -2802,12 +2819,25 @@ DESCRIPTION
|
|
|
2802
2819
|
This command initializes a new ddev configuration in the current directory.
|
|
2803
2820
|
|
|
2804
2821
|
FLAG DESCRIPTIONS
|
|
2822
|
+
--database-id=<value> ID of the application database
|
|
2823
|
+
|
|
2824
|
+
The ID of the database to use for the DDEV project; if set to 'auto', the command will use the database linked to
|
|
2825
|
+
the app installation.
|
|
2826
|
+
|
|
2827
|
+
Setting a database ID (either automatically or manually) is required. To create a DDEV project without a database,
|
|
2828
|
+
set the --without-database flag.
|
|
2829
|
+
|
|
2805
2830
|
--override-type=<value> Override the type of the generated DDEV configuration
|
|
2806
2831
|
|
|
2807
2832
|
The type of the generated DDEV configuration; this can be any of the documented DDEV project types, or 'auto' (which
|
|
2808
2833
|
is also the default) for automatic discovery.
|
|
2809
2834
|
|
|
2810
2835
|
See https://ddev.readthedocs.io/en/latest/users/configuration/config/#type for more information
|
|
2836
|
+
|
|
2837
|
+
--without-database Create a DDEV project without a database
|
|
2838
|
+
|
|
2839
|
+
Use this flag to create a DDEV project without a database; this is useful for projects that do not require a
|
|
2840
|
+
database.
|
|
2811
2841
|
```
|
|
2812
2842
|
|
|
2813
2843
|
## `mw domain dnszone get DNSZONE-ID`
|
|
@@ -3112,10 +3142,10 @@ Display help for mw.
|
|
|
3112
3142
|
|
|
3113
3143
|
```
|
|
3114
3144
|
USAGE
|
|
3115
|
-
$ mw help [COMMAND] [-n]
|
|
3145
|
+
$ mw help [COMMAND...] [-n]
|
|
3116
3146
|
|
|
3117
3147
|
ARGUMENTS
|
|
3118
|
-
COMMAND Command to show help for.
|
|
3148
|
+
COMMAND... Command to show help for.
|
|
3119
3149
|
|
|
3120
3150
|
FLAGS
|
|
3121
3151
|
-n, --nested-commands Include all nested commands in the output.
|
|
@@ -3124,7 +3154,7 @@ DESCRIPTION
|
|
|
3124
3154
|
Display help for mw.
|
|
3125
3155
|
```
|
|
3126
3156
|
|
|
3127
|
-
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.
|
|
3157
|
+
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.18/src/commands/help.ts)_
|
|
3128
3158
|
|
|
3129
3159
|
## `mw login reset`
|
|
3130
3160
|
|
|
@@ -4518,7 +4548,7 @@ EXAMPLES
|
|
|
4518
4548
|
$ mw update --available
|
|
4519
4549
|
```
|
|
4520
4550
|
|
|
4521
|
-
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.
|
|
4551
|
+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.2.0/src/commands/update.ts)_
|
|
4522
4552
|
|
|
4523
4553
|
## `mw user api-token create`
|
|
4524
4554
|
|
package/dist/BaseCommand.d.ts
CHANGED
|
@@ -4,8 +4,4 @@ export declare abstract class BaseCommand extends Command {
|
|
|
4
4
|
protected authenticationRequired: boolean;
|
|
5
5
|
protected apiClient: MittwaldAPIV2Client;
|
|
6
6
|
init(): Promise<void>;
|
|
7
|
-
protected getTokenFilename(): string;
|
|
8
|
-
private readApiToken;
|
|
9
|
-
private readApiTokenFromEnvironment;
|
|
10
|
-
private readApiTokenFromConfig;
|
|
11
7
|
}
|
package/dist/BaseCommand.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as path from "path";
|
|
4
2
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
5
3
|
import { configureAxiosRetry } from "./lib/api_retry.js";
|
|
6
4
|
import { configureConsistencyHandling } from "./lib/api_consistency.js";
|
|
5
|
+
import { getTokenFilename, readApiToken } from "./lib/auth/token.js";
|
|
7
6
|
export class BaseCommand extends Command {
|
|
8
7
|
authenticationRequired = true;
|
|
9
8
|
apiClient = MittwaldAPIV2Client.newUnauthenticated();
|
|
10
9
|
async init() {
|
|
11
10
|
await super.init();
|
|
12
11
|
if (this.authenticationRequired) {
|
|
13
|
-
const token = await this.
|
|
12
|
+
const token = await readApiToken(this.config);
|
|
14
13
|
if (token === undefined) {
|
|
15
|
-
throw new Error(`Could not get token from either config file (${this.
|
|
14
|
+
throw new Error(`Could not get token from either config file (${getTokenFilename(this.config)}) or environment`);
|
|
16
15
|
}
|
|
17
16
|
this.apiClient = MittwaldAPIV2Client.newWithToken(token);
|
|
18
17
|
this.apiClient.axios.defaults.headers["User-Agent"] =
|
|
@@ -21,30 +20,4 @@ export class BaseCommand extends Command {
|
|
|
21
20
|
configureConsistencyHandling(this.apiClient.axios);
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
getTokenFilename() {
|
|
25
|
-
return path.join(this.config.configDir, "token");
|
|
26
|
-
}
|
|
27
|
-
async readApiToken() {
|
|
28
|
-
return (this.readApiTokenFromEnvironment() ??
|
|
29
|
-
(await this.readApiTokenFromConfig()));
|
|
30
|
-
}
|
|
31
|
-
readApiTokenFromEnvironment() {
|
|
32
|
-
const token = process.env.MITTWALD_API_TOKEN;
|
|
33
|
-
if (token === undefined) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
return token.trim();
|
|
37
|
-
}
|
|
38
|
-
async readApiTokenFromConfig() {
|
|
39
|
-
try {
|
|
40
|
-
const tokenFileContents = await fs.readFile(this.getTokenFilename(), "utf-8");
|
|
41
|
-
return tokenFileContents.trim();
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
23
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ExecRenderBaseCommand } from "../../../rendering/react/ExecRenderBaseCommand.js";
|
|
2
2
|
import { AppInstaller, } from "../../../lib/app/Installer.js";
|
|
3
|
-
const installer = new AppInstaller("0b97d59f-ee13-4f18-a1f6-53e1beaf2e70", "
|
|
3
|
+
const installer = new AppInstaller("0b97d59f-ee13-4f18-a1f6-53e1beaf2e70", "Nextcloud", [
|
|
4
4
|
"version",
|
|
5
5
|
"host",
|
|
6
6
|
"admin-user",
|
|
@@ -6,6 +6,8 @@ export declare class Init extends ExecRenderBaseCommand<typeof Init, void> {
|
|
|
6
6
|
static flags: {
|
|
7
7
|
"project-name": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
8
|
"override-mittwald-plugin": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
"database-id": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
|
+
"without-database": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
9
11
|
"override-type": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
12
|
quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
13
|
};
|
|
@@ -15,9 +17,18 @@ export declare class Init extends ExecRenderBaseCommand<typeof Init, void> {
|
|
|
15
17
|
protected exec(): Promise<void>;
|
|
16
18
|
protected render(): React.ReactNode;
|
|
17
19
|
private addSSHCredentials;
|
|
18
|
-
private
|
|
20
|
+
private getAppInstallation;
|
|
19
21
|
private installMittwaldPlugin;
|
|
20
22
|
private initializeDDEVProject;
|
|
21
23
|
private determineProjectName;
|
|
24
|
+
/**
|
|
25
|
+
* This steps writes the users API token to the local DDEV configuration file.
|
|
26
|
+
* This is necessary to authenticate the DDEV project with the mittwald API.
|
|
27
|
+
*
|
|
28
|
+
* The token is written to the `web_environment` section of the
|
|
29
|
+
* `config.local.yaml`, which _should_ be safe to store credentials in, as it
|
|
30
|
+
* is in DDEV's default `.gitignore` file.
|
|
31
|
+
*/
|
|
32
|
+
private writeAuthConfiguration;
|
|
22
33
|
private writeMittwaldConfiguration;
|
|
23
34
|
}
|
|
@@ -2,22 +2,24 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { ExecRenderBaseCommand } from "../../rendering/react/ExecRenderBaseCommand.js";
|
|
3
3
|
import { appInstallationArgs } from "../../lib/app/flags.js";
|
|
4
4
|
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
5
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { DDEVConfigBuilder } from "../../lib/ddev/config_builder.js";
|
|
8
8
|
import { spawnInProcess } from "../../rendering/process/process_exec.js";
|
|
9
9
|
import { Flags } from "@oclif/core";
|
|
10
10
|
import { DDEVInitSuccess } from "../../rendering/react/components/DDEV/DDEVInitSuccess.js";
|
|
11
11
|
import { ddevConfigToFlags } from "../../lib/ddev/config.js";
|
|
12
|
-
import { hasBinary } from "../../lib/hasbin.js";
|
|
13
12
|
import { renderDDEVConfig } from "../../lib/ddev/config_render.js";
|
|
14
13
|
import { loadDDEVConfig } from "../../lib/ddev/config_loader.js";
|
|
15
14
|
import { Value } from "../../rendering/react/components/Value.js";
|
|
16
15
|
import { ddevFlags } from "../../lib/ddev/flags.js";
|
|
17
|
-
import { exec } from "child_process";
|
|
18
|
-
import { promisify } from "util";
|
|
19
16
|
import { compareSemVer } from "semver-parser";
|
|
20
|
-
|
|
17
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
18
|
+
import { readApiToken } from "../../lib/auth/token.js";
|
|
19
|
+
import { isNotFound } from "../../lib/fsutil.js";
|
|
20
|
+
import { dump, load } from "js-yaml";
|
|
21
|
+
import { determineDDEVDatabaseId } from "../../lib/ddev/init_database.js";
|
|
22
|
+
import { assertDDEVIsInstalled, determineDDEVVersion, } from "../../lib/ddev/init_assert.js";
|
|
21
23
|
export class Init extends ExecRenderBaseCommand {
|
|
22
24
|
static summary = "Initialize a new ddev project in the current directory.";
|
|
23
25
|
static description = "This command initializes a new ddev configuration for an existing app installation in the current directory.\n" +
|
|
@@ -54,8 +56,11 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
54
56
|
const appInstallationId = await this.withAppInstallationId(Init);
|
|
55
57
|
const r = makeProcessRenderer(this.flags, "Initializing DDEV project");
|
|
56
58
|
await assertDDEVIsInstalled(r);
|
|
57
|
-
const ddevVersion = await
|
|
58
|
-
const
|
|
59
|
+
const ddevVersion = await determineDDEVVersion(r);
|
|
60
|
+
const appInstallation = await this.getAppInstallation(r, appInstallationId);
|
|
61
|
+
const databaseId = await determineDDEVDatabaseId(r, this.apiClient, this.flags, appInstallation);
|
|
62
|
+
await this.writeAuthConfiguration(r);
|
|
63
|
+
const config = await this.writeMittwaldConfiguration(r, appInstallationId, databaseId);
|
|
59
64
|
const projectName = await this.determineProjectName(r);
|
|
60
65
|
await this.initializeDDEVProject(r, config, projectName, ddevVersion);
|
|
61
66
|
await this.installMittwaldPlugin(r);
|
|
@@ -71,11 +76,14 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
71
76
|
"ssh",
|
|
72
77
|
]);
|
|
73
78
|
}
|
|
74
|
-
async
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
async getAppInstallation(r, appInstallationId) {
|
|
80
|
+
return r.runStep("fetching app installation", async () => {
|
|
81
|
+
const r = await this.apiClient.app.getAppinstallation({
|
|
82
|
+
appInstallationId,
|
|
83
|
+
});
|
|
84
|
+
assertStatus(r, 200);
|
|
85
|
+
return r.data;
|
|
86
|
+
});
|
|
79
87
|
}
|
|
80
88
|
async installMittwaldPlugin(r) {
|
|
81
89
|
const { "override-mittwald-plugin": mittwaldPlugin } = this.flags;
|
|
@@ -94,6 +102,7 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
94
102
|
if (compareSemVer(ddevVersion, "1.22.7") < 0) {
|
|
95
103
|
ddevFlags.push("--create-docroot");
|
|
96
104
|
}
|
|
105
|
+
this.debug("running %o %o", "ddev", ddevFlags);
|
|
97
106
|
await spawnInProcess(r, "initializing DDEV project", "ddev", ddevFlags);
|
|
98
107
|
}
|
|
99
108
|
async determineProjectName(r) {
|
|
@@ -108,23 +117,56 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
108
117
|
}
|
|
109
118
|
return await r.addInput("Enter the project name", false);
|
|
110
119
|
}
|
|
111
|
-
|
|
120
|
+
/**
|
|
121
|
+
* This steps writes the users API token to the local DDEV configuration file.
|
|
122
|
+
* This is necessary to authenticate the DDEV project with the mittwald API.
|
|
123
|
+
*
|
|
124
|
+
* The token is written to the `web_environment` section of the
|
|
125
|
+
* `config.local.yaml`, which _should_ be safe to store credentials in, as it
|
|
126
|
+
* is in DDEV's default `.gitignore` file.
|
|
127
|
+
*/
|
|
128
|
+
async writeAuthConfiguration(r) {
|
|
129
|
+
// NOTE that config.local.yaml is in DDEV's default .gitignore file, so
|
|
130
|
+
// it *should* be safe to store credentials in there.
|
|
131
|
+
const configFile = path.join(".ddev", "config.local.yaml");
|
|
132
|
+
const token = await readApiToken(this.config);
|
|
133
|
+
await r.runStep("writing local-only DDEV configuration", async () => {
|
|
134
|
+
try {
|
|
135
|
+
const existing = await readFile(configFile, { encoding: "utf-8" });
|
|
136
|
+
const parsed = load(existing);
|
|
137
|
+
const alreadyContainsAPIToken = (parsed.web_environment ?? []).some((e) => e.startsWith("MITTWALD_API_TOKEN="));
|
|
138
|
+
if (!alreadyContainsAPIToken) {
|
|
139
|
+
parsed.web_environment = [
|
|
140
|
+
...(parsed.web_environment ?? []),
|
|
141
|
+
`MITTWALD_API_TOKEN=${token}`,
|
|
142
|
+
];
|
|
143
|
+
await writeContentsToFile(configFile, dump(parsed));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (isNotFound(err)) {
|
|
148
|
+
const config = {
|
|
149
|
+
web_environment: [`MITTWALD_API_TOKEN=${token}`],
|
|
150
|
+
};
|
|
151
|
+
await writeContentsToFile(configFile, dump(config));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async writeMittwaldConfiguration(r, appInstallationId, databaseId) {
|
|
161
|
+
const builder = new DDEVConfigBuilder(this.apiClient);
|
|
112
162
|
return await r.runStep("creating mittwald-specific DDEV configuration", async () => {
|
|
113
|
-
const
|
|
114
|
-
const config = await builder.build(appInstallationId, this.flags["override-type"]);
|
|
163
|
+
const config = await builder.build(appInstallationId, databaseId, this.flags["override-type"]);
|
|
115
164
|
const configFile = path.join(".ddev", "config.mittwald.yaml");
|
|
116
165
|
await writeContentsToFile(configFile, renderDDEVConfig(appInstallationId, config));
|
|
117
166
|
return config;
|
|
118
167
|
});
|
|
119
168
|
}
|
|
120
169
|
}
|
|
121
|
-
async function assertDDEVIsInstalled(r) {
|
|
122
|
-
await r.runStep("check if DDEV is installed", async () => {
|
|
123
|
-
if (!(await hasBinary("ddev"))) {
|
|
124
|
-
throw new Error("this command requires DDEV to be installed");
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
170
|
async function writeContentsToFile(filename, data) {
|
|
129
171
|
const dirname = path.dirname(filename);
|
|
130
172
|
await mkdir(dirname, { recursive: true });
|
|
@@ -133,6 +175,3 @@ async function writeContentsToFile(filename, data) {
|
|
|
133
175
|
function InfoUsingExistingName({ name }) {
|
|
134
176
|
return (_jsxs(_Fragment, { children: ["using existing project name: ", _jsx(Value, { children: name })] }));
|
|
135
177
|
}
|
|
136
|
-
function InfoDDEVVersion({ version }) {
|
|
137
|
-
return (_jsxs(_Fragment, { children: ["detected DDEV version: ", _jsx(Value, { children: version })] }));
|
|
138
|
-
}
|
|
@@ -3,6 +3,8 @@ export declare class RenderConfig extends ExtendedBaseCommand<typeof RenderConfi
|
|
|
3
3
|
static summary: string;
|
|
4
4
|
static description: string;
|
|
5
5
|
static flags: {
|
|
6
|
+
"database-id": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
"without-database": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
6
8
|
"override-type": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
9
|
};
|
|
8
10
|
static args: {
|
|
@@ -15,8 +15,11 @@ export class RenderConfig extends ExtendedBaseCommand {
|
|
|
15
15
|
async run() {
|
|
16
16
|
const appInstallationId = await this.withAppInstallationId(RenderConfig);
|
|
17
17
|
const projectType = this.flags["override-type"];
|
|
18
|
+
const databaseId = this.flags["without-database"]
|
|
19
|
+
? "none"
|
|
20
|
+
: this.flags["database-id"];
|
|
18
21
|
const ddevConfigBuilder = new DDEVConfigBuilder(this.apiClient);
|
|
19
|
-
const ddevConfig = await ddevConfigBuilder.build(appInstallationId, projectType);
|
|
22
|
+
const ddevConfig = await ddevConfigBuilder.build(appInstallationId, databaseId, projectType);
|
|
20
23
|
this.log(renderDDEVConfig(appInstallationId, ddevConfig));
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -5,32 +5,35 @@ import { Box, Text } from "ink";
|
|
|
5
5
|
import { Note } from "../../rendering/react/components/Note.js";
|
|
6
6
|
import { FancyProcessRenderer } from "../../rendering/process/process_fancy.js";
|
|
7
7
|
import { Filename } from "../../rendering/react/components/Filename.js";
|
|
8
|
+
import { getTokenFilename } from "../../lib/auth/token.js";
|
|
9
|
+
import { isNotFound } from "../../lib/fsutil.js";
|
|
8
10
|
export default class Reset extends ExecRenderBaseCommand {
|
|
9
11
|
static description = "Reset your local authentication state";
|
|
10
12
|
authenticationRequired = false;
|
|
11
13
|
async exec() {
|
|
12
14
|
const process = new FancyProcessRenderer("Resetting authentication state");
|
|
15
|
+
const tokenFilename = getTokenFilename(this.config);
|
|
13
16
|
process.start();
|
|
14
|
-
if (await this.tokenFileExists()) {
|
|
15
|
-
const step = process.addStep(_jsxs(Text, { children: ["Deleting token file ", _jsx(Filename, { filename:
|
|
16
|
-
await fs.rm(
|
|
17
|
+
if (await this.tokenFileExists(tokenFilename)) {
|
|
18
|
+
const step = process.addStep(_jsxs(Text, { children: ["Deleting token file ", _jsx(Filename, { filename: tokenFilename })] }));
|
|
19
|
+
await fs.rm(tokenFilename, { force: true });
|
|
17
20
|
step.complete();
|
|
18
|
-
process.complete(_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Authentication state successfully reset" }), _jsx(Note, { children: "Please keep in mind that this does not invalidate the token on the server. Invalidate your API token using the mStudio web interface, or using the 'mw user api-token revoke' command." })] }));
|
|
21
|
+
await process.complete(_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Authentication state successfully reset" }), _jsx(Note, { children: "Please keep in mind that this does not invalidate the token on the server. Invalidate your API token using the mStudio web interface, or using the 'mw user api-token revoke' command." })] }));
|
|
19
22
|
return { deleted: true };
|
|
20
23
|
}
|
|
21
|
-
process.complete(_jsx(Text, { children: "No token file found, nothing to do" }));
|
|
24
|
+
await process.complete(_jsx(Text, { children: "No token file found, nothing to do" }));
|
|
22
25
|
return { deleted: false };
|
|
23
26
|
}
|
|
24
27
|
render() {
|
|
25
28
|
return null;
|
|
26
29
|
}
|
|
27
|
-
async tokenFileExists() {
|
|
30
|
+
async tokenFileExists(tokenFilename) {
|
|
28
31
|
try {
|
|
29
|
-
await fs.access(
|
|
32
|
+
await fs.access(tokenFilename);
|
|
30
33
|
return true;
|
|
31
34
|
}
|
|
32
35
|
catch (err) {
|
|
33
|
-
if (err
|
|
36
|
+
if (isNotFound(err)) {
|
|
34
37
|
return false;
|
|
35
38
|
}
|
|
36
39
|
throw err;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Flags, ux } from "@oclif/core";
|
|
2
2
|
import { BaseCommand } from "../../BaseCommand.js";
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
|
+
import { getTokenFilename } from "../../lib/auth/token.js";
|
|
5
|
+
import { isNotFound } from "../../lib/fsutil.js";
|
|
4
6
|
export default class Token extends BaseCommand {
|
|
5
7
|
static description = "Authenticate using an API token";
|
|
6
8
|
static flags = {
|
|
@@ -21,17 +23,18 @@ export default class Token extends BaseCommand {
|
|
|
21
23
|
const token = await ux.prompt("Enter your mStudio API token", {
|
|
22
24
|
type: "mask",
|
|
23
25
|
});
|
|
26
|
+
const tokenFilename = getTokenFilename(this.config);
|
|
24
27
|
await fs.mkdir(this.config.configDir, { recursive: true });
|
|
25
|
-
await fs.writeFile(
|
|
26
|
-
this.log("token saved to %o",
|
|
28
|
+
await fs.writeFile(tokenFilename, token, "utf-8");
|
|
29
|
+
this.log("token saved to %o", tokenFilename);
|
|
27
30
|
}
|
|
28
31
|
async tokenFileExists() {
|
|
29
32
|
try {
|
|
30
|
-
await fs.access(this.
|
|
33
|
+
await fs.access(getTokenFilename(this.config));
|
|
31
34
|
return true;
|
|
32
35
|
}
|
|
33
36
|
catch (err) {
|
|
34
|
-
if (err
|
|
37
|
+
if (isNotFound(err)) {
|
|
35
38
|
return false;
|
|
36
39
|
}
|
|
37
40
|
throw err;
|
|
@@ -2,8 +2,10 @@ import debug from "debug";
|
|
|
2
2
|
const d = debug("mw:api-consistency");
|
|
3
3
|
export function configureConsistencyHandling(axios) {
|
|
4
4
|
let lastEventId = undefined;
|
|
5
|
+
let mutatedPaths = undefined;
|
|
5
6
|
axios.interceptors.request.use((config) => {
|
|
6
|
-
if (lastEventId !== undefined
|
|
7
|
+
if (lastEventId !== undefined &&
|
|
8
|
+
mutatedPaths?.some((path) => config.url?.startsWith(path))) {
|
|
7
9
|
d("setting if-event-reached to %o", lastEventId);
|
|
8
10
|
config.headers["if-event-reached"] = lastEventId;
|
|
9
11
|
}
|
|
@@ -13,8 +15,12 @@ export function configureConsistencyHandling(axios) {
|
|
|
13
15
|
const isMutatingRequest = ["post", "put", "delete", "patch"].indexOf(response.config?.method?.toLowerCase() ?? "") >= 0;
|
|
14
16
|
const headers = response.headers;
|
|
15
17
|
if (headers.has("etag") && isMutatingRequest) {
|
|
16
|
-
d("setting last event id to %o after mutating request", headers.get("etag"));
|
|
17
18
|
lastEventId = headers.get("etag");
|
|
19
|
+
const mutatedPath = response.config?.url;
|
|
20
|
+
if (mutatedPath !== undefined) {
|
|
21
|
+
mutatedPaths = [mutatedPath];
|
|
22
|
+
}
|
|
23
|
+
d("setting last event id to %o after mutating request for path %o", headers["etag"], mutatedPath);
|
|
18
24
|
}
|
|
19
25
|
return response;
|
|
20
26
|
});
|
package/dist/lib/api_retry.js
CHANGED
|
@@ -2,16 +2,26 @@ import debug from "debug";
|
|
|
2
2
|
import axiosRetry from "axios-retry";
|
|
3
3
|
const d = debug("mw:api-retry");
|
|
4
4
|
export function configureAxiosRetry(axios) {
|
|
5
|
+
let shouldRetryAccessDenied = false;
|
|
5
6
|
axios.interceptors.request.use((config) => {
|
|
6
7
|
return {
|
|
7
8
|
...config,
|
|
8
9
|
validateStatus: (status) => status < 300,
|
|
9
10
|
};
|
|
10
11
|
});
|
|
12
|
+
axios.interceptors.request.use((config) => {
|
|
13
|
+
if (config.method?.toLowerCase() === "post") {
|
|
14
|
+
shouldRetryAccessDenied = true;
|
|
15
|
+
}
|
|
16
|
+
return config;
|
|
17
|
+
});
|
|
11
18
|
axiosRetry(axios, {
|
|
12
19
|
retries: 10,
|
|
13
20
|
retryDelay: axiosRetry.exponentialDelay,
|
|
14
|
-
onRetry(count, error) {
|
|
21
|
+
onRetry(count, error, config) {
|
|
22
|
+
if (error.response?.status === 412 && config.headers !== undefined) {
|
|
23
|
+
delete config.headers["if-event-reached"];
|
|
24
|
+
}
|
|
15
25
|
d("retrying request after %d attempts; error: %o", count, error.message);
|
|
16
26
|
},
|
|
17
27
|
retryCondition(error) {
|
|
@@ -22,8 +32,12 @@ export function configureAxiosRetry(axios) {
|
|
|
22
32
|
return true;
|
|
23
33
|
}
|
|
24
34
|
const isSafeRequest = error.config?.method?.toLowerCase() === "get";
|
|
35
|
+
const isPreconditionFailed = error.response?.status === 412;
|
|
25
36
|
const isAccessDenied = error.response?.status === 403;
|
|
26
|
-
|
|
37
|
+
if (isPreconditionFailed) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return isSafeRequest && isAccessDenied && shouldRetryAccessDenied;
|
|
27
41
|
},
|
|
28
42
|
});
|
|
29
43
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Config } from "@oclif/core";
|
|
2
|
+
/**
|
|
3
|
+
* Gets the filename in which the CLI should store the API token.
|
|
4
|
+
*
|
|
5
|
+
* @param config The CLI configuration object.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getTokenFilename(config: Config): string;
|
|
8
|
+
/**
|
|
9
|
+
* Reads the API token from the environment or the configuration file.
|
|
10
|
+
*
|
|
11
|
+
* @param config The CLI configuration object.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readApiToken(config: Config): Promise<string | undefined>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { isNotFound } from "../fsutil.js";
|
|
4
|
+
/**
|
|
5
|
+
* Gets the filename in which the CLI should store the API token.
|
|
6
|
+
*
|
|
7
|
+
* @param config The CLI configuration object.
|
|
8
|
+
*/
|
|
9
|
+
export function getTokenFilename(config) {
|
|
10
|
+
return path.join(config.configDir, "token");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reads the API token from the environment or the configuration file.
|
|
14
|
+
*
|
|
15
|
+
* @param config The CLI configuration object.
|
|
16
|
+
*/
|
|
17
|
+
export async function readApiToken(config) {
|
|
18
|
+
return (readApiTokenFromEnvironment() ?? (await readApiTokenFromConfig(config)));
|
|
19
|
+
}
|
|
20
|
+
/** Reads the API token from the MITTWALD_API_TOKEN environment variable. */
|
|
21
|
+
function readApiTokenFromEnvironment() {
|
|
22
|
+
const token = process.env.MITTWALD_API_TOKEN;
|
|
23
|
+
if (token === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return token.trim();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Reads the API token from the configuration file.
|
|
30
|
+
*
|
|
31
|
+
* @param config The CLI configuration object.
|
|
32
|
+
*/
|
|
33
|
+
async function readApiTokenFromConfig(config) {
|
|
34
|
+
try {
|
|
35
|
+
const tokenFileContents = await fs.readFile(getTokenFilename(config), "utf-8");
|
|
36
|
+
return tokenFileContents.trim();
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (isNotFound(err)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -3,11 +3,13 @@ import { DDEVConfig } from "./config.js";
|
|
|
3
3
|
export declare class DDEVConfigBuilder {
|
|
4
4
|
private apiClient;
|
|
5
5
|
constructor(apiClient: MittwaldAPIV2Client);
|
|
6
|
-
build(appInstallationId: string, type: string): Promise<Partial<DDEVConfig>>;
|
|
6
|
+
build(appInstallationId: string, databaseId: string | undefined, type: string): Promise<Partial<DDEVConfig>>;
|
|
7
7
|
private buildHooks;
|
|
8
8
|
private determineDocumentRoot;
|
|
9
9
|
private determineProjectType;
|
|
10
10
|
private determineDatabaseVersion;
|
|
11
|
+
private determineMySQLDatabaseVersion;
|
|
12
|
+
private determineDatabaseVersionFromInstallation;
|
|
11
13
|
private determinePHPVersion;
|
|
12
14
|
private buildSystemSoftwareVersionMap;
|
|
13
15
|
private getSystemSoftware;
|
|
@@ -8,19 +8,19 @@ export class DDEVConfigBuilder {
|
|
|
8
8
|
constructor(apiClient) {
|
|
9
9
|
this.apiClient = apiClient;
|
|
10
10
|
}
|
|
11
|
-
async build(appInstallationId, type) {
|
|
11
|
+
async build(appInstallationId, databaseId, type) {
|
|
12
12
|
const appInstallation = await this.getAppInstallation(appInstallationId);
|
|
13
13
|
const systemSoftwares = await this.buildSystemSoftwareVersionMap(appInstallation);
|
|
14
14
|
type = await this.determineProjectType(appInstallation, type);
|
|
15
15
|
return {
|
|
16
|
-
override_config: true,
|
|
17
16
|
type,
|
|
18
17
|
webserver_type: "apache-fpm",
|
|
19
18
|
php_version: this.determinePHPVersion(systemSoftwares),
|
|
20
|
-
database: await this.determineDatabaseVersion(
|
|
19
|
+
database: await this.determineDatabaseVersion(databaseId),
|
|
21
20
|
docroot: await this.determineDocumentRoot(appInstallation),
|
|
22
21
|
web_environment: [
|
|
23
22
|
`MITTWALD_APP_INSTALLATION_ID=${appInstallation.shortId}`,
|
|
23
|
+
`MITTWALD_DATABASE_ID=${databaseId ?? ""}`,
|
|
24
24
|
],
|
|
25
25
|
hooks: this.buildHooks(type),
|
|
26
26
|
};
|
|
@@ -70,18 +70,33 @@ export class DDEVConfigBuilder {
|
|
|
70
70
|
throw new Error("Automatic project type detection failed. Please specify the project type manually by setting the `--override-type` flag.");
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
async determineDatabaseVersion(
|
|
73
|
+
async determineDatabaseVersion(databaseId) {
|
|
74
|
+
if (!databaseId) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const mysqlDatabase = await this.determineMySQLDatabaseVersion(databaseId);
|
|
78
|
+
if (mysqlDatabase) {
|
|
79
|
+
return mysqlDatabase;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
async determineMySQLDatabaseVersion(mysqlDatabaseId) {
|
|
84
|
+
const r = await this.apiClient.database.getMysqlDatabase({
|
|
85
|
+
mysqlDatabaseId,
|
|
86
|
+
});
|
|
87
|
+
if (r.status !== 200) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
type: "mysql",
|
|
92
|
+
version: r.data.version,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async determineDatabaseVersionFromInstallation(inst) {
|
|
74
96
|
const isPrimary = (db) => db.purpose === "primary";
|
|
75
97
|
const primary = (inst.linkedDatabases || []).find(isPrimary);
|
|
76
98
|
if (primary?.kind === "mysql") {
|
|
77
|
-
|
|
78
|
-
mysqlDatabaseId: primary.databaseId,
|
|
79
|
-
});
|
|
80
|
-
assertStatus(r, 200);
|
|
81
|
-
return {
|
|
82
|
-
type: "mysql",
|
|
83
|
-
version: r.data.version,
|
|
84
|
-
};
|
|
99
|
+
return this.determineMySQLDatabaseVersion(primary.databaseId);
|
|
85
100
|
}
|
|
86
101
|
return undefined;
|
|
87
102
|
}
|
package/dist/lib/ddev/flags.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { InferredFlags } from "@oclif/core/lib/interfaces/index.js";
|
|
2
|
+
declare const ddevDatabaseFlags: {
|
|
3
|
+
"database-id": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
4
|
+
"without-database": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
5
|
+
};
|
|
1
6
|
export declare const ddevFlags: {
|
|
7
|
+
"database-id": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
"without-database": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
2
9
|
"override-type": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
3
10
|
};
|
|
11
|
+
export type DDEVDatabaseFlags = InferredFlags<typeof ddevDatabaseFlags>;
|
|
12
|
+
export {};
|
package/dist/lib/ddev/flags.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
|
+
const ddevDatabaseFlags = {
|
|
3
|
+
"database-id": Flags.string({
|
|
4
|
+
summary: "ID of the application database",
|
|
5
|
+
description: "The ID of the database to use for the DDEV project; if set to 'auto', the command will use the database linked to the app installation.\n" +
|
|
6
|
+
"\n" +
|
|
7
|
+
"Setting a database ID (either automatically or manually) is required. To create a DDEV project without a database, set the --without-database flag.",
|
|
8
|
+
required: false,
|
|
9
|
+
default: undefined,
|
|
10
|
+
}),
|
|
11
|
+
"without-database": Flags.boolean({
|
|
12
|
+
summary: "Create a DDEV project without a database",
|
|
13
|
+
description: "Use this flag to create a DDEV project without a database; this is useful for projects that do not require a database.",
|
|
14
|
+
default: false,
|
|
15
|
+
exclusive: ["database-id"],
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
2
18
|
export const ddevFlags = {
|
|
3
19
|
"override-type": Flags.string({
|
|
4
20
|
summary: "Override the type of the generated DDEV configuration",
|
|
@@ -7,4 +23,5 @@ export const ddevFlags = {
|
|
|
7
23
|
"\n\n" +
|
|
8
24
|
"See https://ddev.readthedocs.io/en/latest/users/configuration/config/#type for more information",
|
|
9
25
|
}),
|
|
26
|
+
...ddevDatabaseFlags,
|
|
10
27
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { hasBinary } from "../hasbin.js";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { exec } from "child_process";
|
|
5
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
export async function assertDDEVIsInstalled(r) {
|
|
8
|
+
await r.runStep("check if DDEV is installed", async () => {
|
|
9
|
+
if (!(await hasBinary("ddev"))) {
|
|
10
|
+
throw new Error("this command requires DDEV to be installed");
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function determineDDEVVersion(r) {
|
|
15
|
+
const { stdout } = await execAsync("ddev --version");
|
|
16
|
+
const version = stdout.trim().replace(/^ddev version +/, "");
|
|
17
|
+
r.addInfo(_jsx(InfoDDEVVersion, { version: version }));
|
|
18
|
+
return version;
|
|
19
|
+
}
|
|
20
|
+
function InfoDDEVVersion({ version }) {
|
|
21
|
+
return (_jsxs(_Fragment, { children: ["detected DDEV version: ", _jsx(Value, { children: version })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProcessRenderer } from "../../rendering/process/process.js";
|
|
2
|
+
import { DDEVDatabaseFlags } from "./flags.js";
|
|
3
|
+
import { type MittwaldAPIV2, type MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
4
|
+
type AppInstallation = MittwaldAPIV2.Components.Schemas.AppAppInstallation;
|
|
5
|
+
/**
|
|
6
|
+
* Determines the database ID to use for the DDEV project.
|
|
7
|
+
*
|
|
8
|
+
* This is done according to the following rules (in order of precedence):
|
|
9
|
+
*
|
|
10
|
+
* 1. If the `--without-database` flag is set, do not use a database.
|
|
11
|
+
* 2. If the `--database-id` flag is set, use the database with the given ID.
|
|
12
|
+
* 3. If the app installation has a linked database with the purpose "primary", use
|
|
13
|
+
* that database.
|
|
14
|
+
* 4. Otherwise, prompt the user to select a database from the list of databases
|
|
15
|
+
* present in the project.
|
|
16
|
+
* 5. If interactive input is not available, terminate with an error.
|
|
17
|
+
*/
|
|
18
|
+
export declare function determineDDEVDatabaseId(r: ProcessRenderer, apiClient: MittwaldAPIV2Client, flags: DDEVDatabaseFlags, appInstallation: AppInstallation): Promise<string | undefined>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { assertStatus, } from "@mittwald/api-client";
|
|
3
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
4
|
+
import { Text } from "ink";
|
|
5
|
+
/**
|
|
6
|
+
* Determines the database ID to use for the DDEV project.
|
|
7
|
+
*
|
|
8
|
+
* This is done according to the following rules (in order of precedence):
|
|
9
|
+
*
|
|
10
|
+
* 1. If the `--without-database` flag is set, do not use a database.
|
|
11
|
+
* 2. If the `--database-id` flag is set, use the database with the given ID.
|
|
12
|
+
* 3. If the app installation has a linked database with the purpose "primary", use
|
|
13
|
+
* that database.
|
|
14
|
+
* 4. Otherwise, prompt the user to select a database from the list of databases
|
|
15
|
+
* present in the project.
|
|
16
|
+
* 5. If interactive input is not available, terminate with an error.
|
|
17
|
+
*/
|
|
18
|
+
export async function determineDDEVDatabaseId(r, apiClient, flags, appInstallation) {
|
|
19
|
+
let databaseId = flags["database-id"];
|
|
20
|
+
const withoutDatabase = flags["without-database"];
|
|
21
|
+
if (withoutDatabase) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (databaseId === undefined) {
|
|
25
|
+
databaseId = (appInstallation.linkedDatabases ?? []).find((db) => db.purpose === "primary")?.databaseId;
|
|
26
|
+
}
|
|
27
|
+
if (databaseId !== undefined) {
|
|
28
|
+
const mysqlDatabaseResponse = await apiClient.database.getMysqlDatabase({
|
|
29
|
+
mysqlDatabaseId: databaseId,
|
|
30
|
+
});
|
|
31
|
+
assertStatus(mysqlDatabaseResponse, 200);
|
|
32
|
+
r.addInfo(_jsx(InfoDatabase, { name: mysqlDatabaseResponse.data.name }));
|
|
33
|
+
return mysqlDatabaseResponse.data.name;
|
|
34
|
+
}
|
|
35
|
+
return await promptDatabaseFromUser(r, apiClient, appInstallation);
|
|
36
|
+
}
|
|
37
|
+
async function promptDatabaseFromUser(r, apiClient, appInstallation) {
|
|
38
|
+
const { projectId } = appInstallation;
|
|
39
|
+
if (!projectId) {
|
|
40
|
+
throw new Error("app installation has no project ID");
|
|
41
|
+
}
|
|
42
|
+
const mysqlDatabaseResponse = await apiClient.database.listMysqlDatabases({
|
|
43
|
+
projectId,
|
|
44
|
+
});
|
|
45
|
+
assertStatus(mysqlDatabaseResponse, 200);
|
|
46
|
+
return await r.addSelect("select the database to use", [
|
|
47
|
+
...mysqlDatabaseResponse.data.map((db) => ({
|
|
48
|
+
value: db.name,
|
|
49
|
+
label: `${db.name} (${db.description})`,
|
|
50
|
+
})),
|
|
51
|
+
{
|
|
52
|
+
value: undefined,
|
|
53
|
+
label: "no database",
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
function InfoDatabase({ name }) {
|
|
58
|
+
return (_jsxs(Text, { children: ["using database: ", _jsx(Value, { children: name })] }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a command requires interactive input, but the current
|
|
3
|
+
* terminal environment does not support it.
|
|
4
|
+
*/
|
|
5
|
+
export default class InteractiveInputRequiredError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("This command requires an interactive input, but the current environment " +
|
|
8
|
+
"does not support it. Please have a look at this command's --help page " +
|
|
9
|
+
"to learn how to pass the required input non-interactively.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const InteractiveInputDisabled: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
export const InteractiveInputDisabled = () => (_jsx(Text, { color: "red", children: "interactive input required; inspect this command's --help page to learn how to pass the required input non-interactively." }));
|
|
@@ -4,7 +4,7 @@ import { ProcessStateIcon } from "./ProcessStateIcon.js";
|
|
|
4
4
|
import { ProcessState } from "./ProcessState.js";
|
|
5
5
|
import { Box, Text, useStdin } from "ink";
|
|
6
6
|
import TextInput from "ink-text-input";
|
|
7
|
-
|
|
7
|
+
import { InteractiveInputDisabled } from "./InteractiveInputDisabled.js";
|
|
8
8
|
export const ProcessInput = ({ step, onSubmit }) => {
|
|
9
9
|
const [value, setValue] = useState("");
|
|
10
10
|
if (!step.value) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ProcessStepSelect } from "../process.js";
|
|
2
|
+
export declare function ProcessSelect<TVal>({ step, onSubmit, onError, }: {
|
|
3
|
+
step: ProcessStepSelect<TVal>;
|
|
4
|
+
onSubmit: (_: TVal) => void;
|
|
5
|
+
onError?: (err: unknown) => void;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { ProcessStateIcon } from "./ProcessStateIcon.js";
|
|
4
|
+
import { ProcessState } from "./ProcessState.js";
|
|
5
|
+
import { Box, Text, useInput, useStdin } from "ink";
|
|
6
|
+
import { InteractiveInputDisabled } from "./InteractiveInputDisabled.js";
|
|
7
|
+
import InteractiveInputRequiredError from "../../../lib/error/InteractiveInputRequiredError.js";
|
|
8
|
+
function useSelectControls(initial, length, onSelect) {
|
|
9
|
+
const { isRawModeSupported } = useStdin();
|
|
10
|
+
const [value, setValue] = useState(initial);
|
|
11
|
+
const [extraUsageHint, setExtraUsageHint] = useState(false);
|
|
12
|
+
if (!isRawModeSupported) {
|
|
13
|
+
return {
|
|
14
|
+
value: undefined,
|
|
15
|
+
extraUsageHint: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
useInput((_, key) => {
|
|
19
|
+
if (key.return) {
|
|
20
|
+
if (value !== undefined) {
|
|
21
|
+
setValue(value);
|
|
22
|
+
onSelect(value);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
setExtraUsageHint(true);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (key.downArrow) {
|
|
29
|
+
setExtraUsageHint(false);
|
|
30
|
+
setValue((prev) => prev === undefined ? 0 : Math.min(length - 1, prev + 1));
|
|
31
|
+
}
|
|
32
|
+
else if (key.upArrow) {
|
|
33
|
+
setExtraUsageHint(false);
|
|
34
|
+
setValue((prev) => (prev === undefined ? 0 : Math.max(prev - 1, 0)));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
value,
|
|
39
|
+
extraUsageHint,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function ProcessSelect({ step, onSubmit, onError, }) {
|
|
43
|
+
const { value, extraUsageHint } = useSelectControls(Math.max(step.options.findIndex((o) => o.value === step.selected), 0), step.options.length, (idx) => onSubmit(step.options[idx].value));
|
|
44
|
+
const { isRawModeSupported } = useStdin();
|
|
45
|
+
if (step.selected === undefined) {
|
|
46
|
+
if (!isRawModeSupported) {
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
onError && onError(new InteractiveInputRequiredError());
|
|
49
|
+
});
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginX: 2, children: [_jsx(ProcessStateIcon, { step: step }), _jsxs(Text, { children: [step.title, ": "] })] }), _jsx(Box, { marginX: 8, children: _jsx(InteractiveInputDisabled, {}) })] }));
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { marginX: 2, children: [_jsx(ProcessStateIcon, { step: step }), _jsxs(Text, { children: [step.title, ": "] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(SelectUsageHint, { extraHint: extraUsageHint }), step.options.map((option, index) => (_jsx(SelectOption, { selected: index === value, label: option.label }, index)))] })] }));
|
|
53
|
+
}
|
|
54
|
+
return _jsx(ProcessState, { step: step });
|
|
55
|
+
}
|
|
56
|
+
function SelectUsageHint({ extraHint }) {
|
|
57
|
+
return (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: extraHint ? "red" : "gray", bold: extraHint, children: "(use the up and down keys to select an option)" }) }));
|
|
58
|
+
}
|
|
59
|
+
function SelectOption({ selected, label, }) {
|
|
60
|
+
return (_jsx(Box, { marginLeft: 4, children: _jsxs(Text, { color: "blue", children: [selected ? "👉︎ " : " ", label] }) }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
export function ProcessSelectStateSummary({ step, }) {
|
|
4
|
+
if (step.selected) {
|
|
5
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: ": " }), _jsx(Text, { color: "green", children: step.options.find((o) => o.value === step.selected)?.label })] }));
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
return _jsx(Text, { children: ": " });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -4,7 +4,9 @@ export const ProcessStateIcon = ({ step }) => {
|
|
|
4
4
|
if (step.type === "info") {
|
|
5
5
|
return _jsx(Text, { children: "\uD83D\uDCA1 " });
|
|
6
6
|
}
|
|
7
|
-
else if (step.type === "confirm" ||
|
|
7
|
+
else if (step.type === "confirm" ||
|
|
8
|
+
step.type === "input" ||
|
|
9
|
+
step.type === "select") {
|
|
8
10
|
return _jsx(Text, { children: "\u2753" });
|
|
9
11
|
}
|
|
10
12
|
else if (step.phase === "completed") {
|
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { ProcessConfirmationStateSummary } from "./ProcessConfirmationStateSummary.js";
|
|
3
3
|
import { ProcessInputStateSummary } from "./ProcessInputStateSummary.js";
|
|
4
4
|
import { Text } from "ink";
|
|
5
|
+
import { ProcessSelectStateSummary } from "./ProcessSelectStateSummary.js";
|
|
5
6
|
export const ProcessStateSummary = ({ step, }) => {
|
|
6
7
|
if (step.type === "info") {
|
|
7
8
|
return _jsx(_Fragment, {});
|
|
@@ -12,6 +13,9 @@ export const ProcessStateSummary = ({ step, }) => {
|
|
|
12
13
|
else if (step.type === "input") {
|
|
13
14
|
return _jsx(ProcessInputStateSummary, { step: step });
|
|
14
15
|
}
|
|
16
|
+
else if (step.type === "select") {
|
|
17
|
+
return _jsx(ProcessSelectStateSummary, { step: step });
|
|
18
|
+
}
|
|
15
19
|
else if (step.phase === "completed") {
|
|
16
20
|
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: ". " }), _jsx(Text, { color: "green", children: "done" })] }));
|
|
17
21
|
}
|
|
@@ -22,7 +22,16 @@ export type ProcessStepInput = {
|
|
|
22
22
|
mask?: boolean;
|
|
23
23
|
value?: string;
|
|
24
24
|
};
|
|
25
|
-
export type
|
|
25
|
+
export type ProcessStepSelect<TVal> = {
|
|
26
|
+
type: "select";
|
|
27
|
+
title: ReactNode;
|
|
28
|
+
options: {
|
|
29
|
+
value: TVal;
|
|
30
|
+
label: ReactNode;
|
|
31
|
+
}[];
|
|
32
|
+
selected?: TVal;
|
|
33
|
+
};
|
|
34
|
+
export type ProcessStep = ProcessStepInfo | ProcessStepRunnable | ProcessStepConfirm | ProcessStepInput | ProcessStepSelect<unknown>;
|
|
26
35
|
export type CleanupFunction = {
|
|
27
36
|
title: ReactNode;
|
|
28
37
|
fn: () => Promise<unknown>;
|
|
@@ -49,6 +58,10 @@ export interface ProcessRenderer {
|
|
|
49
58
|
addInfo(title: ReactElement): void;
|
|
50
59
|
addConfirmation(question: ReactElement): Promise<boolean>;
|
|
51
60
|
addInput(question: ReactNode, mask?: boolean): Promise<string>;
|
|
61
|
+
addSelect<TVal>(question: ReactNode, options: {
|
|
62
|
+
value: TVal;
|
|
63
|
+
label: ReactNode;
|
|
64
|
+
}[]): Promise<TVal>;
|
|
52
65
|
addCleanup(title: ReactNode, fn: () => Promise<unknown>): void;
|
|
53
66
|
complete(summary: ReactElement): Promise<void>;
|
|
54
67
|
error(err: unknown): Promise<void>;
|
|
@@ -11,6 +11,10 @@ export declare class FancyProcessRenderer implements ProcessRenderer {
|
|
|
11
11
|
runStep<TRes>(title: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
|
|
12
12
|
addInfo(title: ReactElement): void;
|
|
13
13
|
addInput(question: React.ReactElement, mask?: boolean): Promise<string>;
|
|
14
|
+
addSelect<TVal>(question: React.ReactNode, options: {
|
|
15
|
+
value: TVal;
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
}[]): Promise<TVal>;
|
|
14
18
|
addConfirmation(question: ReactElement): Promise<boolean>;
|
|
15
19
|
complete(summary: ReactElement): Promise<void>;
|
|
16
20
|
error(err: unknown): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import { Box, render, Text } from "ink";
|
|
|
5
5
|
import { ProcessState } from "./components/ProcessState.js";
|
|
6
6
|
import { ProcessConfirmation } from "./components/ProcessConfirmation.js";
|
|
7
7
|
import { ProcessInput } from "./components/ProcessInput.js";
|
|
8
|
+
import { ProcessSelect } from "./components/ProcessSelect.js";
|
|
8
9
|
export class FancyProcessRenderer {
|
|
9
10
|
title;
|
|
10
11
|
started = false;
|
|
@@ -78,6 +79,35 @@ export class FancyProcessRenderer {
|
|
|
78
79
|
const renderHandle = render(_jsx(ProcessInput, { step: state, onSubmit: onInput }));
|
|
79
80
|
});
|
|
80
81
|
}
|
|
82
|
+
addSelect(question, options) {
|
|
83
|
+
this.start();
|
|
84
|
+
if (this.currentHandler !== null) {
|
|
85
|
+
this.currentHandler.complete();
|
|
86
|
+
}
|
|
87
|
+
const state = {
|
|
88
|
+
type: "select",
|
|
89
|
+
title: question,
|
|
90
|
+
options,
|
|
91
|
+
selected: undefined,
|
|
92
|
+
};
|
|
93
|
+
return new Promise((res, rej) => {
|
|
94
|
+
const onSelect = (selected) => {
|
|
95
|
+
res(selected);
|
|
96
|
+
state.selected = selected;
|
|
97
|
+
if (renderHandle) {
|
|
98
|
+
renderHandle.rerender(_jsx(ProcessSelect, { step: state, onSubmit: onSelect }));
|
|
99
|
+
renderHandle.unmount();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const onError = (err) => {
|
|
103
|
+
if (renderHandle) {
|
|
104
|
+
renderHandle.unmount();
|
|
105
|
+
}
|
|
106
|
+
rej(err);
|
|
107
|
+
};
|
|
108
|
+
const renderHandle = render(_jsx(ProcessSelect, { step: state, onSubmit: onSelect, onError: onError }));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
81
111
|
addConfirmation(question) {
|
|
82
112
|
this.start();
|
|
83
113
|
if (this.currentHandler !== null) {
|
|
@@ -10,6 +10,7 @@ export declare class SilentProcessRenderer implements ProcessRenderer {
|
|
|
10
10
|
error(err: unknown): Promise<void>;
|
|
11
11
|
addConfirmation(): Promise<boolean>;
|
|
12
12
|
addInput(): Promise<string>;
|
|
13
|
+
addSelect<TVal>(): Promise<TVal>;
|
|
13
14
|
addCleanup(_: ReactNode, fn: () => Promise<unknown>): void;
|
|
14
15
|
private cleanup;
|
|
15
16
|
}
|
|
@@ -29,6 +29,9 @@ export class SilentProcessRenderer {
|
|
|
29
29
|
addInput() {
|
|
30
30
|
throw new Error("no interactive input available in quiet mode");
|
|
31
31
|
}
|
|
32
|
+
addSelect() {
|
|
33
|
+
throw new Error("no interactive input available in quiet mode");
|
|
34
|
+
}
|
|
32
35
|
addCleanup(_, fn) {
|
|
33
36
|
this.cleanupFns.push(fn);
|
|
34
37
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import Link from "ink-link";
|
|
4
3
|
import { FailedFlagValidationError, RequiredArgsError, } from "@oclif/core/lib/parser/errors.js";
|
|
5
4
|
import { ApiClientError, } from "@mittwald/api-client-commons";
|
|
5
|
+
import InteractiveInputRequiredError from "../../../lib/error/InteractiveInputRequiredError.js";
|
|
6
6
|
const color = "red";
|
|
7
7
|
const issueURL = "https://github.com/mittwald/cli/issues/new";
|
|
8
8
|
const boxProps = {
|
|
@@ -16,8 +16,8 @@ const boxProps = {
|
|
|
16
16
|
const ErrorStack = ({ err }) => {
|
|
17
17
|
return (_jsxs(Box, { marginX: 2, marginY: 1, flexDirection: "column", rowGap: 1, children: [_jsx(Text, { color: color, dimColor: true, bold: true, children: "ERROR STACK TRACE" }), _jsx(Text, { color: color, dimColor: true, children: "Please provide this when opening a bug report." }), _jsx(Text, { color: color, dimColor: true, children: err.stack })] }));
|
|
18
18
|
};
|
|
19
|
-
const GenericError = ({ err, withStack, }) => {
|
|
20
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Box, { ...boxProps, borderColor: color, children: [_jsx(Text, { color: color, bold: true, underline: true, children:
|
|
19
|
+
const GenericError = ({ err, withStack, withIssue = true, title = "Error" }) => {
|
|
20
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { ...boxProps, borderColor: color, children: [_jsx(Text, { color: color, bold: true, underline: true, children: title.toUpperCase() }), _jsx(Text, { color: color, children: "An error occurred while executing this command:" }), _jsx(Box, { marginX: 2, children: _jsx(Text, { color: color, children: err.toString() }) }), withIssue ? (_jsxs(Text, { color: color, children: ["If you believe this to be a bug, please open an issue at ", issueURL, "."] })) : undefined] }), withStack && "stack" in err ? _jsx(ErrorStack, { err: err }) : undefined] }));
|
|
21
21
|
};
|
|
22
22
|
const InvalidFlagsError = ({ err }) => {
|
|
23
23
|
const color = "yellow";
|
|
@@ -63,6 +63,9 @@ export const ErrorBox = ({ err }) => {
|
|
|
63
63
|
else if (err instanceof ApiClientError) {
|
|
64
64
|
return _jsx(ApiError, { err: err, withStack: true, withHTTPMessages: "body" });
|
|
65
65
|
}
|
|
66
|
+
else if (err instanceof InteractiveInputRequiredError) {
|
|
67
|
+
return (_jsx(GenericError, { err: err, withStack: false, withIssue: false, title: "Input required" }));
|
|
68
|
+
}
|
|
66
69
|
else if (err instanceof Error) {
|
|
67
70
|
return _jsx(GenericError, { err: err, withStack: true });
|
|
68
71
|
}
|