@mittwald/cli 1.0.0-alpha.41 → 1.0.0-alpha.42

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 (49) hide show
  1. package/README.md +108 -6
  2. package/dist/commands/app/list.js +2 -3
  3. package/dist/commands/app/upload.d.ts +18 -0
  4. package/dist/commands/app/upload.js +74 -0
  5. package/dist/commands/database/mysql/dump.d.ts +1 -2
  6. package/dist/commands/database/mysql/dump.js +18 -56
  7. package/dist/commands/database/mysql/import.d.ts +19 -0
  8. package/dist/commands/database/mysql/import.js +69 -0
  9. package/dist/lib/app/custom_installation.d.ts +7 -0
  10. package/dist/lib/app/custom_installation.js +16 -0
  11. package/dist/lib/app/flags.js +4 -0
  12. package/dist/lib/context_flags.d.ts +4 -0
  13. package/dist/lib/context_flags.js +14 -1
  14. package/dist/lib/database/mysql/connect.d.ts +22 -14
  15. package/dist/lib/database/mysql/connect.js +15 -0
  16. package/dist/lib/database/mysql/flags.d.ts +4 -0
  17. package/dist/lib/database/mysql/flags.js +10 -0
  18. package/dist/lib/database/mysql/temp_user.d.ts +13 -0
  19. package/dist/lib/database/mysql/temp_user.js +52 -0
  20. package/dist/lib/error/UnexpectedShortIDPassedError.d.ts +5 -0
  21. package/dist/lib/error/UnexpectedShortIDPassedError.js +10 -0
  22. package/dist/lib/language.d.ts +10 -0
  23. package/dist/lib/language.js +12 -0
  24. package/dist/lib/project/flags.js +9 -2
  25. package/dist/lib/ssh/exec.d.ts +5 -1
  26. package/dist/lib/ssh/exec.js +2 -2
  27. package/dist/rendering/react/RenderBaseCommand.js +12 -5
  28. package/dist/rendering/react/components/AppInstallation/AppInstallationDetails.js +2 -4
  29. package/dist/rendering/react/components/Error/APIError.d.ts +13 -0
  30. package/dist/rendering/react/components/Error/APIError.js +29 -0
  31. package/dist/rendering/react/components/Error/ErrorBox.d.ts +4 -0
  32. package/dist/rendering/react/components/Error/ErrorBox.js +14 -0
  33. package/dist/rendering/react/components/Error/ErrorStack.d.ts +4 -0
  34. package/dist/rendering/react/components/Error/ErrorStack.js +7 -0
  35. package/dist/rendering/react/components/Error/ErrorText.d.ts +3 -0
  36. package/dist/rendering/react/components/Error/ErrorText.js +6 -0
  37. package/dist/rendering/react/components/Error/GenericError.d.ts +12 -0
  38. package/dist/rendering/react/components/Error/GenericError.js +13 -0
  39. package/dist/rendering/react/components/Error/InvalidArgsError.d.ts +5 -0
  40. package/dist/rendering/react/components/Error/InvalidArgsError.js +8 -0
  41. package/dist/rendering/react/components/Error/InvalidFlagsError.d.ts +5 -0
  42. package/dist/rendering/react/components/Error/InvalidFlagsError.js +8 -0
  43. package/dist/rendering/react/components/Error/UnexpectedShortIDPassedErrorBox.d.ts +4 -0
  44. package/dist/rendering/react/components/Error/UnexpectedShortIDPassedErrorBox.js +7 -0
  45. package/dist/rendering/react/components/ErrorBoundary.d.ts +20 -0
  46. package/dist/rendering/react/components/ErrorBoundary.js +32 -0
  47. package/dist/rendering/react/components/ErrorBox.d.ts +0 -1
  48. package/dist/rendering/react/components/ErrorBox.js +12 -48
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -138,6 +138,7 @@ USAGE
138
138
  * [`mw app list`](#mw-app-list)
139
139
  * [`mw app ssh [INSTALLATION-ID]`](#mw-app-ssh-installation-id)
140
140
  * [`mw app uninstall [INSTALLATION-ID]`](#mw-app-uninstall-installation-id)
141
+ * [`mw app upload [INSTALLATION-ID]`](#mw-app-upload-installation-id)
141
142
  * [`mw app versions [APP]`](#mw-app-versions-app)
142
143
  * [`mw autocomplete [SHELL]`](#mw-autocomplete-shell)
143
144
  * [`mw backup create`](#mw-backup-create)
@@ -170,6 +171,7 @@ USAGE
170
171
  * [`mw database mysql delete DATABASE-ID`](#mw-database-mysql-delete-database-id)
171
172
  * [`mw database mysql dump DATABASE-ID`](#mw-database-mysql-dump-database-id)
172
173
  * [`mw database mysql get DATABASE-ID`](#mw-database-mysql-get-database-id)
174
+ * [`mw database mysql import DATABASE-ID`](#mw-database-mysql-import-database-id)
173
175
  * [`mw database mysql list`](#mw-database-mysql-list)
174
176
  * [`mw database mysql phpmyadmin DATABASE-ID`](#mw-database-mysql-phpmyadmin-database-id)
175
177
  * [`mw database mysql port-forward DATABASE-ID`](#mw-database-mysql-port-forward-database-id)
@@ -1781,6 +1783,47 @@ FLAG DESCRIPTIONS
1781
1783
  scripts), you can use this flag to easily get the IDs of created resources for further processing.
1782
1784
  ```
1783
1785
 
1786
+ ## `mw app upload [INSTALLATION-ID]`
1787
+
1788
+ Upload the filesystem of an app to a project
1789
+
1790
+ ```
1791
+ USAGE
1792
+ $ mw app upload [INSTALLATION-ID] --source <value> [-q] [--ssh-user <value>] [--dry-run] [--delete]
1793
+
1794
+ ARGUMENTS
1795
+ INSTALLATION-ID ID or short ID of an app installation; this argument is optional if a default app installation is set
1796
+ in the context
1797
+
1798
+ FLAGS
1799
+ -q, --quiet suppress process output and only display a machine-readable summary.
1800
+ --delete delete remote files that are not present locally
1801
+ --dry-run do not actually upload the app installation
1802
+ --source=<value> (required) source directory from which to upload the app installation
1803
+ --ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used
1804
+
1805
+ DESCRIPTION
1806
+ Upload the filesystem of an app to a project
1807
+
1808
+ Upload the filesystem of an app from your local machine to a project.
1809
+
1810
+ CAUTION: This is a potentially destructive operation. It will overwrite files on the server with the files from your
1811
+ local machine. This is NOT a turnkey deployment solution. It is intended for development purposes only.
1812
+
1813
+ FLAG DESCRIPTIONS
1814
+ -q, --quiet suppress process output and only display a machine-readable summary.
1815
+
1816
+ This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
1817
+ scripts), you can use this flag to easily get the IDs of created resources for further processing.
1818
+
1819
+ --ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used
1820
+
1821
+ This flag can be used to override the SSH user that is used for a connection; be default, your own personal user
1822
+ will be used for this.
1823
+
1824
+ You can also set this value by setting the MITTWALD_SSH_USER environment variable.
1825
+ ```
1826
+
1784
1827
  ## `mw app versions [APP]`
1785
1828
 
1786
1829
  List supported Apps and Versions
@@ -1825,7 +1868,7 @@ EXAMPLES
1825
1868
  $ mw autocomplete --refresh-cache
1826
1869
  ```
1827
1870
 
1828
- _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.13/src/commands/autocomplete/index.ts)_
1871
+ _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.15/src/commands/autocomplete/index.ts)_
1829
1872
 
1830
1873
  ## `mw backup create`
1831
1874
 
@@ -2540,7 +2583,7 @@ Create a dump of a MySQL database
2540
2583
 
2541
2584
  ```
2542
2585
  USAGE
2543
- $ mw database mysql dump DATABASE-ID -o <value> [-q] [-p <value>] [--ssh-user <value>] [--temporary-user] [--gzip]
2586
+ $ mw database mysql dump DATABASE-ID -o <value> [-q] [-p <value>] [--temporary-user] [--ssh-user <value>] [--gzip]
2544
2587
 
2545
2588
  ARGUMENTS
2546
2589
  DATABASE-ID The ID or name of the database
@@ -2587,8 +2630,8 @@ FLAG DESCRIPTIONS
2587
2630
 
2588
2631
  --[no-]temporary-user create a temporary user for the dump
2589
2632
 
2590
- Create a temporary user for the dump. This user will be deleted after the dump has been created. This is useful if
2591
- you want to dump a database that is not accessible from the outside.
2633
+ Create a temporary user for this operation. This user will be deleted after the operation has completed. This is
2634
+ useful if you want to work with a database that is not accessible from the outside.
2592
2635
 
2593
2636
  If this flag is disabled, you will need to specify the password of the default user; either via the --mysql-password
2594
2637
  flag or via the MYSQL_PWD environment variable.
@@ -2613,6 +2656,65 @@ DESCRIPTION
2613
2656
  Get a MySQLDatabase.
2614
2657
  ```
2615
2658
 
2659
+ ## `mw database mysql import DATABASE-ID`
2660
+
2661
+ Imports a dump of a MySQL database
2662
+
2663
+ ```
2664
+ USAGE
2665
+ $ mw database mysql import DATABASE-ID -i <value> [-q] [-p <value>] [--temporary-user] [--ssh-user <value>] [--gzip]
2666
+
2667
+ ARGUMENTS
2668
+ DATABASE-ID The ID or name of the database
2669
+
2670
+ FLAGS
2671
+ -i, --input=<value> (required) the input file from which to read the dump ("-" for stdin)
2672
+ -p, --mysql-password=<value> the password to use for the MySQL user (env: MYSQL_PWD)
2673
+ -q, --quiet suppress process output and only display a machine-readable summary.
2674
+ --gzip uncompress the dump with gzip
2675
+ --ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used
2676
+ --[no-]temporary-user create a temporary user for the dump
2677
+
2678
+ FLAG DESCRIPTIONS
2679
+ -i, --input=<value> the input file from which to read the dump ("-" for stdin)
2680
+
2681
+ The input file from which to read the dump to. You can specify "-" or "/dev/stdin" to read the dump directly from
2682
+ STDIN.
2683
+
2684
+ -p, --mysql-password=<value> the password to use for the MySQL user (env: MYSQL_PWD)
2685
+
2686
+ The password to use for the MySQL user. If not provided, the environment variable MYSQL_PWD will be used. If that is
2687
+ not set either, the command will interactively ask for the password.
2688
+
2689
+ NOTE: This is a security risk, as the password will be visible in the process list of your system, and will be
2690
+ visible in your Shell history. It is recommended to use the environment variable instead.
2691
+
2692
+ -q, --quiet suppress process output and only display a machine-readable summary.
2693
+
2694
+ This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
2695
+ scripts), you can use this flag to easily get the IDs of created resources for further processing.
2696
+
2697
+ --gzip uncompress the dump with gzip
2698
+
2699
+ Uncompress the dump with gzip while importing. This is useful for large databases, as it can significantly reduce
2700
+ the size of the dump.
2701
+
2702
+ --ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used
2703
+
2704
+ This flag can be used to override the SSH user that is used for a connection; be default, your own personal user
2705
+ will be used for this.
2706
+
2707
+ You can also set this value by setting the MITTWALD_SSH_USER environment variable.
2708
+
2709
+ --[no-]temporary-user create a temporary user for the dump
2710
+
2711
+ Create a temporary user for this operation. This user will be deleted after the operation has completed. This is
2712
+ useful if you want to work with a database that is not accessible from the outside.
2713
+
2714
+ If this flag is disabled, you will need to specify the password of the default user; either via the --mysql-password
2715
+ flag or via the MYSQL_PWD environment variable.
2716
+ ```
2717
+
2616
2718
  ## `mw database mysql list`
2617
2719
 
2618
2720
  List MySQLDatabases belonging to a Project.
@@ -3391,7 +3493,7 @@ DESCRIPTION
3391
3493
  Display help for mw.
3392
3494
  ```
3393
3495
 
3394
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.20/src/commands/help.ts)_
3496
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.21/src/commands/help.ts)_
3395
3497
 
3396
3498
  ## `mw login reset`
3397
3499
 
@@ -4785,7 +4887,7 @@ EXAMPLES
4785
4887
  $ mw update --available
4786
4888
  ```
4787
4889
 
4788
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.2.3/src/commands/update.ts)_
4890
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.2.6/src/commands/update.ts)_
4789
4891
 
4790
4892
  ## `mw user api-token create`
4791
4893
 
@@ -1,9 +1,8 @@
1
1
  import { assertStatus } from "@mittwald/api-client-commons";
2
2
  import { projectFlags, withProjectId } from "../../lib/project/flags.js";
3
3
  import { ListBaseCommand } from "../../ListBaseCommand.js";
4
- import { phpInstaller } from "./create/php.js";
5
- import { nodeInstaller } from "./create/node.js";
6
4
  import { getAppFromUuid, getAppVersionFromUuid } from "../../lib/app/uuid.js";
5
+ import { isCustomAppInstallation } from "../../lib/app/custom_installation.js";
7
6
  export default class List extends ListBaseCommand {
8
7
  static description = "List installed apps in a project.";
9
8
  static flags = {
@@ -44,7 +43,7 @@ export default class List extends ListBaseCommand {
44
43
  appVersion: {
45
44
  header: "Version",
46
45
  get: (i) => {
47
- if ([phpInstaller.appId, nodeInstaller.appId].includes(i.appId)) {
46
+ if (isCustomAppInstallation(i.appId)) {
48
47
  return "n/a";
49
48
  }
50
49
  if (i.appVersionCurrent?.id === i.appVersionDesired.id) {
@@ -0,0 +1,18 @@
1
+ import { ExecRenderBaseCommand } from "../../rendering/react/ExecRenderBaseCommand.js";
2
+ import { ReactNode } from "react";
3
+ export declare class Upload extends ExecRenderBaseCommand<typeof Upload, void> {
4
+ static summary: string;
5
+ static description: string;
6
+ static args: {
7
+ "installation-id": import("@oclif/core/lib/interfaces/parser.js").Arg<string>;
8
+ };
9
+ static flags: {
10
+ "dry-run": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ delete: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ source: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ "ssh-user": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
+ quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
+ };
16
+ protected exec(): Promise<void>;
17
+ protected render(): ReactNode;
18
+ }
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ExecRenderBaseCommand } from "../../rendering/react/ExecRenderBaseCommand.js";
3
+ import { appInstallationArgs } from "../../lib/app/flags.js";
4
+ import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
5
+ import { Flags } from "@oclif/core";
6
+ import { Success } from "../../rendering/react/components/Success.js";
7
+ import { hasBinary } from "../../lib/hasbin.js";
8
+ import { getSSHConnectionForAppInstallation } from "../../lib/ssh/appinstall.js";
9
+ import { spawnInProcess } from "../../rendering/process/process_exec.js";
10
+ import { sshConnectionFlags } from "../../lib/ssh/flags.js";
11
+ export class Upload extends ExecRenderBaseCommand {
12
+ static summary = "Upload the filesystem of an app to a project";
13
+ static description = "Upload the filesystem of an app from your local machine to a project.\n\n" +
14
+ "" +
15
+ "CAUTION: This is a potentially destructive operation. It will overwrite files on the server with the files from your local machine. " +
16
+ "This is NOT a turnkey deployment solution. It is intended for development purposes only.";
17
+ static args = {
18
+ ...appInstallationArgs,
19
+ };
20
+ static flags = {
21
+ ...processFlags,
22
+ ...sshConnectionFlags,
23
+ "dry-run": Flags.boolean({
24
+ description: "do not actually upload the app installation",
25
+ default: false,
26
+ }),
27
+ delete: Flags.boolean({
28
+ description: "delete remote files that are not present locally",
29
+ default: false,
30
+ }),
31
+ source: Flags.directory({
32
+ description: "source directory from which to upload the app installation",
33
+ required: true,
34
+ exists: true,
35
+ }),
36
+ };
37
+ async exec() {
38
+ const appInstallationId = await this.withAppInstallationId(Upload);
39
+ const { "dry-run": dryRun, source, delete: deleteRemote, "ssh-user": sshUser, } = this.flags;
40
+ const p = makeProcessRenderer(this.flags, "Uploading app installation");
41
+ const { host, user, directory } = await p.runStep("getting connection data", async () => {
42
+ return getSSHConnectionForAppInstallation(this.apiClient, appInstallationId, sshUser);
43
+ });
44
+ await p.runStep("check if rsync is installed", async () => {
45
+ if (!(await hasBinary("rsync"))) {
46
+ throw new Error("this command requires rsync to be installed");
47
+ }
48
+ });
49
+ const rsyncOpts = [
50
+ "--archive",
51
+ "--recursive",
52
+ "--verbose",
53
+ "--progress",
54
+ "--exclude=typo3temp",
55
+ ];
56
+ if (dryRun) {
57
+ rsyncOpts.push("--dry-run");
58
+ }
59
+ if (deleteRemote) {
60
+ rsyncOpts.push("--delete");
61
+ }
62
+ await spawnInProcess(p, "uploading app installation" + (dryRun ? " (dry-run)" : ""), "rsync", [...rsyncOpts, source, `${user}@${host}:${directory}/`]);
63
+ await p.complete(_jsx(UploadSuccess, { dryRun: dryRun }));
64
+ }
65
+ render() {
66
+ return undefined;
67
+ }
68
+ }
69
+ function UploadSuccess({ dryRun }) {
70
+ if (dryRun) {
71
+ return (_jsx(Success, { children: "App would (probably) have successfully been uploaded. \uD83D\uDE42" }));
72
+ }
73
+ return _jsx(Success, { children: "App successfully uploaded; have fun! \uD83D\uDE80" });
74
+ }
@@ -3,10 +3,10 @@ import { ReactNode } from "react";
3
3
  export declare class Dump extends ExecRenderBaseCommand<typeof Dump, Record<string, never>> {
4
4
  static summary: string;
5
5
  static flags: {
6
- "temporary-user": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
7
6
  output: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
7
  gzip: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
8
  "ssh-user": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ "temporary-user": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
10
  "mysql-password": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
11
  quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
12
  };
@@ -16,5 +16,4 @@ export declare class Dump extends ExecRenderBaseCommand<typeof Dump, Record<stri
16
16
  protected exec(): Promise<Record<string, never>>;
17
17
  protected render(): ReactNode;
18
18
  private getOutputStream;
19
- private createTemporaryUser;
20
19
  }
@@ -6,27 +6,17 @@ import { Text } from "ink";
6
6
  import { Value } from "../../../rendering/react/components/Value.js";
7
7
  import * as fs from "fs";
8
8
  import { Success } from "../../../rendering/react/components/Success.js";
9
- import { mysqlArgs, mysqlConnectionFlags, withMySQLId, } from "../../../lib/database/mysql/flags.js";
10
- import { getConnectionDetailsWithPassword } from "../../../lib/database/mysql/connect.js";
11
- import { assertStatus } from "@mittwald/api-client";
12
- import { randomBytes } from "crypto";
9
+ import { mysqlArgs, mysqlConnectionFlagsWithTempUser, withMySQLId, } from "../../../lib/database/mysql/flags.js";
10
+ import { runWithConnectionDetails } from "../../../lib/database/mysql/connect.js";
13
11
  import { executeViaSSH } from "../../../lib/ssh/exec.js";
14
- import assertSuccess from "../../../lib/assert_success.js";
15
12
  import shellEscape from "shell-escape";
16
13
  import { sshConnectionFlags } from "../../../lib/ssh/flags.js";
17
14
  export class Dump extends ExecRenderBaseCommand {
18
15
  static summary = "Create a dump of a MySQL database";
19
16
  static flags = {
20
17
  ...processFlags,
21
- ...mysqlConnectionFlags,
18
+ ...mysqlConnectionFlagsWithTempUser,
22
19
  ...sshConnectionFlags,
23
- "temporary-user": Flags.boolean({
24
- summary: "create a temporary user for the dump",
25
- description: "Create a temporary user for the dump. This user will be deleted after the dump has been created. This is useful if you want to dump a database that is not accessible from the outside.\n\nIf this flag is disabled, you will need to specify the password of the default user; either via the --mysql-password flag or via the MYSQL_PWD environment variable.",
26
- default: true,
27
- required: false,
28
- allowNo: true,
29
- }),
30
20
  output: Flags.string({
31
21
  char: "o",
32
22
  summary: 'the output file to write the dump to ("-" for stdout)',
@@ -45,29 +35,20 @@ export class Dump extends ExecRenderBaseCommand {
45
35
  async exec() {
46
36
  const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
47
37
  const p = makeProcessRenderer(this.flags, "Dumping a MySQL database");
48
- const connectionDetails = await getConnectionDetailsWithPassword(this.apiClient, databaseId, p, this.flags);
49
- if (this.flags["temporary-user"]) {
50
- const [tempUser, tempPassword] = await p.runStep("creating a temporary database user", () => this.createTemporaryUser(databaseId));
51
- p.addCleanup("removing temporary database user", async () => {
52
- const r = await this.apiClient.database.deleteMysqlUser({
53
- mysqlUserId: tempUser.id,
54
- });
55
- assertSuccess(r);
56
- });
57
- connectionDetails.user = tempUser.name;
58
- connectionDetails.password = tempPassword;
59
- }
60
- const { project } = connectionDetails;
61
- const mysqldumpArgs = buildMySqlDumpArgs(connectionDetails);
62
- let cmd = { command: "mysqldump", args: mysqldumpArgs };
63
- if (this.flags.gzip) {
64
- const escapedArgs = shellEscape(mysqldumpArgs);
65
- cmd = {
66
- shell: `set -e -o pipefail > /dev/null ; mysqldump ${escapedArgs} | gzip`,
67
- };
68
- }
69
- await p.runStep(_jsxs(Text, { children: ["starting mysqldump via SSH on project ", _jsx(Value, { children: project.shortId })] }), () => executeViaSSH(this.apiClient, this.flags["ssh-user"], { projectId: connectionDetails.project.id }, cmd, this.getOutputStream()));
70
- await p.complete(_jsx(DumpSuccess, { database: connectionDetails.database, output: this.flags.output }));
38
+ const databaseName = await runWithConnectionDetails(this.apiClient, databaseId, p, this.flags, async (connectionDetails) => {
39
+ const { project } = connectionDetails;
40
+ const mysqldumpArgs = buildMySqlDumpArgs(connectionDetails);
41
+ let cmd = { command: "mysqldump", args: mysqldumpArgs };
42
+ if (this.flags.gzip) {
43
+ const escapedArgs = shellEscape(mysqldumpArgs);
44
+ cmd = {
45
+ shell: `set -e -o pipefail > /dev/null ; mysqldump ${escapedArgs} | gzip`,
46
+ };
47
+ }
48
+ await p.runStep(_jsxs(Text, { children: ["starting mysqldump via SSH on project", " ", _jsx(Value, { children: project.shortId })] }), () => executeViaSSH(this.apiClient, this.flags["ssh-user"], { projectId: connectionDetails.project.id }, cmd, { input: null, output: this.getOutputStream() }));
49
+ return connectionDetails.database;
50
+ });
51
+ await p.complete(_jsx(DumpSuccess, { database: databaseName, output: this.flags.output }));
71
52
  return {};
72
53
  }
73
54
  render() {
@@ -79,29 +60,10 @@ export class Dump extends ExecRenderBaseCommand {
79
60
  }
80
61
  return fs.createWriteStream(this.flags.output);
81
62
  }
82
- async createTemporaryUser(databaseId) {
83
- const password = randomBytes(32).toString("base64");
84
- const createResponse = await this.apiClient.database.createMysqlUser({
85
- mysqlDatabaseId: databaseId,
86
- data: {
87
- accessLevel: "full", // needed for "PROCESS" privilege
88
- externalAccess: false,
89
- password,
90
- databaseId,
91
- description: "Temporary user for exporting database",
92
- },
93
- });
94
- assertStatus(createResponse, 201);
95
- const userResponse = await this.apiClient.database.getMysqlUser({
96
- mysqlUserId: createResponse.data.id,
97
- });
98
- assertStatus(userResponse, 200);
99
- return [userResponse.data, password];
100
- }
101
63
  }
102
64
  function DumpSuccess({ database, output, }) {
103
65
  return (_jsxs(Success, { children: ["Dump of MySQL database ", _jsx(Value, { children: database }), " written to", " ", _jsx(Value, { children: output })] }));
104
66
  }
105
67
  function buildMySqlDumpArgs(d) {
106
- return ["-h", d.hostname, "-u", d.user, "-p" + d.password, d.database];
68
+ return ["-h", d.hostname, "-u", d.user, `-p${d.password}`, d.database];
107
69
  }
@@ -0,0 +1,19 @@
1
+ import { ExecRenderBaseCommand } from "../../../rendering/react/ExecRenderBaseCommand.js";
2
+ import { ReactNode } from "react";
3
+ export declare class Import extends ExecRenderBaseCommand<typeof Import, Record<string, never>> {
4
+ static summary: string;
5
+ static flags: {
6
+ input: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
7
+ gzip: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
8
+ "ssh-user": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ "temporary-user": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
+ "mysql-password": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ };
13
+ static args: {
14
+ "database-id": import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
15
+ };
16
+ protected exec(): Promise<Record<string, never>>;
17
+ protected render(): ReactNode;
18
+ private getInputStream;
19
+ }
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ExecRenderBaseCommand } from "../../../rendering/react/ExecRenderBaseCommand.js";
3
+ import { Flags } from "@oclif/core";
4
+ import { makeProcessRenderer, processFlags, } from "../../../rendering/process/process_flags.js";
5
+ import { Text } from "ink";
6
+ import { Value } from "../../../rendering/react/components/Value.js";
7
+ import * as fs from "fs";
8
+ import { Success } from "../../../rendering/react/components/Success.js";
9
+ import { mysqlArgs, mysqlConnectionFlagsWithTempUser, withMySQLId, } from "../../../lib/database/mysql/flags.js";
10
+ import { executeViaSSH } from "../../../lib/ssh/exec.js";
11
+ import { sshConnectionFlags } from "../../../lib/ssh/flags.js";
12
+ import shellEscape from "shell-escape";
13
+ import { runWithConnectionDetails } from "../../../lib/database/mysql/connect.js";
14
+ export class Import extends ExecRenderBaseCommand {
15
+ static summary = "Imports a dump of a MySQL database";
16
+ static flags = {
17
+ ...processFlags,
18
+ ...mysqlConnectionFlagsWithTempUser,
19
+ ...sshConnectionFlags,
20
+ input: Flags.string({
21
+ char: "i",
22
+ summary: 'the input file from which to read the dump ("-" for stdin)',
23
+ description: 'The input file from which to read the dump to. You can specify "-" or "/dev/stdin" to read the dump directly from STDIN.',
24
+ required: true,
25
+ }),
26
+ gzip: Flags.boolean({
27
+ summary: "uncompress the dump with gzip",
28
+ aliases: ["gz"],
29
+ description: "Uncompress the dump with gzip while importing. This is useful for large databases, as it can significantly reduce the size of the dump.",
30
+ default: false,
31
+ required: false,
32
+ }),
33
+ };
34
+ static args = { ...mysqlArgs };
35
+ async exec() {
36
+ const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
37
+ const p = makeProcessRenderer(this.flags, "Importing a MySQL database");
38
+ const databaseName = await runWithConnectionDetails(this.apiClient, databaseId, p, this.flags, async (connectionDetails) => {
39
+ const { project } = connectionDetails;
40
+ const mysqlArgs = buildMySqlArgs(connectionDetails);
41
+ let cmd = { command: "mysql", args: mysqlArgs };
42
+ if (this.flags.gzip) {
43
+ const escapedArgs = shellEscape(mysqlArgs);
44
+ cmd = {
45
+ shell: `set -e -o pipefail > /dev/null ; gunzip | mysql ${escapedArgs}`,
46
+ };
47
+ }
48
+ await p.runStep(_jsxs(Text, { children: ["starting mysql via SSH on project ", _jsx(Value, { children: project.shortId })] }), () => executeViaSSH(this.apiClient, this.flags["ssh-user"], { projectId: connectionDetails.project.id }, cmd, { input: this.getInputStream(), output: null }));
49
+ return connectionDetails.database;
50
+ });
51
+ await p.complete(_jsx(ImportSuccess, { database: databaseName, input: this.flags.input }));
52
+ return {};
53
+ }
54
+ render() {
55
+ return undefined;
56
+ }
57
+ getInputStream() {
58
+ if (this.flags.input === "-") {
59
+ return process.stdin;
60
+ }
61
+ return fs.createReadStream(this.flags.input);
62
+ }
63
+ }
64
+ function ImportSuccess({ database, input, }) {
65
+ return (_jsxs(Success, { children: ["Dump of MySQL database ", _jsx(Value, { children: database }), " successfully imported", " ", "from ", _jsx(Value, { children: input })] }));
66
+ }
67
+ function buildMySqlArgs(d) {
68
+ return ["-h", d.hostname, "-u", d.user, `-p${d.password}`, d.database];
69
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tests if an app installation is for a custom app (for example, a custom PHP
3
+ * or Node.js app). These are treated differently in the UI.
4
+ *
5
+ * @param appId
6
+ */
7
+ export declare function isCustomAppInstallation(appId: string): boolean;
@@ -0,0 +1,16 @@
1
+ import { phpInstaller } from "../../commands/app/create/php.js";
2
+ import { nodeInstaller } from "../../commands/app/create/node.js";
3
+ import { pythonInstaller } from "../../commands/app/create/python.js";
4
+ /**
5
+ * Tests if an app installation is for a custom app (for example, a custom PHP
6
+ * or Node.js app). These are treated differently in the UI.
7
+ *
8
+ * @param appId
9
+ */
10
+ export function isCustomAppInstallation(appId) {
11
+ return [
12
+ phpInstaller.appId,
13
+ nodeInstaller.appId,
14
+ pythonInstaller.appId,
15
+ ].includes(appId);
16
+ }
@@ -13,6 +13,10 @@ import { makeFlagSet } from "../context_flags.js";
13
13
  export const { flags: appInstallationFlags, args: appInstallationArgs, withId: withAppInstallationId, } = makeFlagSet("installation", "i", {
14
14
  displayName: "app installation",
15
15
  normalize: normalizeAppInstallationId,
16
+ expectedShortIDFormat: {
17
+ pattern: /^a-.*/,
18
+ display: "a-XXXXXX",
19
+ },
16
20
  });
17
21
  function buildFlagsWithDescription(appName) {
18
22
  return {
@@ -33,6 +33,10 @@ export type FlagSet<TName extends ContextNames> = {
33
33
  export type FlagSetOptions = {
34
34
  normalize: NormalizeFn;
35
35
  displayName: string;
36
+ expectedShortIDFormat: {
37
+ pattern: RegExp;
38
+ display: string;
39
+ };
36
40
  };
37
41
  export type NormalizeFn = (apiClient: MittwaldAPIV2Client, id: string) => string | Promise<string>;
38
42
  export declare function makeMissingContextInputError<TName extends ContextNames>(commandType: {
@@ -1,5 +1,8 @@
1
1
  import { Args, Flags } from "@oclif/core";
2
2
  import { Context } from "./context.js";
3
+ import UnexpectedShortIDPassedError from "./error/UnexpectedShortIDPassedError.js";
4
+ import { isUuid } from "../normalize_id.js";
5
+ import { articleForWord } from "./language.js";
3
6
  export class MissingFlagError extends Error {
4
7
  constructor(name, flagName) {
5
8
  super(`No ${name} ID given. Please specify one with --${flagName} or set a default ${name} with 'mittwald context set --${flagName} <${flagName}>'`);
@@ -30,7 +33,7 @@ export function makeMissingContextInputError(commandType, name, flagName, contex
30
33
  }
31
34
  export function makeFlagSet(name, char, opts = {}) {
32
35
  const { displayName = name, normalize = (_, id) => id } = opts;
33
- const article = displayName.match(/^[aeiou]/i) ? "an" : "a";
36
+ const article = articleForWord(displayName);
34
37
  const flagName = `${name}-id`;
35
38
  const flags = {
36
39
  [flagName]: Flags.string({
@@ -55,9 +58,19 @@ export function makeFlagSet(name, char, opts = {}) {
55
58
  }
56
59
  return undefined;
57
60
  };
61
+ let idInputSanityCheck = () => { };
62
+ if (opts.expectedShortIDFormat != null) {
63
+ const format = opts.expectedShortIDFormat;
64
+ idInputSanityCheck = (id) => {
65
+ if (!isUuid(id) && !format.pattern.test(id)) {
66
+ throw new UnexpectedShortIDPassedError(displayName, format.display);
67
+ }
68
+ };
69
+ }
58
70
  const withId = async (apiClient, commandType, flags, args, cfg) => {
59
71
  const idInput = idFromArgsOrFlag(flags, args);
60
72
  if (idInput) {
73
+ idInputSanityCheck(idInput);
61
74
  return normalize(apiClient, idInput);
62
75
  }
63
76
  const idFromContext = await new Context(apiClient, cfg).getContextValue(flagName);
@@ -1,23 +1,31 @@
1
1
  import { ProcessRenderer } from "../../../rendering/process/process.js";
2
- import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
3
- export declare function getConnectionDetailsWithPassword(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: {
2
+ import type { MittwaldAPIV2 } from "@mittwald/api-client";
3
+ import { MittwaldAPIV2Client } from "@mittwald/api-client";
4
+ type Project = MittwaldAPIV2.Components.Schemas.ProjectProject;
5
+ export interface MySQLConnectionFlags {
4
6
  "mysql-password": string | undefined;
5
7
  "temporary-user"?: boolean;
6
8
  "ssh-user"?: string;
7
- }): Promise<{
8
- password: string;
9
+ }
10
+ export interface MySQLConnectionDetails {
9
11
  hostname: string;
10
12
  database: string;
11
13
  user: string;
12
14
  sshHost: string;
13
15
  sshUser: string;
14
- project: MittwaldAPIV2.Components.Schemas.ProjectProject;
15
- }>;
16
- export declare function getConnectionDetails(apiClient: MittwaldAPIV2Client, databaseId: string, sshUser: string | undefined, p: ProcessRenderer): Promise<{
17
- hostname: string;
18
- database: string;
19
- user: string;
20
- sshHost: string;
21
- sshUser: string;
22
- project: MittwaldAPIV2.Components.Schemas.ProjectProject;
23
- }>;
16
+ project: Project;
17
+ }
18
+ export type MySQLConnectionDetailsWithPassword = MySQLConnectionDetails & {
19
+ password: string;
20
+ };
21
+ /**
22
+ * Runs a callback function with connection details for a MySQL database.
23
+ *
24
+ * Depending on the flags, this function will either use the credentials
25
+ * provided in the flags (or prompt for a password), or create a temporary user
26
+ * for the operation, which will be cleaned up afterwards.
27
+ */
28
+ export declare function runWithConnectionDetails<TRes>(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: MySQLConnectionFlags, cb: (connectionDetails: MySQLConnectionDetailsWithPassword) => Promise<TRes>): Promise<TRes>;
29
+ export declare function getConnectionDetailsWithPassword(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: MySQLConnectionFlags): Promise<MySQLConnectionDetailsWithPassword>;
30
+ export declare function getConnectionDetails(apiClient: MittwaldAPIV2Client, databaseId: string, sshUser: string | undefined, p: ProcessRenderer): Promise<MySQLConnectionDetails>;
31
+ export {};