diff --git a/README.md b/README.md index 558a96c..b3b410f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ $ npm install -g @contentstack/apps-cli $ csdx COMMAND running command... $ csdx (--version|-v) -@contentstack/apps-cli/1.0.7 darwin-arm64 node-v18.16.0 +@contentstack/apps-cli/1.1.0 darwin-arm64 node-v18.16.0 $ csdx --help [COMMAND] USAGE $ csdx COMMAND @@ -35,6 +35,7 @@ USAGE * [`csdx app:delete`](#csdx-appdelete) * [`csdx app:get`](#csdx-appget) * [`csdx app:install`](#csdx-appinstall) +* [`csdx app:reinstall`](#csdx-appreinstall) * [`csdx app:uninstall`](#csdx-appuninstall) * [`csdx app:update`](#csdx-appupdate) @@ -61,9 +62,11 @@ EXAMPLES $ csdx app:install $ csdx app:uninstall + + $ csdx app:reinstall ``` -_See code: [src/commands/app/index.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/index.ts)_ +_See code: [src/commands/app/index.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/index.ts)_ ## `csdx app:create` @@ -94,7 +97,7 @@ EXAMPLES $ csdx app:create --name App-3 --app-type organization --org -d ./boilerplate -c ./external-config.json ``` -_See code: [src/commands/app/create.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/create.ts)_ +_See code: [src/commands/app/create.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/create.ts)_ ## `csdx app:delete` @@ -119,7 +122,7 @@ EXAMPLES $ csdx app:delete --app-uid --org -d ./boilerplate ``` -_See code: [src/commands/app/delete.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/delete.ts)_ +_See code: [src/commands/app/delete.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/delete.ts)_ ## `csdx app:get` @@ -149,7 +152,7 @@ EXAMPLES $ csdx app:get --org --app-uid --app-type organization ``` -_See code: [src/commands/app/get.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/get.ts)_ +_See code: [src/commands/app/get.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/get.ts)_ ## `csdx app:install` @@ -175,7 +178,33 @@ EXAMPLES $ csdx app:install --org --app-uid --stack-api-key ``` -_See code: [src/commands/app/install.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/install.ts)_ +_See code: [src/commands/app/install.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/install.ts)_ + +## `csdx app:reinstall` + +Reinstall an app from the marketplace + +``` +USAGE + $ csdx app:reinstall [--org ] [--app-uid ] [--stack-api-key ] + +FLAGS + --app-uid= Provide the app UID of an existing app. + --org= Provide the organization UID to fetch the app details for the desired operation. + --stack-api-key= API key of the stack where the app is to be installed. + +DESCRIPTION + Reinstall an app from the marketplace + +EXAMPLES + $ csdx app:reinstall + + $ csdx app:reinstall --org --app-uid + + $ csdx app:reinstall --org --app-uid --stack-api-key +``` + +_See code: [src/commands/app/reinstall.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/reinstall.ts)_ ## `csdx app:uninstall` @@ -202,7 +231,7 @@ EXAMPLES $ csdx app:uninstall --org --app-uid --installation-uid ``` -_See code: [src/commands/app/uninstall.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/uninstall.ts)_ +_See code: [src/commands/app/uninstall.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/uninstall.ts)_ ## `csdx app:update` @@ -225,5 +254,5 @@ EXAMPLES $ csdx app:update --app-manifest ./boilerplate/manifest.json ``` -_See code: [src/commands/app/update.ts](https://github.com/contentstack/apps-cli/blob/v1.0.7/src/commands/app/update.ts)_ +_See code: [src/commands/app/update.ts](https://github.com/contentstack/apps-cli/blob/v1.1.0/src/commands/app/update.ts)_ diff --git a/package-lock.json b/package-lock.json index e7c77fb..08f149f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/apps-cli", - "version": "1.0.7", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/apps-cli", - "version": "1.0.7", + "version": "1.1.0", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.17", diff --git a/package.json b/package.json index a6d89e6..8023f8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/apps-cli", - "version": "1.0.7", + "version": "1.1.0", "description": "App ClI", "author": "Contentstack CLI", "homepage": "https://github.com/contentstack/contentstack-apps-cli", @@ -96,7 +96,8 @@ "app:update": "APUPT", "app:delete": "APDLT", "app:install": "API", - "app:uninstall": "APUI" + "app:uninstall": "APUI", + "app:reinstall": "APRI" } } } \ No newline at end of file diff --git a/src/commands/app/index.ts b/src/commands/app/index.ts index a70bf4c..6caeef9 100644 --- a/src/commands/app/index.ts +++ b/src/commands/app/index.ts @@ -14,6 +14,7 @@ export default class App extends Command { "$ <%= config.bin %> <%= command.id %>:delete", "$ <%= config.bin %> <%= command.id %>:install", "$ <%= config.bin %> <%= command.id %>:uninstall", + "$ <%= config.bin %> <%= command.id %>:reinstall", ]; async run(): Promise { diff --git a/src/commands/app/install.ts b/src/commands/app/install.ts index d70965c..9223286 100644 --- a/src/commands/app/install.ts +++ b/src/commands/app/install.ts @@ -122,6 +122,12 @@ export default class Install extends AppCLIBaseCommand { this.displayStackUrl(); } catch (error: any) { this.log(error?.errorMessage || error?.message || error, "error"); + if ( + error?.errorMessage === "Installation for app is already done" && + error?.status === 400 + ) { + this.displayReInstallMsg(); + } this.exit(1); } } @@ -136,4 +142,11 @@ export default class Install extends AppCLIBaseCommand { "info" ); } -} \ No newline at end of file + + /** + * @method displayStackUrl - show guid to stack after installing app successfully in the stack + */ + displayReInstallMsg(): void { + this.log(this.messages.APP_ALREADY_INSTALLED, "info"); + } +} diff --git a/src/commands/app/reinstall.ts b/src/commands/app/reinstall.ts new file mode 100644 index 0000000..d1d49b1 --- /dev/null +++ b/src/commands/app/reinstall.ts @@ -0,0 +1,134 @@ +import { cliux, flags } from "@contentstack/cli-utilities"; +import { AppCLIBaseCommand } from "../../app-cli-base-coomand"; +import { $t, commonMsg, reinstallAppMsg } from "../../messages"; +import { + getOrg, + getApp, + getStack, + reinstallApp, + fetchApp, + fetchStack, +} from "../../util"; + +export default class Reinstall extends AppCLIBaseCommand { + static description: string | undefined = + "Reinstall an app from the marketplace"; + + static examples = [ + "$ <%= config.bin %> <%= command.id %>", + "$ <%= config.bin %> <%= command.id %> --org --app-uid ", + "$ <%= config.bin %> <%= command.id %> --org --app-uid --stack-api-key ", + ]; + + static flags = { + "app-uid": flags.string({ + description: commonMsg.APP_UID, + }), + "stack-api-key": flags.string({ + description: commonMsg.STACK_API_KEY, + }), + }; + + async run(): Promise { + try { + let app, stack, appType; + this.flags["app-uid"] = this.manifestData?.uid ?? this.flags["app-uid"]; + + if (this.flags["stack-api-key"]) { + stack = await fetchStack(this.flags, { + managementSdk: this.managementSdk, + log: this.log, + }); + } + + this.sharedConfig.org = + this.manifestData?.organization_uid ?? + (await getOrg(this.flags, { + managementSdk: this.managementSdk, + log: this.log, + })); + + if (!this.flags["app-uid"]) { + app = await getApp(this.flags, this.sharedConfig.org, { + managementSdk: this.managementAppSdk, + log: this.log, + }); + } else { + app = await fetchApp(this.flags, this.sharedConfig.org, { + managementSdk: this.managementAppSdk, + log: this.log, + }); + } + appType = app?.["target_type"]; + this.flags["app-uid"] = app?.uid; + + if (appType === "organization" && this.flags["stack-api-key"]) { + appType = "organization"; + const confirmation = + this.flags["yes"] || + (await cliux.inquire({ + type: "confirm", + message: $t(reinstallAppMsg.REINSTALL_ORG_APP_TO_STACK, { + app: app?.name || app?.uid, + }), + name: "confirmation", + })); + if (!confirmation) { + throw new Error(commonMsg.USER_TERMINATION); + } + } + + if (appType === "stack" && !this.flags["stack-api-key"]) { + appType = "stack"; + + this.log( + $t(reinstallAppMsg.MISSING_STACK_API_KEY, { + app: app?.name || app?.uid, + }), + "warn" + ); + stack = await getStack(this.sharedConfig.org, { + managementSdk: this.managementSdk, + log: this.log, + }); + this.flags["stack-api-key"] = stack?.["api_key"]; + } + + this.log( + $t(reinstallAppMsg.REINSTALLING_APP_NOTICE, { + app: app?.name || app?.uid, + type: appType, + target: this.flags["stack-api-key"] || this.sharedConfig.org, + }), + "info" + ); + await reinstallApp({ + flags: this.flags, + type: appType, + developerHubBaseUrl: this.developerHubBaseUrl, + orgUid: this.sharedConfig.org, + manifestUid: this.manifestData.uid, + }); + this.log( + $t(reinstallAppMsg.APP_REINSTALLED_SUCCESSFULLY, { + app: app?.name || (this.flags["app-uid"] as string), + target: stack?.name || this.sharedConfig.org, + }), + "info" + ); + + this.displayStackUrl(); + } catch (error: any) { + this.log(error?.errorMessage || error?.message || error, "error"); + this.exit(1); + } + } + + displayStackUrl(): void { + const stackPath = `${this.uiHost}/#!/stack/${this.flags["stack-api-key"]}/dashboard`; + this.log( + `Please use the following URL to start using the stack: ${stackPath}`, + "info" + ); + } +} diff --git a/src/messages/index.ts b/src/messages/index.ts index 1b93122..f42960c 100644 --- a/src/messages/index.ts +++ b/src/messages/index.ts @@ -89,6 +89,7 @@ const installAppMsg = { INSTALL_ORG_APP_TO_STACK: "{app} is an organization app. It cannot be installed to a stack. Do you want to proceed?", MISSING_STACK_API_KEY: "As {app} is a stack app, it can only be installed in a stack. Please select a stack.", INSTALLING_APP_NOTICE: "Installing {app} on {type} {target}.", + APP_ALREADY_INSTALLED: "Please use $ csdx app:reinstall to reinstall the app.", } const uninstallAppMsg = { @@ -100,6 +101,15 @@ const uninstallAppMsg = { UNINSTALL_ALL: "Please select stacks from where the app must be uninstalled.", } +const reinstallAppMsg = { + CHOOSE_A_STACK: "Please select a stack", + APP_REINSTALLED_SUCCESSFULLY: "{app} reinstalled successfully in {target}.", + REINSTALL_ORG_APP_TO_STACK: "{app} is an organization app. It cannot be reinstalled to a stack. Do you want to proceed?", + MISSING_STACK_API_KEY: "As {app} is a stack app, it can only be reinstalled in a stack. Please select a stack.", + REINSTALLING_APP_NOTICE: "Reinstalling {app} on {type} {target}.", + APP_UID: "Provide the app UID of an existing app to be reinstalled.", +} + const messages: typeof errors & typeof commonMsg & typeof appCreate & @@ -107,7 +117,8 @@ const messages: typeof errors & typeof getAppMsg & typeof deleteAppMsg & typeof installAppMsg & - typeof uninstallAppMsg = { + typeof uninstallAppMsg & + typeof reinstallAppMsg = { ...errors, ...commonMsg, ...appCreate, @@ -115,7 +126,8 @@ const messages: typeof errors & ...getAppMsg, ...deleteAppMsg, ...installAppMsg, - ...uninstallAppMsg + ...uninstallAppMsg, + ...reinstallAppMsg }; const $t = (msg: string, args: Record): string => { @@ -130,4 +142,4 @@ const $t = (msg: string, args: Record): string => { }; export default messages; -export { $t, errors, commonMsg, appCreate, appUpdate, getAppMsg, deleteAppMsg, installAppMsg, uninstallAppMsg }; +export { $t, errors, commonMsg, appCreate, appUpdate, getAppMsg, deleteAppMsg, installAppMsg, uninstallAppMsg, reinstallAppMsg }; diff --git a/src/types/app.ts b/src/types/app.ts index 81fc9f9..d2f34f1 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,3 +1,6 @@ +import { FlagInput } from "@contentstack/cli-utilities"; +import { ConfigType } from "./utils"; + export interface TokenConfiguration { enabled?: boolean; scopes?: string[]; @@ -113,3 +116,12 @@ export interface AppManifest { export interface AppManifestWithUiLocation extends AppManifest { ui_location: LocationConfiguration; } + +export interface ReinstallParams { + flags: FlagInput, + type: string, + orgUid: string, + manifestUid: string, + configType: ConfigType, + developerHubBaseUrl: string +}; \ No newline at end of file diff --git a/src/util/api-request-handler.ts b/src/util/api-request-handler.ts new file mode 100644 index 0000000..c064bf1 --- /dev/null +++ b/src/util/api-request-handler.ts @@ -0,0 +1,58 @@ +import { HttpClient, configHandler } from "@contentstack/cli-utilities"; +import { formatErrors } from "./error-helper"; + +interface RequestParams { + orgUid: string; + method: "GET" | "POST" | "PUT" | "DELETE"; + queryParams?: Record; + payload?: any; + url: string; +} + +export async function apiRequestHandler(params: RequestParams): Promise { + const { orgUid, method, queryParams, payload, url } = params; + const authtoken = configHandler.get("authtoken"); + + const headers = { + organization_uid: orgUid, + authtoken, + }; + + const httpClient = new HttpClient(); + httpClient.headers(headers); + + if (queryParams) { + httpClient.queryParams(queryParams); + } + + try { + let response; + switch (method) { + case "GET": + response = await httpClient.get(url); + break; + case "POST": + response = await httpClient.post(url, payload); + break; + case "PUT": + response = await httpClient.put(url, payload); + break; + case "DELETE": + response = await httpClient.delete(url); + break; + default: + throw new Error(`Unsupported HTTP method: ${method}`); + } + + const { status, data } = response; + if (status >= 200 && status < 300) { + return data; + } + const errorMessage = data?.error + ? formatErrors(data) + : data?.error_message || "Something went wrong"; + throw errorMessage; + } catch (error) { + throw error; + } +} diff --git a/src/util/common-utils.ts b/src/util/common-utils.ts index 9f473bb..6a843e2 100644 --- a/src/util/common-utils.ts +++ b/src/util/common-utils.ts @@ -1,6 +1,7 @@ import { ContentstackClient, FlagInput } from "@contentstack/cli-utilities"; import { AppLocation, Extension, LogFn } from "../types"; import { cliux, Stack } from "@contentstack/cli-utilities"; +import { apiRequestHandler } from "./api-request-handler"; export type CommonOptions = { log: LogFn; @@ -134,6 +135,33 @@ function installApp( }); } +async function reinstallApp(params: { + flags: FlagInput; + type: string; + developerHubBaseUrl: string; + orgUid: string; + manifestUid: string; +}): Promise { + const { type, developerHubBaseUrl, flags, orgUid, manifestUid } = params; + const payload = { + target_type: type, + target_uid: (flags["stack-api-key"] as any) || orgUid, + }; + + const url = `https://${developerHubBaseUrl}/manifests/${manifestUid}/reinstall`; + try { + const result = await apiRequestHandler({ + orgUid, + payload, + url, + method: "PUT", + }); + return result; + } catch (err) { + throw err; + } +} + function fetchStack(flags: FlagInput, options: CommonOptions) { const { managementSdk } = options; return managementSdk @@ -225,9 +253,8 @@ async function fetchInstalledApps( return batchRequests.flat(); } - -// To remove the relative path -const sanitizePath = (str: string) => str?.replace(/^(\.\.(\/|\\|$))+/, ''); +// To remove the relative path +const sanitizePath = (str: string) => str?.replace(/^(\.\.(\/|\\|$))+/, ""); export { getOrganizations, @@ -241,5 +268,6 @@ export { fetchStack, uninstallApp, fetchInstalledApps, - sanitizePath + reinstallApp, + sanitizePath, }; diff --git a/src/util/error-helper.ts b/src/util/error-helper.ts new file mode 100644 index 0000000..f5ef089 --- /dev/null +++ b/src/util/error-helper.ts @@ -0,0 +1,7 @@ +export function formatErrors(errors: any): string { + let errorMessage: string = ""; + if (errors.message) { + errorMessage = errors.message; + } + return errorMessage; +}