@mittwald/cli 1.0.0-alpha.43 → 1.0.0-alpha.45

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 CHANGED
@@ -136,8 +136,10 @@ USAGE
136
136
  * [`mw app install typo3`](#mw-app-install-typo3)
137
137
  * [`mw app install wordpress`](#mw-app-install-wordpress)
138
138
  * [`mw app list`](#mw-app-list)
139
+ * [`mw app list-upgrade-candidates [INSTALLATION-ID]`](#mw-app-list-upgrade-candidates-installation-id)
139
140
  * [`mw app ssh [INSTALLATION-ID]`](#mw-app-ssh-installation-id)
140
141
  * [`mw app uninstall [INSTALLATION-ID]`](#mw-app-uninstall-installation-id)
142
+ * [`mw app upgrade [INSTALLATION-ID]`](#mw-app-upgrade-installation-id)
141
143
  * [`mw app upload [INSTALLATION-ID]`](#mw-app-upload-installation-id)
142
144
  * [`mw app versions [APP]`](#mw-app-versions-app)
143
145
  * [`mw autocomplete [SHELL]`](#mw-autocomplete-shell)
@@ -1751,6 +1753,33 @@ FLAG DESCRIPTIONS
1751
1753
  to persistently set a default project for all commands that accept this flag.
1752
1754
  ```
1753
1755
 
1756
+ ## `mw app list-upgrade-candidates [INSTALLATION-ID]`
1757
+
1758
+ List upgrade candidates for an app installation.
1759
+
1760
+ ```
1761
+ USAGE
1762
+ $ mw app list-upgrade-candidates [INSTALLATION-ID] [--columns <value> | -x] [--no-header | [--csv | --no-truncate]] [-o
1763
+ txt|json|yaml|csv | | ] [--no-relative-dates]
1764
+
1765
+ ARGUMENTS
1766
+ INSTALLATION-ID ID or short ID of an app installation; this argument is optional if a default app installation is set
1767
+ in the context
1768
+
1769
+ FLAGS
1770
+ -o, --output=<option> [default: txt] output in a more machine friendly format
1771
+ <options: txt|json|yaml|csv>
1772
+ -x, --extended show extra columns
1773
+ --columns=<value> only show provided columns (comma-separated)
1774
+ --csv output is csv format [alias: --output=csv]
1775
+ --no-header hide table header from output
1776
+ --no-relative-dates show dates in absolute format, not relative
1777
+ --no-truncate do not truncate output to fit screen
1778
+
1779
+ DESCRIPTION
1780
+ List upgrade candidates for an app installation.
1781
+ ```
1782
+
1754
1783
  ## `mw app ssh [INSTALLATION-ID]`
1755
1784
 
1756
1785
  Connect to an app via SSH
@@ -1826,6 +1855,42 @@ FLAG DESCRIPTIONS
1826
1855
  scripts), you can use this flag to easily get the IDs of created resources for further processing.
1827
1856
  ```
1828
1857
 
1858
+ ## `mw app upgrade [INSTALLATION-ID]`
1859
+
1860
+ Upgrade app installation to target version
1861
+
1862
+ ```
1863
+ USAGE
1864
+ $ mw app upgrade [INSTALLATION-ID] [--target-version <value>] [-w] [-f] [-p <value>] [-q]
1865
+
1866
+ ARGUMENTS
1867
+ INSTALLATION-ID ID or short ID of an app installation; this argument is optional if a default app installation is set
1868
+ in the context
1869
+
1870
+ FLAGS
1871
+ -f, --force Do not ask for confirmation.
1872
+ -p, --project-id=<value> ID or short ID of a project; this flag is optional if a default project is set in the
1873
+ context
1874
+ -q, --quiet suppress process output and only display a machine-readable summary.
1875
+ -w, --wait wait for the upgrade process to finish
1876
+ --target-version=<value> target version to upgrade app to; if omitted, target version will be prompted
1877
+ interactively
1878
+
1879
+ DESCRIPTION
1880
+ Upgrade app installation to target version
1881
+
1882
+ FLAG DESCRIPTIONS
1883
+ -p, --project-id=<value> ID or short ID of a project; this flag is optional if a default project is set in the context
1884
+
1885
+ May contain a short ID or a full ID of a project; you can also use the "mw context set --project-id=<VALUE>" command
1886
+ to persistently set a default project for all commands that accept this flag.
1887
+
1888
+ -q, --quiet suppress process output and only display a machine-readable summary.
1889
+
1890
+ This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
1891
+ scripts), you can use this flag to easily get the IDs of created resources for further processing.
1892
+ ```
1893
+
1829
1894
  ## `mw app upload [INSTALLATION-ID]`
1830
1895
 
1831
1896
  Upload the filesystem of an app to a project
@@ -1932,7 +1997,7 @@ EXAMPLES
1932
1997
  $ mw autocomplete --refresh-cache
1933
1998
  ```
1934
1999
 
1935
- _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.16/src/commands/autocomplete/index.ts)_
2000
+ _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.18/src/commands/autocomplete/index.ts)_
1936
2001
 
1937
2002
  ## `mw backup create`
1938
2003
 
@@ -3703,7 +3768,7 @@ DESCRIPTION
3703
3768
  Display help for mw.
3704
3769
  ```
3705
3770
 
3706
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.21/src/commands/help.ts)_
3771
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.0.22/src/commands/help.ts)_
3707
3772
 
3708
3773
  ## `mw login reset`
3709
3774
 
@@ -5200,7 +5265,7 @@ EXAMPLES
5200
5265
  $ mw update --available
5201
5266
  ```
5202
5267
 
5203
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.2.8/src/commands/update.ts)_
5268
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.2.11/src/commands/update.ts)_
5204
5269
 
5205
5270
  ## `mw user api-token create`
5206
5271
 
@@ -3,6 +3,7 @@ import { MittwaldAPIV2Client } from "@mittwald/api-client";
3
3
  import { configureAxiosRetry } from "./lib/api_retry.js";
4
4
  import { configureConsistencyHandling } from "./lib/api_consistency.js";
5
5
  import { getTokenFilename, readApiToken } from "./lib/auth/token.js";
6
+ import { configureAxiosLogging } from "./lib/api_logging.js";
6
7
  export class BaseCommand extends Command {
7
8
  authenticationRequired = true;
8
9
  apiClient = MittwaldAPIV2Client.newUnauthenticated();
@@ -16,6 +17,7 @@ export class BaseCommand extends Command {
16
17
  this.apiClient = MittwaldAPIV2Client.newWithToken(token);
17
18
  this.apiClient.axios.defaults.headers["User-Agent"] =
18
19
  `mittwald-cli/${this.config.version}`;
20
+ configureAxiosLogging(this.apiClient.axios);
19
21
  configureAxiosRetry(this.apiClient.axios);
20
22
  configureConsistencyHandling(this.apiClient.axios);
21
23
  }
@@ -0,0 +1,40 @@
1
+ import { Simplify } from "@mittwald/api-client-commons";
2
+ import { ListBaseCommand } from "../../ListBaseCommand.js";
3
+ import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
4
+ import { ListColumns } from "../../Formatter.js";
5
+ type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2AppsAppIdVersions.Get.Responses.$200.Content.ApplicationJson[number]>;
6
+ type Response = Awaited<ReturnType<MittwaldAPIV2Client["app"]["listAppversions"]>>;
7
+ export default class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
8
+ static description: string;
9
+ static args: {
10
+ "installation-id": import("@oclif/core/lib/interfaces/parser.js").Arg<string>;
11
+ };
12
+ static flags: {
13
+ columns?: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined> | undefined;
14
+ csv?: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean> | undefined;
15
+ extended?: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean> | undefined;
16
+ filter?: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined> | undefined;
17
+ 'no-header'?: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean> | undefined;
18
+ 'no-truncate'?: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean> | undefined;
19
+ output: import("@oclif/core/lib/interfaces/parser.js").FlagProps & {
20
+ type: "option";
21
+ helpValue?: string | undefined;
22
+ options?: readonly string[] | undefined;
23
+ multiple?: boolean | undefined;
24
+ multipleNonGreedy?: boolean | undefined;
25
+ delimiter?: "," | undefined;
26
+ allowStdin?: boolean | "only" | undefined;
27
+ } & {
28
+ parse: import("@oclif/core/lib/interfaces/parser.js").FlagParser<string | undefined, string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
29
+ defaultHelp?: import("@oclif/core/lib/interfaces/parser.js").FlagDefaultHelp<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
30
+ input: string[];
31
+ default?: import("@oclif/core/lib/interfaces/parser.js").FlagDefault<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
32
+ };
33
+ sort?: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined> | undefined;
34
+ "no-relative-dates": import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
35
+ };
36
+ protected getData(): Promise<Response>;
37
+ protected mapData(data: ResponseItem[]): Promise<ResponseItem[]>;
38
+ protected getColumns(): ListColumns<ResponseItem>;
39
+ }
40
+ export {};
@@ -0,0 +1,37 @@
1
+ import { appInstallationArgs, withAppInstallationId, } from "../../lib/app/flags.js";
2
+ import { ListBaseCommand } from "../../ListBaseCommand.js";
3
+ import { getAppInstallationFromUuid } from "../../lib/app/uuid.js";
4
+ import { sortArrayByExternalVersion } from "../../lib/app/versions.js";
5
+ export default class List extends ListBaseCommand {
6
+ static description = "List upgrade candidates for an app installation.";
7
+ static args = {
8
+ ...appInstallationArgs,
9
+ };
10
+ static flags = {
11
+ ...ListBaseCommand.baseFlags,
12
+ };
13
+ async getData() {
14
+ const appInstallationId = await withAppInstallationId(this.apiClient, List, this.flags, this.args, this.config);
15
+ const currentAppInstallation = await getAppInstallationFromUuid(this.apiClient, appInstallationId);
16
+ if (currentAppInstallation.appVersion.current === undefined) {
17
+ throw new Error("current app version could not be determined");
18
+ }
19
+ return await this.apiClient.app.listUpdateCandidatesForAppversion({
20
+ appId: currentAppInstallation.appId,
21
+ baseAppVersionId: currentAppInstallation.appVersion.current,
22
+ });
23
+ }
24
+ async mapData(data) {
25
+ return sortArrayByExternalVersion(data);
26
+ }
27
+ getColumns() {
28
+ return {
29
+ externalVersion: {
30
+ header: "Version",
31
+ get: (i) => {
32
+ return i.externalVersion;
33
+ },
34
+ },
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,17 @@
1
+ import { ExecRenderBaseCommand } from "../../rendering/react/ExecRenderBaseCommand.js";
2
+ import { ReactNode } from "react";
3
+ export declare class UpgradeApp extends ExecRenderBaseCommand<typeof UpgradeApp, void> {
4
+ static description: string;
5
+ static args: {
6
+ "installation-id": import("@oclif/core/lib/interfaces/parser.js").Arg<string>;
7
+ };
8
+ static flags: {
9
+ quiet: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
+ "project-id": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string>;
11
+ "target-version": import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
+ wait: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
+ force: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
+ };
15
+ protected exec(): Promise<void>;
16
+ protected render(): ReactNode;
17
+ }
@@ -0,0 +1,108 @@
1
+ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { ExecRenderBaseCommand } from "../../rendering/react/ExecRenderBaseCommand.js";
3
+ import { appInstallationArgs, withAppInstallationId, } from "../../lib/app/flags.js";
4
+ import { projectFlags } from "../../lib/project/flags.js";
5
+ import { Flags, ux } from "@oclif/core";
6
+ import { Text } from "ink";
7
+ import { getAppFromUuid, getAppInstallationFromUuid, getAppVersionFromUuid, } from "../../lib/app/uuid.js";
8
+ import { getAllUpgradeCandidatesFromAppInstallationId, getAvailableTargetAppVersionFromExternalVersion, getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates, } from "../../lib/app/versions.js";
9
+ import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
10
+ import { Success } from "../../rendering/react/components/Success.js";
11
+ import { waitUntilAppStateHasNormalized } from "../../lib/app/wait.js";
12
+ import { assertStatus } from "@mittwald/api-client-commons";
13
+ export class UpgradeApp extends ExecRenderBaseCommand {
14
+ static description = "Upgrade app installation to target version";
15
+ static args = {
16
+ ...appInstallationArgs,
17
+ };
18
+ static flags = {
19
+ "target-version": Flags.string({
20
+ description: "target version to upgrade app to; if omitted, target version will be prompted interactively",
21
+ }),
22
+ wait: Flags.boolean({
23
+ description: "wait for the upgrade process to finish",
24
+ char: "w",
25
+ }),
26
+ force: Flags.boolean({
27
+ char: "f",
28
+ description: "Do not ask for confirmation.",
29
+ }),
30
+ ...projectFlags,
31
+ ...processFlags,
32
+ };
33
+ async exec() {
34
+ const process = makeProcessRenderer(this.flags, "App upgrade");
35
+ const appInstallationId = await withAppInstallationId(this.apiClient, UpgradeApp, this.flags, this.args, this.config), currentAppInstallation = await getAppInstallationFromUuid(this.apiClient, appInstallationId), currentApp = await getAppFromUuid(this.apiClient, currentAppInstallation.appId), targetAppVersionCandidates = await getAllUpgradeCandidatesFromAppInstallationId(this.apiClient, currentAppInstallation.id);
36
+ if (currentAppInstallation.appVersion.current === undefined) {
37
+ process.error("Current version could not be determined properly.");
38
+ ux.exit(1);
39
+ }
40
+ const currentAppVersion = await getAppVersionFromUuid(this.apiClient, currentApp.id, currentAppInstallation.appVersion.current);
41
+ if (targetAppVersionCandidates.length == 0) {
42
+ process.addInfo(_jsxs(Text, { children: ["Your ", currentApp.name, " ", currentAppVersion.externalVersion, " is already Up-To-Date. \u2705"] }));
43
+ process.complete(_jsx(_Fragment, {}));
44
+ ux.exit(0);
45
+ }
46
+ let targetAppVersion;
47
+ if (this.flags["target-version"] == "latest") {
48
+ targetAppVersion =
49
+ await getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(this.apiClient, currentApp.id, currentAppVersion.id);
50
+ }
51
+ else if (this.flags["target-version"]) {
52
+ const targetVersionMatchFromCandidates = targetAppVersionCandidates.find((targetAppVersionCandidate) => targetAppVersionCandidate.externalVersion ===
53
+ this.flags["target-version"]);
54
+ if (targetVersionMatchFromCandidates) {
55
+ targetAppVersion = targetVersionMatchFromCandidates;
56
+ }
57
+ else {
58
+ process.addInfo(_jsx(Text, { children: "The given target upgrade version does not seem to be a valid upgrade candidate." }));
59
+ targetAppVersion = await forceTargetVersionSelection(process, this.apiClient, targetAppVersionCandidates, currentApp, currentAppVersion);
60
+ }
61
+ }
62
+ else {
63
+ targetAppVersion = await forceTargetVersionSelection(process, this.apiClient, targetAppVersionCandidates, currentApp, currentAppVersion);
64
+ }
65
+ if (!targetAppVersion) {
66
+ process.error("Target app version could not be determined properly.");
67
+ return;
68
+ }
69
+ if (!this.flags.force) {
70
+ const confirmed = await process.addConfirmation(_jsxs(Text, { children: ["Confirm upgrading ", currentApp.name, " ", currentAppVersion.externalVersion, " (", currentAppInstallation.description, ") to version", " ", targetAppVersion.externalVersion] }));
71
+ if (!confirmed) {
72
+ process.addInfo(_jsx(Text, { children: "Upgrade will not be triggered." }));
73
+ process.complete(_jsx(_Fragment, {}));
74
+ ux.exit(1);
75
+ }
76
+ }
77
+ else {
78
+ process.addInfo(_jsxs(Text, { children: ["Commencing upgrade of ", currentApp.name, " ", currentAppVersion.externalVersion, " (", currentAppInstallation.description, ") to Version", " ", targetAppVersion.externalVersion, "."] }));
79
+ }
80
+ const patchAppTriggerResponse = await this.apiClient.app.patchAppinstallation({
81
+ appInstallationId,
82
+ data: { appVersionId: targetAppVersion.id },
83
+ });
84
+ assertStatus(patchAppTriggerResponse, 204);
85
+ let successText;
86
+ if (this.flags.wait) {
87
+ await waitUntilAppStateHasNormalized(this.apiClient, process, appInstallationId, "waiting for app upgrade to be done");
88
+ successText =
89
+ "The upgrade finished successfully. Please check if everything is in its place. 🔎";
90
+ }
91
+ else {
92
+ successText = "The upgrade has been started. Buckle up! 🚀";
93
+ }
94
+ await process.complete(_jsx(Success, { children: successText }));
95
+ }
96
+ render() {
97
+ return true;
98
+ }
99
+ }
100
+ async function forceTargetVersionSelection(process, apiClient, targetAppVersionCandidates, currentApp, currentAppVersion) {
101
+ const targetAppVersionString = await process.addSelect(`Please select target upgrade for your ${currentApp.name} ${currentAppVersion.externalVersion} from one of the following`, [
102
+ ...targetAppVersionCandidates.map((targetAppVersionCandidate) => ({
103
+ value: targetAppVersionCandidate.externalVersion,
104
+ label: `${targetAppVersionCandidate.externalVersion}`,
105
+ })),
106
+ ]);
107
+ return await getAvailableTargetAppVersionFromExternalVersion(apiClient, currentApp.id, currentAppVersion.id, targetAppVersionString);
108
+ }
@@ -0,0 +1,8 @@
1
+ import { AxiosInstance } from "@mittwald/api-client-commons";
2
+ /**
3
+ * Configure logging for Axios requests and responses using the `debug` module.
4
+ *
5
+ * Run the CLI with `DEBUG=mw:api:client:*` to see the logs. Keep in mind that
6
+ * this will also log sensitive information in request bodies or headers.
7
+ */
8
+ export declare function configureAxiosLogging(axios: AxiosInstance): void;
@@ -0,0 +1,20 @@
1
+ import debug from "debug";
2
+ /**
3
+ * Configure logging for Axios requests and responses using the `debug` module.
4
+ *
5
+ * Run the CLI with `DEBUG=mw:api:client:*` to see the logs. Keep in mind that
6
+ * this will also log sensitive information in request bodies or headers.
7
+ */
8
+ export function configureAxiosLogging(axios) {
9
+ const baseDebugger = debug("mw:api:client");
10
+ const reqDebugger = baseDebugger.extend("request");
11
+ const resDebugger = baseDebugger.extend("response");
12
+ axios.interceptors.request.use((config) => {
13
+ reqDebugger("%s %s requested with %O", config.method?.toUpperCase(), config.url, config.data);
14
+ return config;
15
+ });
16
+ axios.interceptors.response.use((response) => {
17
+ resDebugger("%s %s responded with %o %O", response.config.method?.toUpperCase(), response.config.url, response.status + " " + response.statusText, response.data);
18
+ return response;
19
+ });
20
+ }
@@ -4,7 +4,7 @@ import { withProjectId } from "../project/flags.js";
4
4
  import { autofillFlags, provideSupportedFlags, } from "./flags.js";
5
5
  import { normalizeToAppVersionUuid } from "./versions.js";
6
6
  import { triggerAppInstallation } from "./install.js";
7
- import { waitUntilAppIsInstalled } from "./wait.js";
7
+ import { waitUntilAppStateHasNormalized } from "./wait.js";
8
8
  import { Success } from "../../rendering/react/components/Success.js";
9
9
  export class AppInstaller {
10
10
  appId;
@@ -36,7 +36,7 @@ export class AppInstaller {
36
36
  const appInstallationId = await triggerAppInstallation(apiClient, process, projectId, flags, appVersion);
37
37
  let successText;
38
38
  if (flags.wait) {
39
- await waitUntilAppIsInstalled(apiClient, process, appInstallationId);
39
+ await waitUntilAppStateHasNormalized(apiClient, process, appInstallationId, "waiting for app installation to be ready");
40
40
  successText = `Your ${this.appName} installation is now complete. Have fun! 🎉`;
41
41
  }
42
42
  else {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
2
+ type AppAppInstallation = MittwaldAPIV2.Components.Schemas.AppAppInstallation;
2
3
  type AppAppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
3
4
  type AppApp = MittwaldAPIV2.Components.Schemas.AppApp;
4
5
  /**
@@ -16,6 +17,13 @@ export declare function getAppFromUuid(apiClient: MittwaldAPIV2Client, appId: st
16
17
  * @param appVersionId
17
18
  */
18
19
  export declare function getAppVersionFromUuid(apiClient: MittwaldAPIV2Client, appId: string, appVersionId: string): Promise<AppAppVersion>;
20
+ /**
21
+ * Lookup an appInstallation by its UUID
22
+ *
23
+ * @param apiClient
24
+ * @param appInstallationId
25
+ */
26
+ export declare function getAppInstallationFromUuid(apiClient: MittwaldAPIV2Client, appInstallationId: string): Promise<AppAppInstallation>;
19
27
  /**
20
28
  * Lookup an app by its human readable name
21
29
  *
@@ -1,4 +1,3 @@
1
- import { isUuid } from "../../normalize_id.js";
2
1
  import { assertStatus, } from "@mittwald/api-client";
3
2
  /**
4
3
  * Lookup an app by its UUID
@@ -19,9 +18,6 @@ export async function getAppFromUuid(apiClient, appId) {
19
18
  * @param appVersionId
20
19
  */
21
20
  export async function getAppVersionFromUuid(apiClient, appId, appVersionId) {
22
- if (!isUuid(appId) && !isUuid(appVersionId)) {
23
- throw new Error("Given UUID not valid.");
24
- }
25
21
  const appVersion = await apiClient.app.getAppversion({
26
22
  appId: appId,
27
23
  appVersionId: appVersionId,
@@ -29,6 +25,19 @@ export async function getAppVersionFromUuid(apiClient, appId, appVersionId) {
29
25
  assertStatus(appVersion, 200);
30
26
  return appVersion.data;
31
27
  }
28
+ /**
29
+ * Lookup an appInstallation by its UUID
30
+ *
31
+ * @param apiClient
32
+ * @param appInstallationId
33
+ */
34
+ export async function getAppInstallationFromUuid(apiClient, appInstallationId) {
35
+ const appInstallation = await apiClient.app.getAppinstallation({
36
+ appInstallationId: appInstallationId,
37
+ });
38
+ assertStatus(appInstallation, 200);
39
+ return appInstallation.data;
40
+ }
32
41
  /**
33
42
  * Convert an app name in a format suitable for fuzzy comparison (ignore casing,
34
43
  * punctuation, etc.)
@@ -3,5 +3,9 @@ import { ProcessRenderer } from "../../rendering/process/process.js";
3
3
  type AppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
4
4
  export declare function normalizeToAppVersionUuid(apiClient: MittwaldAPIV2Client, version: string, process: ProcessRenderer, appUuid: string): Promise<MittwaldAPIV2.Components.Schemas.AppAppVersion>;
5
5
  export declare function getLatestAvailableAppVersionForApp(apiClient: MittwaldAPIV2Client, appId: string): Promise<AppVersion | undefined>;
6
+ export declare function getAllUpgradeCandidatesFromAppInstallationId(apiClient: MittwaldAPIV2Client, appInstallationId: string): Promise<AppVersion[]>;
7
+ export declare function getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(apiClient: MittwaldAPIV2Client, appId: string, baseAppVersionId: string): Promise<AppVersion | undefined>;
8
+ export declare function getAvailableTargetAppVersionFromExternalVersion(apiClient: MittwaldAPIV2Client, appId: string, baseAppVersionId: string, targetExternalVersion: string): Promise<AppVersion | undefined>;
6
9
  export declare function getAppVersionUuidFromAppVersion(apiClient: MittwaldAPIV2Client, appId: string, appVersion: string | undefined): Promise<AppVersion | undefined>;
10
+ export declare function sortArrayByExternalVersion(versions: AppVersion[]): AppVersion[];
7
11
  export {};
@@ -3,7 +3,8 @@ import { assertStatus } from "@mittwald/api-client-commons";
3
3
  import { gt } from "semver";
4
4
  import { Value } from "../../rendering/react/components/Value.js";
5
5
  import { Text } from "ink";
6
- import { getAppNameFromUuid } from "./uuid.js";
6
+ import { getAppInstallationFromUuid, getAppNameFromUuid } from "./uuid.js";
7
+ import { compare } from "semver";
7
8
  export async function normalizeToAppVersionUuid(apiClient, version, process, appUuid) {
8
9
  let appVersion;
9
10
  if (version && version !== "latest") {
@@ -35,7 +36,39 @@ export async function getLatestAvailableAppVersionForApp(apiClient, appId) {
35
36
  }
36
37
  return versions.data.find((item) => item.internalVersion === latestVersion);
37
38
  }
38
- // App Version UUID from App Version irellevant if internal or external
39
+ export async function getAllUpgradeCandidatesFromAppInstallationId(apiClient, appInstallationId) {
40
+ const currentAppInstallation = await getAppInstallationFromUuid(apiClient, appInstallationId), updateCandidates = await apiClient.app.listUpdateCandidatesForAppversion({
41
+ appId: currentAppInstallation.appId,
42
+ baseAppVersionId: currentAppInstallation.appVersion.current,
43
+ });
44
+ assertStatus(updateCandidates, 200);
45
+ return sortArrayByExternalVersion(updateCandidates.data);
46
+ }
47
+ export async function getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(apiClient, appId, baseAppVersionId) {
48
+ const versions = await apiClient.app.listUpdateCandidatesForAppversion({
49
+ appId,
50
+ baseAppVersionId,
51
+ });
52
+ assertStatus(versions, 200);
53
+ if (versions.data.length === 0) {
54
+ return undefined;
55
+ }
56
+ let latestVersion;
57
+ for (const version of versions.data) {
58
+ if (gt(version.internalVersion, latestVersion?.internalVersion ?? "0.0.0")) {
59
+ latestVersion = version;
60
+ }
61
+ }
62
+ return latestVersion;
63
+ }
64
+ export async function getAvailableTargetAppVersionFromExternalVersion(apiClient, appId, baseAppVersionId, targetExternalVersion) {
65
+ const versions = await apiClient.app.listUpdateCandidatesForAppversion({
66
+ appId,
67
+ baseAppVersionId,
68
+ });
69
+ assertStatus(versions, 200);
70
+ return versions.data.find((item) => item.externalVersion === targetExternalVersion);
71
+ }
39
72
  export async function getAppVersionUuidFromAppVersion(apiClient, appId, appVersion) {
40
73
  const versions = await apiClient.app.listAppversions({
41
74
  appId,
@@ -47,3 +80,6 @@ export async function getAppVersionUuidFromAppVersion(apiClient, appId, appVersi
47
80
  return versions.data.find((item) => item.internalVersion === appVersion ||
48
81
  item.externalVersion === appVersion);
49
82
  }
83
+ export function sortArrayByExternalVersion(versions) {
84
+ return versions.sort((a, b) => compare(b.externalVersion, a.externalVersion));
85
+ }
@@ -1,3 +1,3 @@
1
1
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
2
  import { ProcessRenderer } from "../../rendering/process/process.js";
3
- export declare function waitUntilAppIsInstalled(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, appInstallationId: string): Promise<void>;
3
+ export declare function waitUntilAppStateHasNormalized(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, appInstallationId: string, label: string): Promise<void>;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { waitUntil } from "../wait.js";
3
3
  import { Text } from "ink";
4
- export async function waitUntilAppIsInstalled(apiClient, process, appInstallationId) {
5
- const stepWaiting = process.addStep(_jsx(Text, { children: "waiting for app installation to be ready" }));
4
+ export async function waitUntilAppStateHasNormalized(apiClient, process, appInstallationId, label) {
5
+ const stepWaiting = process.addStep(_jsx(Text, { children: label }));
6
6
  await waitUntil(async () => {
7
7
  const installationResponse = await apiClient.app.getAppinstallation({
8
8
  appInstallationId,
@@ -1,6 +1,6 @@
1
- import { ApiClientError } from "@mittwald/api-client-commons";
1
+ import { AxiosError } from "@mittwald/api-client-commons";
2
2
  interface APIErrorProps {
3
- err: ApiClientError;
3
+ err: AxiosError;
4
4
  withStack: boolean;
5
5
  withHTTPMessages: "no" | "body" | "full";
6
6
  }
@@ -1,8 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import ErrorStack from "./ErrorStack.js";
4
4
  import ErrorText from "./ErrorText.js";
5
5
  import ErrorBox from "./ErrorBox.js";
6
+ import { SingleResultTable } from "../SingleResult.js";
7
+ import { Value } from "../Value.js";
6
8
  function RequestHeaders({ headers }) {
7
9
  const lines = headers.trim().split("\r\n");
8
10
  const requestLine = lines.shift();
@@ -19,11 +21,21 @@ function HttpMessages({ err }) {
19
21
  const response = err.response ? (_jsx(Response, { status: err.response.status, statusText: err.response.statusText, body: err.response.data, headers: err.response.headers })) : (_jsx(Text, { children: "no response received" }));
20
22
  return (_jsxs(Box, { marginX: 2, marginY: 1, flexDirection: "column", rowGap: 1, children: [_jsx(RequestHeaders, { headers: err.request._header }), response] }));
21
23
  }
24
+ function ErrorDetails({ err }) {
25
+ const errorBody = err.response?.data;
26
+ return (_jsx(SingleResultTable, { rows: {
27
+ Request: (_jsxs(Value, { bold: true, children: [err.config?.method?.toUpperCase(), " ", err.config?.url] })),
28
+ Response: (_jsxs(Value, { bold: true, children: [err.response?.status, " ", err.response?.statusText] })),
29
+ "Error Type": _jsx(Value, { bold: true, children: errorBody?.type }),
30
+ Message: _jsx(Value, { bold: true, children: errorBody?.message }),
31
+ "Trace ID": _jsx(Value, { bold: true, children: errorBody?.params?.["traceId"] }),
32
+ } }));
33
+ }
22
34
  /**
23
35
  * Render an API client error to the terminal. In the case of an API client
24
36
  * error, the error message will be displayed, as well as (when enabled) the
25
37
  * request and response headers and body.
26
38
  */
27
39
  export default function APIError({ err, withStack, withHTTPMessages, }) {
28
- return (_jsxs(_Fragment, { children: [_jsxs(ErrorBox, { children: [_jsx(ErrorText, { bold: true, underline: true, children: "API CLIENT ERROR" }), _jsxs(ErrorText, { children: ["An error occurred while communicating with the API: ", err.message] }), _jsx(Text, { children: JSON.stringify(err.response?.data, undefined, 2) })] }), withHTTPMessages === "full" ? _jsx(HttpMessages, { err: err }) : undefined, withStack && "stack" in err ? _jsx(ErrorStack, { err: err }) : undefined] }));
40
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(ErrorBox, { children: [_jsx(ErrorText, { bold: true, underline: true, children: "API CLIENT ERROR" }), _jsxs(ErrorText, { children: ["An error occurred while communicating with the API: ", err.message] }), _jsx(ErrorDetails, { err: err }), _jsx(Text, { children: JSON.stringify(err.response?.data, undefined, 2) })] }), withHTTPMessages === "full" ? _jsx(HttpMessages, { err: err }) : undefined, withStack && "stack" in err ? _jsx(ErrorStack, { err: err }) : undefined] }));
29
41
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from "ink";
3
3
  import ErrorStack from "./ErrorStack.js";
4
4
  import ErrorText from "./ErrorText.js";
@@ -9,5 +9,5 @@ const issueURL = "https://github.com/mittwald/cli/issues/new";
9
9
  * have a specific rendering function.
10
10
  */
11
11
  export default function GenericError({ err, withStack, withIssue = true, title = "Error", }) {
12
- return (_jsxs(_Fragment, { children: [_jsxs(ErrorBox, { children: [_jsx(ErrorText, { bold: true, underline: true, children: title.toUpperCase() }), _jsx(ErrorText, { children: "An error occurred while executing this command:" }), _jsx(Box, { marginX: 2, children: _jsx(ErrorText, { children: err.toString() }) }), withIssue ? (_jsxs(ErrorText, { 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] }));
12
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(ErrorBox, { children: [_jsx(ErrorText, { bold: true, underline: true, children: title.toUpperCase() }), _jsx(ErrorText, { children: "An error occurred while executing this command:" }), _jsx(Box, { marginX: 2, children: _jsx(ErrorText, { children: err.toString() }) }), withIssue ? (_jsxs(ErrorText, { 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] }));
13
13
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { FailedFlagValidationError, RequiredArgsError, } from "@oclif/core/lib/parser/errors.js";
3
- import { ApiClientError } from "@mittwald/api-client-commons";
3
+ import { AxiosError } from "@mittwald/api-client-commons";
4
4
  import InteractiveInputRequiredError from "../../../lib/error/InteractiveInputRequiredError.js";
5
5
  import UnexpectedShortIDPassedError from "../../../lib/error/UnexpectedShortIDPassedError.js";
6
6
  import GenericError from "./Error/GenericError.js";
@@ -21,7 +21,7 @@ export const ErrorBox = ({ err }) => {
21
21
  else if (err instanceof RequiredArgsError) {
22
22
  return _jsx(InvalidArgsError, { err: err });
23
23
  }
24
- else if (err instanceof ApiClientError) {
24
+ else if (err instanceof AxiosError) {
25
25
  return _jsx(APIError, { err: err, withStack: true, withHTTPMessages: "body" });
26
26
  }
27
27
  else if (err instanceof InteractiveInputRequiredError) {
@@ -1,5 +1,6 @@
1
1
  import React, { PropsWithChildren } from "react";
2
2
  export type ValueProps = PropsWithChildren<{
3
3
  notSet?: boolean;
4
+ bold?: boolean;
4
5
  }>;
5
6
  export declare const Value: React.FC<ValueProps>;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Text } from "ink";
3
- export const Value = ({ children, notSet }) => {
3
+ export const Value = ({ children, notSet, bold }) => {
4
4
  if (notSet || children === undefined) {
5
5
  return _jsx(Text, { color: "gray", children: "not set" });
6
6
  }
7
- return _jsx(Text, { color: "blue", children: children });
7
+ return (_jsx(Text, { color: "blue", bold: bold, children: children }));
8
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.0.0-alpha.43",
3
+ "version": "1.0.0-alpha.45",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {