diff --git a/.talismanrc b/.talismanrc index 77b44b968..3da5c6cb7 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,14 +1,14 @@ fileignoreconfig: - - filename: pnpm-lock.yaml - checksum: f0bc1ecd9132790009fd54318236bf3ded95d3c44132ab7b55fe37f3759cd3ea - filename: .cursor/skills/code-review/SKILL.md checksum: 29d812ac5c2ed4c55490f8d31e15eb592851601a6a141354cb458b1b9f1daa7a - - filename: .cursor/rules/oclif-commands.mdc - checksum: 8e269309cbfc9687e4a889c4a7983f145e77066d515dae53968d7553ae726b41 - - filename: .cursor/skills/code-review/references/code-review-checklist.md - checksum: bdf7453f08d7209deaee411f47a1132ee872b28f0eb082563dfe20aa56eab057 - filename: .cursor/commands/code-review.md checksum: a2737c43d58de842cf48c06b0471648a7c38b5fa8854d7c30f3d9258cd8b48f9 + - filename: .cursor/skills/code-review/references/code-review-checklist.md + checksum: bdf7453f08d7209deaee411f47a1132ee872b28f0eb082563dfe20aa56eab057 + - filename: .cursor/rules/dev-workflow.md + checksum: 3c3a483b44901bb440b4ce311a40d6e8c11decf9795f6d2d1d3a3787aa9981c3 + - filename: .cursor/rules/oclif-commands.mdc + checksum: 8e269309cbfc9687e4a889c4a7983f145e77066d515dae53968d7553ae726b41 - filename: .cursor/skills/testing/references/testing-patterns.md checksum: 0a6cb66f27eda46b40508517063a2f43fea1b4b8df878e7ddff404ab7fc126f8 - filename: .cursor/skills/contentstack-cli/SKILL.md diff --git a/packages/contentstack-audit/README.md b/packages/contentstack-audit/README.md index b72080c66..f4d532fc8 100644 --- a/packages/contentstack-audit/README.md +++ b/packages/contentstack-audit/README.md @@ -19,7 +19,7 @@ $ npm install -g @contentstack/cli-audit $ csdx COMMAND running command... $ csdx (--version|-v) -@contentstack/cli-audit/1.19.1 darwin-arm64 node-v22.13.1 +@contentstack/cli-audit/1.19.1 darwin-arm64 node-v24.14.0 $ csdx --help [COMMAND] USAGE $ csdx COMMAND @@ -35,16 +35,6 @@ USAGE * [`csdx cm:stacks:audit`](#csdx-cmstacksaudit) * [`csdx cm:stacks:audit:fix`](#csdx-cmstacksauditfix) * [`csdx help [COMMAND]`](#csdx-help-command) -* [`csdx plugins`](#csdx-plugins) -* [`csdx plugins:add PLUGIN`](#csdx-pluginsadd-plugin) -* [`csdx plugins:inspect PLUGIN...`](#csdx-pluginsinspect-plugin) -* [`csdx plugins:install PLUGIN`](#csdx-pluginsinstall-plugin) -* [`csdx plugins:link PATH`](#csdx-pluginslink-path) -* [`csdx plugins:remove [PLUGIN]`](#csdx-pluginsremove-plugin) -* [`csdx plugins:reset`](#csdx-pluginsreset) -* [`csdx plugins:uninstall [PLUGIN]`](#csdx-pluginsuninstall-plugin) -* [`csdx plugins:unlink [PLUGIN]`](#csdx-pluginsunlink-plugin) -* [`csdx plugins:update`](#csdx-pluginsupdate) ## `csdx audit` @@ -287,294 +277,4 @@ DESCRIPTION ``` _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.37/src/commands/help.ts)_ - -## `csdx plugins` - -List installed plugins. - -``` -USAGE - $ csdx plugins [--json] [--core] - -FLAGS - --core Show core plugins. - -GLOBAL FLAGS - --json Format output as json. - -DESCRIPTION - List installed plugins. - -EXAMPLES - $ csdx plugins -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/index.ts)_ - -## `csdx plugins:add PLUGIN` - -Installs a plugin into csdx. - -``` -USAGE - $ csdx plugins:add PLUGIN... [--json] [-f] [-h] [-s | -v] - -ARGUMENTS - PLUGIN... Plugin to install. - -FLAGS - -f, --force Force npm to fetch remote resources even if a local copy exists on disk. - -h, --help Show CLI help. - -s, --silent Silences npm output. - -v, --verbose Show verbose npm output. - -GLOBAL FLAGS - --json Format output as json. - -DESCRIPTION - Installs a plugin into csdx. - - Uses npm to install plugins. - - Installation of a user-installed plugin will override a core plugin. - - Use the CSDX_NPM_LOG_LEVEL environment variable to set the npm loglevel. - Use the CSDX_NPM_REGISTRY environment variable to set the npm registry. - -ALIASES - $ csdx plugins:add - -EXAMPLES - Install a plugin from npm registry. - - $ csdx plugins:add myplugin - - Install a plugin from a github url. - - $ csdx plugins:add https://github.com/someuser/someplugin - - Install a plugin from a github slug. - - $ csdx plugins:add someuser/someplugin -``` - -## `csdx plugins:inspect PLUGIN...` - -Displays installation properties of a plugin. - -``` -USAGE - $ csdx plugins:inspect PLUGIN... - -ARGUMENTS - PLUGIN... [default: .] Plugin to inspect. - -FLAGS - -h, --help Show CLI help. - -v, --verbose - -GLOBAL FLAGS - --json Format output as json. - -DESCRIPTION - Displays installation properties of a plugin. - -EXAMPLES - $ csdx plugins:inspect myplugin -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/inspect.ts)_ - -## `csdx plugins:install PLUGIN` - -Installs a plugin into csdx. - -``` -USAGE - $ csdx plugins:install PLUGIN... [--json] [-f] [-h] [-s | -v] - -ARGUMENTS - PLUGIN... Plugin to install. - -FLAGS - -f, --force Force npm to fetch remote resources even if a local copy exists on disk. - -h, --help Show CLI help. - -s, --silent Silences npm output. - -v, --verbose Show verbose npm output. - -GLOBAL FLAGS - --json Format output as json. - -DESCRIPTION - Installs a plugin into csdx. - - Uses npm to install plugins. - - Installation of a user-installed plugin will override a core plugin. - - Use the CSDX_NPM_LOG_LEVEL environment variable to set the npm loglevel. - Use the CSDX_NPM_REGISTRY environment variable to set the npm registry. - -ALIASES - $ csdx plugins:add - -EXAMPLES - Install a plugin from npm registry. - - $ csdx plugins:install myplugin - - Install a plugin from a github url. - - $ csdx plugins:install https://github.com/someuser/someplugin - - Install a plugin from a github slug. - - $ csdx plugins:install someuser/someplugin -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/install.ts)_ - -## `csdx plugins:link PATH` - -Links a plugin into the CLI for development. - -``` -USAGE - $ csdx plugins:link PATH [-h] [--install] [-v] - -ARGUMENTS - PATH [default: .] path to plugin - -FLAGS - -h, --help Show CLI help. - -v, --verbose - --[no-]install Install dependencies after linking the plugin. - -DESCRIPTION - Links a plugin into the CLI for development. - - Installation of a linked plugin will override a user-installed or core plugin. - - e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello' - command will override the user-installed or core plugin implementation. This is useful for development work. - - -EXAMPLES - $ csdx plugins:link myplugin -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/link.ts)_ - -## `csdx plugins:remove [PLUGIN]` - -Removes a plugin from the CLI. - -``` -USAGE - $ csdx plugins:remove [PLUGIN...] [-h] [-v] - -ARGUMENTS - [PLUGIN...] plugin to uninstall - -FLAGS - -h, --help Show CLI help. - -v, --verbose - -DESCRIPTION - Removes a plugin from the CLI. - -ALIASES - $ csdx plugins:unlink - $ csdx plugins:remove - -EXAMPLES - $ csdx plugins:remove myplugin -``` - -## `csdx plugins:reset` - -Remove all user-installed and linked plugins. - -``` -USAGE - $ csdx plugins:reset [--hard] [--reinstall] - -FLAGS - --hard Delete node_modules and package manager related files in addition to uninstalling plugins. - --reinstall Reinstall all plugins after uninstalling. -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/reset.ts)_ - -## `csdx plugins:uninstall [PLUGIN]` - -Removes a plugin from the CLI. - -``` -USAGE - $ csdx plugins:uninstall [PLUGIN...] [-h] [-v] - -ARGUMENTS - [PLUGIN...] plugin to uninstall - -FLAGS - -h, --help Show CLI help. - -v, --verbose - -DESCRIPTION - Removes a plugin from the CLI. - -ALIASES - $ csdx plugins:unlink - $ csdx plugins:remove - -EXAMPLES - $ csdx plugins:uninstall myplugin -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/uninstall.ts)_ - -## `csdx plugins:unlink [PLUGIN]` - -Removes a plugin from the CLI. - -``` -USAGE - $ csdx plugins:unlink [PLUGIN...] [-h] [-v] - -ARGUMENTS - [PLUGIN...] plugin to uninstall - -FLAGS - -h, --help Show CLI help. - -v, --verbose - -DESCRIPTION - Removes a plugin from the CLI. - -ALIASES - $ csdx plugins:unlink - $ csdx plugins:remove - -EXAMPLES - $ csdx plugins:unlink myplugin -``` - -## `csdx plugins:update` - -Update installed plugins. - -``` -USAGE - $ csdx plugins:update [-h] [-v] - -FLAGS - -h, --help Show CLI help. - -v, --verbose - -DESCRIPTION - Update installed plugins. -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v5.4.56/src/commands/plugins/update.ts)_ diff --git a/packages/contentstack-audit/package.json b/packages/contentstack-audit/package.json index 0ed5d49f8..f3177f8ce 100644 --- a/packages/contentstack-audit/package.json +++ b/packages/contentstack-audit/package.json @@ -22,11 +22,10 @@ "@contentstack/cli-utilities": "~1.18.0", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", - "@oclif/plugin-plugins": "^5.4.54", "chalk": "^4.1.2", "fast-csv": "^4.3.6", "fs-extra": "^11.3.0", - "lodash": "4.18.1", + "lodash": "^4.18.1", "uuid": "^9.0.1", "winston": "^3.17.0" }, @@ -53,8 +52,7 @@ "bin": "csdx", "commands": "./lib/commands", "plugins": [ - "@oclif/plugin-help", - "@oclif/plugin-plugins" + "@oclif/plugin-help" ], "topicSeparator": ":", "additionalHelpFlags": [ diff --git a/packages/contentstack-branches/README.md b/packages/contentstack-branches/README.md index c3127b6a1..2041250db 100755 --- a/packages/contentstack-branches/README.md +++ b/packages/contentstack-branches/README.md @@ -37,7 +37,7 @@ $ npm install -g @contentstack/cli-cm-branches $ csdx COMMAND running command... $ csdx (--version) -@contentstack/cli-cm-branches/1.7.1 darwin-arm64 node-v22.13.1 +@contentstack/cli-cm-branches/1.8.0 darwin-arm64 node-v24.14.0 $ csdx --help [COMMAND] USAGE $ csdx COMMAND @@ -52,7 +52,9 @@ USAGE * [`csdx cm:branches:create`](#csdx-cmbranchescreate) * [`csdx cm:branches:delete [-uid ] [-k ]`](#csdx-cmbranchesdelete--uid-value--k-value) * [`csdx cm:branches:diff [--base-branch ] [--compare-branch ] [-k ][--module ] [--format ] [--csv-path ]`](#csdx-cmbranchesdiff---base-branch-value---compare-branch-value--k-value--module-value---format-value---csv-path-value) +* [`csdx cm:branches:generate-scripts -k --merge-uid `](#csdx-cmbranchesgenerate-scripts--k-value---merge-uid-value) * [`csdx cm:branches:merge [-k ][--compare-branch ] [--no-revert] [--export-summary-path ] [--use-merge-summary ] [--comment ] [--base-branch ]`](#csdx-cmbranchesmerge--k-value--compare-branch-value---no-revert---export-summary-path-value---use-merge-summary-value---comment-value---base-branch-value) +* [`csdx cm:branches:merge-status -k --merge-uid `](#csdx-cmbranchesmerge-status--k-value---merge-uid-value) ## `csdx cm:branches` @@ -192,6 +194,29 @@ EXAMPLES _See code: [src/commands/cm/branches/diff.ts](https://github.com/contentstack/cli/blob/main/packages/contentstack-export/src/commands/cm/branches/diff.ts)_ +## `csdx cm:branches:generate-scripts -k --merge-uid ` + +Generate entry migration scripts for a completed merge job + +``` +USAGE + $ csdx cm:branches:generate-scripts -k --merge-uid + +FLAGS + -k, --stack-api-key= (required) Provide your stack API key. + --merge-uid= (required) Merge job UID to generate scripts for. + +DESCRIPTION + Generate entry migration scripts for a completed merge job + +EXAMPLES + $ csdx cm:branches:generate-scripts -k bltxxxxxxxx --merge-uid merge_abc123 + + $ csdx cm:branches:generate-scripts --stack-api-key bltxxxxxxxx --merge-uid merge_abc123 +``` + +_See code: [src/commands/cm/branches/generate-scripts.ts](https://github.com/contentstack/cli/blob/main/packages/contentstack-export/src/commands/cm/branches/generate-scripts.ts)_ + ## `csdx cm:branches:merge [-k ][--compare-branch ] [--no-revert] [--export-summary-path ] [--use-merge-summary ] [--comment ] [--base-branch ]` Merge changes from a branch @@ -230,4 +255,27 @@ EXAMPLES ``` _See code: [src/commands/cm/branches/merge.ts](https://github.com/contentstack/cli/blob/main/packages/contentstack-export/src/commands/cm/branches/merge.ts)_ + +## `csdx cm:branches:merge-status -k --merge-uid ` + +Check the status of a branch merge job + +``` +USAGE + $ csdx cm:branches:merge-status -k --merge-uid + +FLAGS + -k, --stack-api-key= (required) Provide your stack API key. + --merge-uid= (required) Merge job UID to check status for. + +DESCRIPTION + Check the status of a branch merge job + +EXAMPLES + $ csdx cm:branches:merge-status -k bltxxxxxxxx --merge-uid merge_abc123 + + $ csdx cm:branches:merge-status --stack-api-key bltxxxxxxxx --merge-uid merge_abc123 +``` + +_See code: [src/commands/cm/branches/merge-status.ts](https://github.com/contentstack/cli/blob/main/packages/contentstack-export/src/commands/cm/branches/merge-status.ts)_ diff --git a/packages/contentstack-branches/package.json b/packages/contentstack-branches/package.json index 6e3953fda..a1450011b 100644 --- a/packages/contentstack-branches/package.json +++ b/packages/contentstack-branches/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-branches", "description": "Contentstack CLI plugin to do branches operations", - "version": "1.7.1", + "version": "1.8.0", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { @@ -11,7 +11,7 @@ "@contentstack/cli-utilities": "~1.18.0", "chalk": "^4.1.2", "just-diff": "^6.0.2", - "lodash": "4.18.1" + "lodash": "^4.18.1" }, "devDependencies": { "@contentstack/cli-dev-dependencies": "^1.3.1", @@ -75,6 +75,7 @@ "cm:branches:delete": "BRDEL", "cm:branches:diff": "BRDIF", "cm:branches:merge": "BRMRG", + "cm:branches:merge-status": "BRMST", "cm:branches": "BRLS" } }, diff --git a/packages/contentstack-branches/src/branch/diff-handler.ts b/packages/contentstack-branches/src/branch/diff-handler.ts index 72aeaf0f2..4ada1bc48 100644 --- a/packages/contentstack-branches/src/branch/diff-handler.ts +++ b/packages/contentstack-branches/src/branch/diff-handler.ts @@ -1,20 +1,21 @@ -import startCase from 'lodash/startCase'; -import camelCase from 'lodash/camelCase'; import { cliux } from '@contentstack/cli-utilities'; +import camelCase from 'lodash/camelCase'; +import startCase from 'lodash/startCase'; + +import { BranchDiffPayload, BranchOptions } from '../interfaces'; import { getbranchConfig } from '../utils'; -import { BranchOptions, BranchDiffPayload } from '../interfaces'; -import { askBaseBranch, askCompareBranch, askStackAPIKey, selectModule } from '../utils/interactive'; import { fetchBranchesDiff, - parseSummary, - printSummary, + filterBranchDiffDataByModule, parseCompactText, - printCompactTextView, + parseSummary, parseVerbose, + printCompactTextView, + printSummary, printVerboseTextView, - filterBranchDiffDataByModule, } from '../utils/branch-diff-utility'; import { exportCSVReport } from '../utils/csv-utility'; +import { askBaseBranch, askCompareBranch, askStackAPIKey, selectModule } from '../utils/interactive'; export default class BranchDiffHandler { private options: BranchOptions; @@ -23,46 +24,36 @@ export default class BranchDiffHandler { this.options = params; } - async run(): Promise { - await this.validateMandatoryFlags(); - await this.initBranchDiffUtility(); - } - /** - * @methods validateMandatoryFlags - validate flags and prompt to select required flags - * @returns {*} {Promise} + * @methods displayBranchDiffTextAndVerbose - to show branch differences in compactText or detailText format + * @returns {*} {void} * @memberof BranchDiff */ - async validateMandatoryFlags(): Promise { - let baseBranch: string; - if (!this.options.stackAPIKey) { - this.options.stackAPIKey = await askStackAPIKey(); - } - - if (!this.options.baseBranch) { - baseBranch = getbranchConfig(this.options.stackAPIKey); - if (!baseBranch) { - this.options.baseBranch = await askBaseBranch(); - } else { - this.options.baseBranch = baseBranch; - } - } - - if (!this.options.compareBranch) { - this.options.compareBranch = await askCompareBranch(); - } - - if (!this.options.module) { - this.options.module = await selectModule(); - } + async displayBranchDiffTextAndVerbose(branchDiffData: any[], payload: BranchDiffPayload): Promise { + const spinner = cliux.loaderV2('Loading branch differences...'); + if (this.options.format === 'compact-text') { + const branchTextRes = parseCompactText(branchDiffData); + cliux.loaderV2('', spinner); + printCompactTextView(branchTextRes); + } else if (this.options.format === 'detailed-text') { + const verboseRes = await parseVerbose(branchDiffData, payload); + cliux.loaderV2('', spinner); + printVerboseTextView(verboseRes); - if (this.options.format === 'detailed-text' && !this.options.csvPath) { - this.options.csvPath = process.cwd(); + exportCSVReport(payload.module, verboseRes, this.options.csvPath); } + } - if(baseBranch){ - cliux.print(`\nBase branch: ${baseBranch}`, { color: 'grey' }); - } + /** + * @methods displaySummary - show branches summary on CLI + * @returns {*} {void} + * @memberof BranchDiff + */ + displaySummary(branchDiffData: any[], module: string): void { + cliux.print(' '); + cliux.print(`${startCase(camelCase(module))} Summary:`, { color: 'yellow' }); + const diffSummary = parseSummary(branchDiffData, this.options.baseBranch, this.options.compareBranch); + printSummary(diffSummary); } /** @@ -73,11 +64,11 @@ export default class BranchDiffHandler { async initBranchDiffUtility(): Promise { const spinner = cliux.loaderV2('Loading branch differences...'); const payload: BranchDiffPayload = { - module: '', apiKey: this.options.stackAPIKey, baseBranch: this.options.baseBranch, compareBranch: this.options.compareBranch, - host: this.options.host + host: this.options.host, + module: '' }; if (this.options.module === 'content-types') { @@ -91,7 +82,7 @@ export default class BranchDiffHandler { cliux.loaderV2('', spinner); if(this.options.module === 'all'){ - for (let module in diffData) { + for (const module in diffData) { const branchDiff = diffData[module]; payload.module = module; this.displaySummary(branchDiff, module); @@ -104,35 +95,45 @@ export default class BranchDiffHandler { } } - /** - * @methods displaySummary - show branches summary on CLI - * @returns {*} {void} - * @memberof BranchDiff - */ - displaySummary(branchDiffData: any[], module: string): void { - cliux.print(' '); - cliux.print(`${startCase(camelCase(module))} Summary:`, { color: 'yellow' }); - const diffSummary = parseSummary(branchDiffData, this.options.baseBranch, this.options.compareBranch); - printSummary(diffSummary); + async run(): Promise { + await this.validateMandatoryFlags(); + await this.initBranchDiffUtility(); } /** - * @methods displayBranchDiffTextAndVerbose - to show branch differences in compactText or detailText format - * @returns {*} {void} + * @methods validateMandatoryFlags - validate flags and prompt to select required flags + * @returns {*} {Promise} * @memberof BranchDiff */ - async displayBranchDiffTextAndVerbose(branchDiffData: any[], payload: BranchDiffPayload): Promise { - const spinner = cliux.loaderV2('Loading branch differences...'); - if (this.options.format === 'compact-text') { - const branchTextRes = parseCompactText(branchDiffData); - cliux.loaderV2('', spinner); - printCompactTextView(branchTextRes); - } else if (this.options.format === 'detailed-text') { - const verboseRes = await parseVerbose(branchDiffData, payload); - cliux.loaderV2('', spinner); - printVerboseTextView(verboseRes); + async validateMandatoryFlags(): Promise { + let baseBranch: string; + if (!this.options.stackAPIKey) { + this.options.stackAPIKey = await askStackAPIKey(); + } - exportCSVReport(payload.module, verboseRes, this.options.csvPath); + if (!this.options.baseBranch) { + baseBranch = getbranchConfig(this.options.stackAPIKey); + if (!baseBranch) { + this.options.baseBranch = await askBaseBranch(); + } else { + this.options.baseBranch = baseBranch; + } + } + + if (!this.options.compareBranch) { + this.options.compareBranch = await askCompareBranch(); + } + + if (!this.options.module) { + this.options.module = await selectModule(); + } + + if (this.options.format === 'detailed-text' && !this.options.csvPath) { + this.options.csvPath = process.cwd(); + } + + if(baseBranch){ + cliux.print(`\nBase branch: ${baseBranch}`, { color: 'grey' }); } } } diff --git a/packages/contentstack-branches/src/branch/index.ts b/packages/contentstack-branches/src/branch/index.ts index 9da452fc1..032e22966 100644 --- a/packages/contentstack-branches/src/branch/index.ts +++ b/packages/contentstack-branches/src/branch/index.ts @@ -2,5 +2,5 @@ * Business logics can be written inside this directory */ -export { default as MergeHandler } from './merge-handler'; export { default as BranchDiffHandler } from './diff-handler'; +export { default as MergeHandler } from './merge-handler'; diff --git a/packages/contentstack-branches/src/branch/merge-handler.ts b/packages/contentstack-branches/src/branch/merge-handler.ts index 0d2755215..04071ee26 100644 --- a/packages/contentstack-branches/src/branch/merge-handler.ts +++ b/packages/contentstack-branches/src/branch/merge-handler.ts @@ -1,38 +1,39 @@ -import os from 'os'; -import path from 'path'; -import forEach from 'lodash/forEach'; import { cliux } from '@contentstack/cli-utilities'; import chalk from 'chalk'; +import forEach from 'lodash/forEach'; +import os from 'os'; +import path from 'path'; + import { MergeInputOptions, MergeSummary } from '../interfaces'; import { - selectMergeStrategy, - selectMergeStrategySubOptions, - selectMergeExecution, - prepareMergeRequestPayload, - displayMergeSummary, askExportMergeSummaryPath, askMergeComment, - writeFile, + displayMergeSummary, executeMerge, generateMergeScripts, - selectCustomPreferences, - selectContentMergePreference, + prepareMergeRequestPayload, selectContentMergeCustomPreferences, + selectContentMergePreference, + selectCustomPreferences, + selectMergeExecution, + selectMergeStrategy, + selectMergeStrategySubOptions, + writeFile, } from '../utils'; export default class MergeHandler { - private strategy: string; - private strategySubOption?: string; private branchCompareData: any; - private mergeSettings: any; - private executeOption?: string; private displayFormat: string; + private enableEntryExp: boolean; + private executeOption?: string; private exportSummaryPath: string; + private host: string; + private mergeSettings: any; private mergeSummary: MergeSummary; private stackAPIKey: string; + private strategy: string; + private strategySubOption?: string; private userInputs: MergeInputOptions; - private host: string; - private enableEntryExp: boolean; constructor(options: MergeInputOptions) { this.stackAPIKey = options.stackAPIKey; @@ -45,8 +46,8 @@ export default class MergeHandler { this.mergeSummary = options.mergeSummary; this.userInputs = options; this.mergeSettings = { - baseBranch: options.baseBranch, // UID of the base branch, where the changes will be merged into - compareBranch: options.compareBranch, // UID of the branch to merge + baseBranch: options.baseBranch, + compareBranch: options.compareBranch, mergeComment: options.mergeComment, mergeContent: {}, noRevert: options.noRevert, @@ -55,29 +56,67 @@ export default class MergeHandler { this.enableEntryExp = options.enableEntryExp; } - async start() { - if (this.mergeSummary) { - this.loadMergeSettings(); - await this.displayMergeSummary(); - return await this.executeMerge(this.mergeSummary.requestPayload); - } - await this.collectMergeSettings(); - const mergePayload = prepareMergeRequestPayload(this.mergeSettings); - if (this.executeOption === 'execute') { - await this.exportSummary(mergePayload); - await this.executeMerge(mergePayload); - } else if (this.executeOption === 'export') { - await this.exportSummary(mergePayload); - } else if (this.executeOption === 'merge_n_scripts') { - this.enableEntryExp = true; - await this.executeMerge(mergePayload); - } else if (this.executeOption === 'summary_n_scripts') { - this.enableEntryExp = true; - await this.exportSummary(mergePayload); - } else { - await this.exportSummary(mergePayload); - await this.executeMerge(mergePayload); + /** + * Checks whether the selection of modules in the compare branch data is empty. + * + * This method evaluates the branch compare data and determines if there are any changes + * (added, modified, or deleted) in the modules based on the merge strategy defined in the + * merge settings. It categorizes the status of each module as either existing and empty or + * not empty. + * + * @returns An object containing: + * - `allEmpty`: A boolean indicating whether all modules are either non-existent or empty. + * - `moduleStatus`: A record mapping module types (`contentType` and `globalField`) to their + * respective statuses, which include: + * - `exists`: A boolean indicating whether the module exists in the branch comparison data. + * - `empty`: A boolean indicating whether the module has no changes (added, modified, or deleted). + */ + checkEmptySelection(): { + allEmpty: boolean; + moduleStatus: Record; + } { + const strategy = this.mergeSettings.strategy; + + const useMergeContent = new Set(['custom_preferences', 'ignore']); + const modifiedOnlyStrategies = new Set(['merge_modified_only_prefer_base', 'merge_modified_only_prefer_compare']); + const addedOnlyStrategies = new Set(['merge_new_only']); + + const moduleStatus: Record = { + contentType: { empty: true, exists: false }, + globalField: { empty: true, exists: false }, + }; + + for (const module in this.branchCompareData) { + const content = useMergeContent.has(strategy) + ? this.mergeSettings.mergeContent[module] + : this.branchCompareData[module]; + + if (!content) continue; + + const isGlobalField = module === 'global_fields'; + const type = isGlobalField ? 'globalField' : 'contentType'; + moduleStatus[type].exists = true; + + let hasChanges = false; + if (modifiedOnlyStrategies.has(strategy)) { + hasChanges = Array.isArray(content.modified) && content.modified.length > 0; + } else if (addedOnlyStrategies.has(strategy)) { + hasChanges = Array.isArray(content.added) && content.added.length > 0; + } else { + hasChanges = + (Array.isArray(content.modified) && content.modified.length > 0) || + (Array.isArray(content.added) && content.added.length > 0) || + (Array.isArray(content.deleted) && content.deleted.length > 0); + } + + if (hasChanges) { + moduleStatus[type].empty = false; + } } + + const allEmpty = Object.values(moduleStatus).every((status) => !status.exists || status.empty); + + return { allEmpty, moduleStatus }; } async collectMergeSettings() { @@ -101,11 +140,11 @@ export default class MergeHandler { } if (this.strategy === 'custom_preferences') { this.mergeSettings.itemMergeStrategies = []; - for (let module in this.branchCompareData) { + for (const module in this.branchCompareData) { this.mergeSettings.mergeContent[module] = { added: [], - modified: [], deleted: [], + modified: [], }; const selectedItems = await selectCustomPreferences(module, this.branchCompareData[module]); if (selectedItems?.length) { @@ -138,22 +177,22 @@ export default class MergeHandler { const { allEmpty, moduleStatus } = this.checkEmptySelection(); const strategyName = this.mergeSettings.strategy; - + if (allEmpty) { cliux.print(chalk.red(`No items selected according to the '${strategyName}' strategy.`)); process.exit(1); } - - for (const [type, { exists, empty }] of Object.entries(moduleStatus)) { + + for (const [type, { empty, exists }] of Object.entries(moduleStatus)) { if (exists && empty) { const readable = type === 'contentType' ? 'Content Types' : 'Global fields'; - cliux.print('\n') + cliux.print('\n'); cliux.print(chalk.yellow(`Note: No ${readable} selected according to the '${strategyName}' strategy.`)); } } - + this.displayMergeSummary(); - + if (!this.executeOption) { const executionResponse = await selectMergeExecution(); if (executionResponse === 'previous') { @@ -171,160 +210,22 @@ export default class MergeHandler { } } - /** - * Checks whether the selection of modules in the compare branch data is empty. - * - * This method evaluates the branch compare data and determines if there are any changes - * (added, modified, or deleted) in the modules based on the merge strategy defined in the - * merge settings. It categorizes the status of each module as either existing and empty or - * not empty. - * - * @returns An object containing: - * - `allEmpty`: A boolean indicating whether all modules are either non-existent or empty. - * - `moduleStatus`: A record mapping module types (`contentType` and `globalField`) to their - * respective statuses, which include: - * - `exists`: A boolean indicating whether the module exists in the branch comparison data. - * - `empty`: A boolean indicating whether the module has no changes (added, modified, or deleted). - */ - checkEmptySelection(): { - allEmpty: boolean; - moduleStatus: Record; - } { - const strategy = this.mergeSettings.strategy; - - const useMergeContent = new Set(['custom_preferences', 'ignore']); - const modifiedOnlyStrategies = new Set(['merge_modified_only_prefer_base', 'merge_modified_only_prefer_compare']); - const addedOnlyStrategies = new Set(['merge_new_only']); - - const moduleStatus: Record = { - contentType: { exists: false, empty: true }, - globalField: { exists: false, empty: true }, - }; - - for (const module in this.branchCompareData) { - const content = useMergeContent.has(strategy) - ? this.mergeSettings.mergeContent[module] - : this.branchCompareData[module]; - - if (!content) continue; - - const isGlobalField = module === 'global_fields'; - const type = isGlobalField ? 'globalField' : 'contentType'; - moduleStatus[type].exists = true; - - let hasChanges = false; - if (modifiedOnlyStrategies.has(strategy)) { - hasChanges = Array.isArray(content.modified) && content.modified.length > 0; - } else if (addedOnlyStrategies.has(strategy)) { - hasChanges = Array.isArray(content.added) && content.added.length > 0; - } else { - hasChanges = - (Array.isArray(content.modified) && content.modified.length > 0) || - (Array.isArray(content.added) && content.added.length > 0) || - (Array.isArray(content.deleted) && content.deleted.length > 0); - } - - if (hasChanges) { - moduleStatus[type].empty = false; - } - } - - const allEmpty = Object.values(moduleStatus).every( - (status) => !status.exists || status.empty - ); - - return { allEmpty, moduleStatus }; - } - displayMergeSummary() { if (this.mergeSettings.strategy !== 'ignore') { - for (let module in this.branchCompareData) { + for (const module in this.branchCompareData) { this.mergeSettings.mergeContent[module] = {}; this.filterBranchCompareData(module, this.branchCompareData[module]); } } displayMergeSummary({ - format: this.displayFormat, compareData: this.mergeSettings.mergeContent, + format: this.displayFormat, }); } - filterBranchCompareData(module, moduleBranchCompareData) { - const { strategy, mergeContent } = this.mergeSettings; - switch (strategy) { - case 'merge_prefer_base': - mergeContent[module].added = moduleBranchCompareData.added; - mergeContent[module].modified = moduleBranchCompareData.modified; - mergeContent[module].deleted = moduleBranchCompareData.deleted; - break; - case 'merge_prefer_compare': - mergeContent[module].added = moduleBranchCompareData.added; - mergeContent[module].modified = moduleBranchCompareData.modified; - mergeContent[module].deleted = moduleBranchCompareData.deleted; - break; - case 'merge_new_only': - mergeContent[module].added = moduleBranchCompareData.added; - break; - case 'merge_modified_only_prefer_base': - mergeContent[module].modified = moduleBranchCompareData.modified; - break; - case 'merge_modified_only_prefer_compare': - mergeContent[module].modified = moduleBranchCompareData.modified; - break; - case 'merge_modified_only_prefer_compare': - mergeContent[module].modified = moduleBranchCompareData.modified; - break; - case 'overwrite_with_compare': - mergeContent[module].added = moduleBranchCompareData.added; - mergeContent[module].modified = moduleBranchCompareData.modified; - mergeContent[module].deleted = moduleBranchCompareData.deleted; - break; - default: - cliux.error(`Error: Invalid strategy '${strategy}'`); - process.exit(1); - } - } - - async exportSummary(mergePayload) { - if (!this.exportSummaryPath) { - this.exportSummaryPath = await askExportMergeSummaryPath(); - } - const summary: MergeSummary = { - requestPayload: mergePayload, - }; - await writeFile(path.join(this.exportSummaryPath, 'merge-summary.json'), summary); - cliux.success('Exported the summary successfully'); - - if (this.enableEntryExp) { - this.executeEntryExpFlow(this.stackAPIKey, mergePayload); - } - } - - async executeMerge(mergePayload) { - let spinner; - try { - if (!this.mergeSettings.mergeComment) { - this.mergeSettings.mergeComment = await askMergeComment(); - mergePayload.merge_comment = this.mergeSettings.mergeComment; - } - - spinner = cliux.loaderV2('Merging the changes...'); - const mergeResponse = await executeMerge(this.stackAPIKey, mergePayload, this.host); - cliux.loaderV2('', spinner); - cliux.success(`Merged the changes successfully. Merge UID: ${mergeResponse.uid}`); - - if (this.enableEntryExp) { - this.executeEntryExpFlow(mergeResponse.uid, mergePayload); - } - } catch (error) { - cliux.loaderV2('', spinner); - cliux.error('Failed to merge the changes', error.message || error); - } - } - async executeEntryExpFlow(mergeJobUID: string, mergePayload) { const { mergeContent } = this.mergeSettings; - let mergePreference = await selectContentMergePreference(); + const mergePreference = await selectContentMergePreference(); const updateEntryMergeStrategy = (items, mergeStrategy) => { items && @@ -334,10 +235,10 @@ export default class MergeHandler { }; const mergePreferencesMap = { + ask_preference: 'custom', + existing: 'merge_existing', existing_new: 'merge_existing_new', new: 'merge_new', - existing: 'merge_existing', - ask_preference: 'custom', }; const selectedMergePreference = mergePreferencesMap[mergePreference]; @@ -346,8 +247,8 @@ export default class MergeHandler { const selectedMergeItems = await selectContentMergeCustomPreferences(mergeContent.content_types); mergeContent.content_types = { added: [], - modified: [], deleted: [], + modified: [], }; selectedMergeItems?.forEach((item) => { @@ -362,7 +263,7 @@ export default class MergeHandler { process.exit(1); } - let scriptFolderPath = generateMergeScripts(mergeContent.content_types, mergeJobUID); + const scriptFolderPath = generateMergeScripts(mergeContent.content_types, mergeJobUID); if (scriptFolderPath !== undefined) { cliux.success(`\nSuccess! We have generated entry migration files in the folder ${scriptFolderPath}`); @@ -385,6 +286,102 @@ export default class MergeHandler { } } + /** + * Executes the merge operation with improved polling. + * Handles polling timeout gracefully by returning merge UID for later status checking. + * If enableEntryExp is true and merge is complete, generates scripts. + * + * @param mergePayload - Merge request payload with branch info + */ + async executeMerge(mergePayload) { + let spinner; + try { + if (!this.mergeSettings.mergeComment) { + this.mergeSettings.mergeComment = await askMergeComment(); + mergePayload.merge_comment = this.mergeSettings.mergeComment; + } + + spinner = cliux.loaderV2('Merging the changes...'); + const mergeResponse = await executeMerge(this.stackAPIKey, mergePayload, this.host); + cliux.loaderV2('', spinner); + + if (mergeResponse.merge_details?.status === 'complete') { + cliux.success(`Merged the changes successfully. Merge UID: ${mergeResponse.uid}`); + + if (this.enableEntryExp) { + await this.executeEntryExpFlow(mergeResponse.uid, mergePayload); + } + } else if (mergeResponse.pollingTimeout) { + cliux.success(`Merge job initiated successfully. Merge UID: ${mergeResponse.uid}`); + cliux.print('\n⏱ The merge is still processing in the background...', { color: 'yellow' }); + cliux.print('\nCheck status later using:', { color: 'grey' }); + cliux.print(` csdx cm:branches:merge-status -k ${this.stackAPIKey} --merge-uid ${mergeResponse.uid}`, { + color: 'cyan', + }); + } + } catch (error) { + cliux.loaderV2('', spinner); + cliux.error('Failed to merge the changes', error.message || error); + } + } + + async exportSummary(mergePayload) { + if (!this.exportSummaryPath) { + this.exportSummaryPath = await askExportMergeSummaryPath(); + } + const summary: MergeSummary = { + requestPayload: mergePayload, + }; + await writeFile(path.join(this.exportSummaryPath, 'merge-summary.json'), summary); + cliux.success('Exported the summary successfully'); + + if (this.enableEntryExp) { + await this.executeEntryExpFlow(this.stackAPIKey, mergePayload); + } + } + + filterBranchCompareData(module, moduleBranchCompareData) { + const { mergeContent, strategy } = this.mergeSettings; + switch (strategy) { + case 'merge_prefer_base': + mergeContent[module].added = moduleBranchCompareData.added; + mergeContent[module].modified = moduleBranchCompareData.modified; + mergeContent[module].deleted = moduleBranchCompareData.deleted; + break; + case 'merge_prefer_compare': + mergeContent[module].added = moduleBranchCompareData.added; + mergeContent[module].modified = moduleBranchCompareData.modified; + mergeContent[module].deleted = moduleBranchCompareData.deleted; + break; + case 'merge_new_only': + mergeContent[module].added = moduleBranchCompareData.added; + break; + case 'merge_modified_only_prefer_base': + mergeContent[module].modified = moduleBranchCompareData.modified; + break; + case 'merge_modified_only_prefer_compare': + mergeContent[module].modified = moduleBranchCompareData.modified; + break; + case 'overwrite_with_compare': + mergeContent[module].added = moduleBranchCompareData.added; + mergeContent[module].modified = moduleBranchCompareData.modified; + mergeContent[module].deleted = moduleBranchCompareData.deleted; + break; + default: + cliux.error(`Error: Invalid strategy '${strategy}'`); + process.exit(1); + } + } + + loadMergeSettings() { + this.mergeSettings.baseBranch = this.mergeSummary.requestPayload.base_branch; + this.mergeSettings.compareBranch = this.mergeSummary.requestPayload.compare_branch; + this.mergeSettings.strategy = this.mergeSummary.requestPayload.default_merge_strategy; + this.mergeSettings.itemMergeStrategies = this.mergeSummary.requestPayload.item_merge_strategies; + this.mergeSettings.noRevert = this.mergeSummary.requestPayload.no_revert; + this.mergeSettings.mergeComment = this.mergeSummary.requestPayload.merge_comment; + } + async restartMergeProcess() { if (!this.userInputs.strategy) { this.strategy = null; @@ -404,12 +401,29 @@ export default class MergeHandler { await this.start(); } - loadMergeSettings() { - this.mergeSettings.baseBranch = this.mergeSummary.requestPayload.base_branch; - this.mergeSettings.compareBranch = this.mergeSummary.requestPayload.compare_branch; - this.mergeSettings.strategy = this.mergeSummary.requestPayload.default_merge_strategy; - this.mergeSettings.itemMergeStrategies = this.mergeSummary.requestPayload.item_merge_strategies; - this.mergeSettings.noRevert = this.mergeSummary.requestPayload.no_revert; - this.mergeSettings.mergeComment = this.mergeSummary.requestPayload.merge_comment; + async start() { + if (this.mergeSummary) { + this.loadMergeSettings(); + await this.displayMergeSummary(); + return await this.executeMerge(this.mergeSummary.requestPayload); + } + await this.collectMergeSettings(); + const mergePayload = prepareMergeRequestPayload(this.mergeSettings); + if (this.executeOption === 'execute') { + await this.exportSummary(mergePayload); + await this.executeMerge(mergePayload); + } else if (this.executeOption === 'export') { + await this.exportSummary(mergePayload); + } else if (this.executeOption === 'merge_n_scripts') { + this.enableEntryExp = true; + await this.executeMerge(mergePayload); + } else if (this.executeOption === 'summary_n_scripts') { + this.enableEntryExp = true; + await this.exportSummary(mergePayload); + } else { + await this.exportSummary(mergePayload); + await this.executeMerge(mergePayload); + this.enableEntryExp = true; + } } } diff --git a/packages/contentstack-branches/src/commands/cm/branches/merge-status.ts b/packages/contentstack-branches/src/commands/cm/branches/merge-status.ts new file mode 100644 index 000000000..0bb22571a --- /dev/null +++ b/packages/contentstack-branches/src/commands/cm/branches/merge-status.ts @@ -0,0 +1,71 @@ +import { Command } from '@contentstack/cli-command'; +import { cliux, flags, isAuthenticated, managementSDKClient } from '@contentstack/cli-utilities'; +import { displayMergeStatusDetails, handleErrorMsg } from '../../../utils'; + +/** + * Command to check the status of a branch merge job. + * Allows users to check merge progress and status asynchronously. + */ +export default class BranchMergeStatusCommand extends Command { + static readonly description: string = 'Check the status of a branch merge job'; + + static readonly examples: string[] = [ + 'csdx cm:branches:merge-status -k bltxxxxxxxx --merge-uid merge_abc123', + 'csdx cm:branches:merge-status --stack-api-key bltxxxxxxxx --merge-uid merge_abc123', + ]; + + static readonly usage: string = 'cm:branches:merge-status -k --merge-uid '; + + static readonly flags = { + 'stack-api-key': flags.string({ + char: 'k', + description: 'Provide your stack API key.', + required: true, + }), + 'merge-uid': flags.string({ + description: 'Merge job UID to check status for.', + required: true, + }), + }; + + static readonly aliases: string[] = []; + + /** + * Fetches and displays the current status of a branch merge job. + * Useful for checking long-running merges asynchronously without blocking. + */ + async run(): Promise { + try { + const { flags: mergeStatusFlags } = await this.parse(BranchMergeStatusCommand); + + if (!isAuthenticated()) { + const err = { errorMessage: 'You are not logged in. Please login with command $ csdx auth:login' }; + handleErrorMsg(err); + } + + const { 'stack-api-key': stackAPIKey, 'merge-uid': mergeUID } = mergeStatusFlags; + + const stackAPIClient = await (await managementSDKClient({ host: this.cmaHost })).stack({ + api_key: stackAPIKey, + }); + + const spinner = cliux.loaderV2('Fetching merge status...'); + const mergeStatusResponse = await stackAPIClient + .branch() + .mergeQueue(mergeUID) + .fetch(); + cliux.loaderV2('', spinner); + + if (!mergeStatusResponse?.queue?.length) { + cliux.error(`No merge job found with UID: ${mergeUID}`); + process.exit(1); + } + + const mergeJobStatus = mergeStatusResponse.queue[0]; + displayMergeStatusDetails(mergeJobStatus); + } catch (error) { + cliux.error('Failed to fetch merge status', error.message || error); + process.exit(1); + } + } +} diff --git a/packages/contentstack-branches/src/config/index.ts b/packages/contentstack-branches/src/config/index.ts index c5a62ca1c..cb195a8d6 100644 --- a/packages/contentstack-branches/src/config/index.ts +++ b/packages/contentstack-branches/src/config/index.ts @@ -1,5 +1,5 @@ const config = { - skip: 0, - limit: 100 + limit: 100, + skip: 0 }; export default config; diff --git a/packages/contentstack-branches/src/interfaces/index.ts b/packages/contentstack-branches/src/interfaces/index.ts index 62b5655f6..29eba9801 100644 --- a/packages/contentstack-branches/src/interfaces/index.ts +++ b/packages/contentstack-branches/src/interfaces/index.ts @@ -1,34 +1,34 @@ export interface BranchOptions { + authToken?: string; + baseBranch?: string; compareBranch: string; - stackAPIKey: string; - module: string; + csvPath?: string; format: string; - baseBranch?: string; - authToken?: string; host?: string; - csvPath?: string; + module: string; + stackAPIKey: string; } export interface BranchDiffRes { - uid: string; + merge_strategy?: string; + status: string; title: string; type: string; - status: string; - merge_strategy?: string; + uid: string; } export interface BranchDiffSummary { base: string; - compare: string; base_only: number; + compare: string; compare_only: number; modified: number; } export interface BranchCompactTextRes { - modified?: BranchDiffRes[]; added?: BranchDiffRes[]; deleted?: BranchDiffRes[]; + modified?: BranchDiffRes[]; } export interface MergeSummary { @@ -40,61 +40,61 @@ type MergeSummaryRequestPayload = { compare_branch: string; default_merge_strategy: string; item_merge_strategies?: any[]; - no_revert?: boolean; merge_comment?: string; + no_revert?: boolean; }; export interface MergeInputOptions { - compareBranch: string; - strategy: string; - strategySubOption: string; + baseBranch: string; branchCompareData: any; - mergeComment?: string; + compareBranch: string; + enableEntryExp: boolean; executeOption?: string; - noRevert?: boolean; - baseBranch: string; - format?: string; exportSummaryPath?: string; + format?: string; + host: string; + mergeComment?: string; mergeSummary?: MergeSummary; + noRevert?: boolean; stackAPIKey: string; - host: string; - enableEntryExp: boolean; + strategy: string; + strategySubOption: string; } export interface ModifiedFieldsType { - uid: string; - displayName: string; - path: string; - field: string; - propertyChanges?: PropertyChange[]; changeCount?: number; changeDetails?: string; - oldValue?: any; + displayName: string; + field: string; newValue?: any; + oldValue?: any; + path: string; + propertyChanges?: PropertyChange[]; + uid: string; } export interface PropertyChange { - property: string; - changeType: 'modified' | 'added' | 'deleted'; - oldValue?: any; + changeType: 'added' | 'deleted' | 'modified'; newValue?: any; + oldValue?: any; + property: string; } export interface CSVRow { - srNo: number; contentTypeName: string; fieldName: string; fieldPath: string; operation: string; sourceBranchValue: string; + srNo: number; targetBranchValue: string; } export interface AddCSVRowParams { - srNo: number; contentTypeName: string; fieldName: string; fieldType: string; sourceValue: string; + srNo: number; targetValue: string; } @@ -108,43 +108,43 @@ export interface ContentTypeItem { } export interface ModifiedFieldsInput { - modified?: ModifiedFieldsType[]; added?: ModifiedFieldsType[]; deleted?: ModifiedFieldsType[]; + modified?: ModifiedFieldsType[]; } export interface BranchModifiedDetails { - moduleDetails: BranchDiffRes; modifiedFields: ModifiedFieldsInput; + moduleDetails: BranchDiffRes; } export interface BranchDiffVerboseRes { - modified?: BranchModifiedDetails[]; added?: BranchDiffRes[]; - deleted?: BranchDiffRes[]; csvData?: CSVRow[]; // Pre-processed CSV data + deleted?: BranchDiffRes[]; + modified?: BranchModifiedDetails[]; } export interface BranchDiffPayload { - module: string; apiKey: string; baseBranch: string; compareBranch: string; filter?: string; host?: string; - uid?: string; + module: string; spinner?: any; + uid?: string; url?: string; } export type MergeStrategy = - | 'merge_prefer_base' - | 'merge_prefer_compare' - | 'overwrite_with_compare' - | 'merge_new_only' + | 'ignore' | 'merge_modified_only_prefer_base' | 'merge_modified_only_prefer_compare' - | 'ignore'; + | 'merge_new_only' + | 'merge_prefer_base' + | 'merge_prefer_compare' + | 'overwrite_with_compare'; export interface MergeParams { base_branch: string; @@ -153,3 +153,33 @@ export interface MergeParams { merge_comment: string; no_revert?: boolean; } + +export interface MergeStatusOptions { + host?: string; + mergeUID: string; + stackAPIKey: string; +} + +export interface GenerateScriptsOptions { + host?: string; + mergeUID: string; + stackAPIKey: string; +} + +export interface MergeJobStatusResponse { + errors?: Array<{ details?: string; field?: string; message: string }>; + merge_details: { + completed_at?: string; + completion_percentage?: number; + created_at: string; + status: string; + updated_at: string; + }; + merge_summary: { + content_types: { added: number; deleted: number; modified: number }; + global_fields: { added: number; deleted: number; modified: number }; + }; + pollingTimeout?: boolean; + status: 'complete' | 'failed' | 'in_progress' | 'unknown'; + uid: string; +} diff --git a/packages/contentstack-branches/src/utils/branch-diff-utility.ts b/packages/contentstack-branches/src/utils/branch-diff-utility.ts index fc76eb5d5..28b9bf3c6 100644 --- a/packages/contentstack-branches/src/utils/branch-diff-utility.ts +++ b/packages/contentstack-branches/src/utils/branch-diff-utility.ts @@ -1,26 +1,26 @@ +import { cliux, managementSDKClient, messageHandler } from '@contentstack/cli-utilities'; import chalk from 'chalk'; +import { diff } from 'just-diff'; +import camelCase from 'lodash/camelCase'; +import find from 'lodash/find'; import forEach from 'lodash/forEach'; +import isArray from 'lodash/isArray'; import padStart from 'lodash/padStart'; import startCase from 'lodash/startCase'; -import camelCase from 'lodash/camelCase'; import unionWith from 'lodash/unionWith'; -import find from 'lodash/find'; -import { cliux, messageHandler, managementSDKClient } from '@contentstack/cli-utilities'; -import isArray from 'lodash/isArray'; -import { diff } from 'just-diff'; -import { extractValueFromPath, getFieldDisplayName, generateCSVDataFromVerbose } from './csv-utility'; +import config from '../config'; import { - BranchDiffRes, - ModifiedFieldsInput, - ModifiedFieldsType, - BranchModifiedDetails, + BranchCompactTextRes, BranchDiffPayload, + BranchDiffRes, BranchDiffSummary, - BranchCompactTextRes, BranchDiffVerboseRes, + BranchModifiedDetails, + ModifiedFieldsInput, + ModifiedFieldsType, } from '../interfaces/index'; -import config from '../config'; +import { extractValueFromPath, generateCSVDataFromVerbose, getFieldDisplayName } from './csv-utility'; /** * Fetch differences between two branches @@ -124,9 +124,9 @@ function handleErrorMsg(err, spinner) { * @returns {*} BranchDiffSummary */ function parseSummary(branchesDiffData: any[], baseBranch: string, compareBranch: string): BranchDiffSummary { - let baseCount: number = 0, - compareCount: number = 0, - modifiedCount: number = 0; + let baseCount = 0, + compareCount = 0, + modifiedCount = 0; if (branchesDiffData?.length) { forEach(branchesDiffData, (diff: BranchDiffRes) => { @@ -138,8 +138,8 @@ function parseSummary(branchesDiffData: any[], baseBranch: string, compareBranch const branchSummary: BranchDiffSummary = { base: baseBranch, - compare: compareBranch, base_only: baseCount, + compare: compareBranch, compare_only: compareCount, modified: modifiedCount, }; @@ -166,7 +166,7 @@ function printSummary(diffSummary: BranchDiffSummary): void { * @returns {*} BranchCompactTextRes */ function parseCompactText(branchesDiffData: any[]): BranchCompactTextRes { - let listOfModified: BranchDiffRes[] = [], + const listOfModified: BranchDiffRes[] = [], listOfAdded: BranchDiffRes[] = [], listOfDeleted: BranchDiffRes[] = []; @@ -179,9 +179,9 @@ function parseCompactText(branchesDiffData: any[]): BranchCompactTextRes { } const branchTextRes: BranchCompactTextRes = { - modified: listOfModified, added: listOfAdded, deleted: listOfDeleted, + modified: listOfModified, }; return branchTextRes; } @@ -223,32 +223,32 @@ function printCompactTextView(branchTextRes: BranchCompactTextRes): void { * @returns {*} Promise */ async function parseVerbose(branchesDiffData: any[], payload: BranchDiffPayload): Promise { - const { added, modified, deleted } = parseCompactText(branchesDiffData); - let modifiedDetailList: BranchModifiedDetails[] = []; + const { added, deleted, modified } = parseCompactText(branchesDiffData); + const modifiedDetailList: BranchModifiedDetails[] = []; for (let i = 0; i < modified?.length; i++) { const diff: BranchDiffRes = modified[i]; payload.uid = diff?.uid; const branchDiff = await branchCompareSDK(payload); if (branchDiff) { - const { listOfModifiedFields, listOfAddedFields, listOfDeletedFields } = await prepareBranchVerboseRes( + const { listOfAddedFields, listOfDeletedFields, listOfModifiedFields } = await prepareBranchVerboseRes( branchDiff, ); modifiedDetailList.push({ - moduleDetails: diff, modifiedFields: { - modified: listOfModifiedFields, - deleted: listOfDeletedFields, added: listOfAddedFields, + deleted: listOfDeletedFields, + modified: listOfModifiedFields, }, + moduleDetails: diff, }); } } const verboseRes: BranchDiffVerboseRes = { - modified: modifiedDetailList, added: added, deleted: deleted, + modified: modifiedDetailList, }; verboseRes.csvData = generateCSVDataFromVerbose(verboseRes); @@ -263,7 +263,7 @@ async function parseVerbose(branchesDiffData: any[], payload: BranchDiffPayload) * @returns */ async function prepareBranchVerboseRes(branchDiff: any) { - let listOfModifiedFields = [], + const listOfModifiedFields = [], listOfDeletedFields = [], listOfAddedFields = []; @@ -287,9 +287,9 @@ async function prepareBranchVerboseRes(branchDiff: any) { baseBranchFieldExists, compareBranchFieldExists, diffData, - listOfModifiedFields, - listOfDeletedFields, listOfAddedFields, + listOfDeletedFields, + listOfModifiedFields, }); }); } @@ -306,9 +306,9 @@ async function baseAndCompareBranchDiff(params: { baseBranchFieldExists: any; compareBranchFieldExists: any; diffData: any; - listOfModifiedFields: any[]; - listOfDeletedFields: any[]; listOfAddedFields: any[]; + listOfDeletedFields: any[]; + listOfModifiedFields: any[]; }) { const { baseBranchFieldExists, compareBranchFieldExists } = params; if (baseBranchFieldExists && compareBranchFieldExists) { @@ -323,10 +323,10 @@ async function baseAndCompareBranchDiff(params: { field = 'metadata' } params.listOfDeletedFields.push({ - path: path, displayName:displayName, - uid: baseBranchFieldExists?.uid, field: field, + path: path, + uid: baseBranchFieldExists?.uid, }); } else if (!baseBranchFieldExists && compareBranchFieldExists) { let displayName= compareBranchFieldExists?.display_name; @@ -338,10 +338,10 @@ async function baseAndCompareBranchDiff(params: { field = 'metadata' } params.listOfAddedFields.push({ - path: path, displayName: displayName, - uid: compareBranchFieldExists?.uid, field: field, + path: path, + uid: compareBranchFieldExists?.uid, }); } } @@ -349,9 +349,9 @@ async function baseAndCompareBranchDiff(params: { async function prepareModifiedDiff(params: { baseBranchFieldExists: any; compareBranchFieldExists: any; - listOfModifiedFields: any[]; - listOfDeletedFields: any[]; listOfAddedFields: any[]; + listOfDeletedFields: any[]; + listOfModifiedFields: any[]; }) { const { baseBranchFieldExists, compareBranchFieldExists } = params; if ( @@ -381,49 +381,49 @@ async function prepareModifiedDiff(params: { changeDetails = `Changed from "${oldTitle}" to "${newTitle}"`; } params.listOfModifiedFields.push({ - path: '', + changeDetails, displayName: displayName, - uid: baseBranchFieldExists.path, field: 'changed', - changeDetails, - oldValue: baseBranchFieldExists.value, newValue: compareBranchFieldExists.value, + oldValue: baseBranchFieldExists.value, + path: '', + uid: baseBranchFieldExists.path, }); } else { const fieldDisplayName = getFieldDisplayName(compareBranchFieldExists); - const { modified, deleted, added } = await deepDiff(baseBranchFieldExists, compareBranchFieldExists); - for (let field of Object.values(added)) { + const { added, deleted, modified } = await deepDiff(baseBranchFieldExists, compareBranchFieldExists); + for (const field of Object.values(added)) { if (field) { params.listOfAddedFields.push({ - path: field['path'], - displayName: getFieldDisplayName(field), - uid: field['uid'], + displayName: getFieldDisplayName(field), field: field['fieldType'] || field['data_type'] || 'field', + path: field['path'], + uid: field['uid'], }); } } - for (let field of Object.values(deleted)) { + for (const field of Object.values(deleted)) { if (field) { params.listOfDeletedFields.push({ - path: field['path'], - displayName: getFieldDisplayName(field), - uid: field['uid'], + displayName: getFieldDisplayName(field), field: field['fieldType'] || field['data_type'] || 'field', + path: field['path'], + uid: field['uid'], }); } } - for (let field of Object.values(modified)) { + for (const field of Object.values(modified)) { if (field) { params.listOfModifiedFields.push({ - path: field['path'], + changeCount: field['changeCount'], displayName: field['displayName'] || field['display_name'] || fieldDisplayName, - uid: field['uid'] || compareBranchFieldExists?.uid, field: `${field['fieldType'] || field['data_type'] || compareBranchFieldExists?.data_type || 'field'} field`, + path: field['path'], propertyChanges: field['propertyChanges'], - changeCount: field['changeCount'], + uid: field['uid'] || compareBranchFieldExists?.uid, }); } } @@ -487,7 +487,7 @@ function printModifiedFields(modfiedFields: ModifiedFieldsInput): void { * @returns */ function filterBranchDiffDataByModule(branchDiffData: any[]) { - let moduleRes = { + const moduleRes = { content_types: [], global_fields: [], }; @@ -503,22 +503,22 @@ const buildPath = (path, key) => (path === '' ? key : `${path}.${key}`); async function deepDiff(baseObj, compareObj) { const changes = { - modified: {}, added: {}, deleted: {}, + modified: {}, }; function baseAndCompareSchemaDiff(baseObj, compareObj, path = '') { - const { schema: baseSchema, path: basePath, ...restBaseObj } = baseObj; - const { schema: compareSchema, path: comparePath, ...restCompareObj } = compareObj; + const { path: basePath, schema: baseSchema, ...restBaseObj } = baseObj; + const { path: comparePath, schema: compareSchema, ...restCompareObj } = compareObj; const currentPath = buildPath(path, baseObj['uid']); if (restBaseObj['uid'] === restCompareObj['uid']) { prepareModifiedField({ - restBaseObj, - restCompareObj, - currentPath, changes, + currentPath, fullFieldContext: baseObj, parentContext: baseObj, + restBaseObj, + restCompareObj, }); } @@ -531,10 +531,10 @@ async function deepDiff(baseObj, compareObj) { let newPath: string; if (baseBranchField && !compareBranchField) { newPath = `${currentPath}.${baseBranchField['uid']}`; - prepareDeletedField({ path: newPath, changes, baseField: baseBranchField }); + prepareDeletedField({ baseField: baseBranchField, changes, path: newPath }); } else if (compareBranchField && !baseBranchField) { newPath = `${currentPath}.${compareBranchField['uid']}`; - prepareAddedField({ path: newPath, changes, compareField: compareBranchField }); + prepareAddedField({ changes, compareField: compareBranchField, path: newPath }); } else if (compareBranchField && baseBranchField) { baseAndCompareSchemaDiff(baseBranchField, compareBranchField, currentPath); } @@ -545,14 +545,14 @@ async function deepDiff(baseObj, compareObj) { if (baseSchema?.length && !compareSchema?.length && isArray(baseSchema)) { forEach(baseSchema, (base, key) => { const newPath = `${currentPath}.${base['uid']}`; - prepareDeletedField({ path: newPath, changes, baseField: base }); + prepareDeletedField({ baseField: base, changes, path: newPath }); }); } //case3:- compare schema exists only if (!baseSchema?.length && compareSchema?.length && isArray(compareSchema)) { forEach(compareSchema, (compare, key) => { const newPath = `${currentPath}.${compare['uid']}`; - prepareAddedField({ path: newPath, changes, compareField: compare }); + prepareAddedField({ changes, compareField: compare, path: newPath }); }); } } @@ -560,53 +560,53 @@ async function deepDiff(baseObj, compareObj) { return changes; } -function prepareAddedField(params: { path: string; changes: any; compareField: any }) { - const { path, changes, compareField } = params; +function prepareAddedField(params: { changes: any; compareField: any; path: string }) { + const { changes, compareField, path } = params; if (!changes.added[path]) { const obj = { - path: path, - uid: compareField['uid'], displayName: compareField['display_name'], fieldType: compareField['data_type'], - oldValue: undefined, newValue: compareField, + oldValue: undefined, + path: path, + uid: compareField['uid'], }; changes.added[path] = obj; } } -function prepareDeletedField(params: { path: string; changes: any; baseField: any }) { - const { path, changes, baseField } = params; +function prepareDeletedField(params: { baseField: any; changes: any; path: string }) { + const { baseField, changes, path } = params; if (!changes.added[path]) { const obj = { - path: path, - uid: baseField['uid'], displayName: baseField['display_name'], fieldType: baseField['data_type'], + path: path, + uid: baseField['uid'], }; changes.deleted[path] = obj; } } function prepareModifiedField(params: { - restBaseObj: any; - restCompareObj: any; - currentPath: string; changes: any; + currentPath: string; fullFieldContext: any; parentContext: any; + restBaseObj: any; + restCompareObj: any; }) { - const { restBaseObj, restCompareObj, currentPath, changes, fullFieldContext } = params; + const { changes, currentPath, fullFieldContext, restBaseObj, restCompareObj } = params; const differences = diff(restBaseObj, restCompareObj); if (differences.length) { const modifiedField = { - path: currentPath, - uid: fullFieldContext['uid'] || restCompareObj['uid'], + changeCount: differences.length, displayName: getFieldDisplayName(fullFieldContext) || getFieldDisplayName(restCompareObj) || 'Field', fieldType: restCompareObj['data_type'] || 'field', + path: currentPath, propertyChanges: differences.map((diff) => { let oldValue = 'from' in diff ? diff.from : undefined; - let newValue = diff.value; + const newValue = diff.value; if (!('from' in diff) && fullFieldContext && diff.path && diff.path.length > 0) { const contextValue = extractValueFromPath(fullFieldContext, diff.path); if (contextValue !== undefined) { @@ -615,29 +615,29 @@ function prepareModifiedField(params: { } return { - property: diff.path.join('.'), changeType: diff.op === 'add' ? 'added' : diff.op === 'remove' ? 'deleted' : 'modified', - oldValue: oldValue, newValue: newValue, + oldValue: oldValue, + property: diff.path.join('.'), }; }), - changeCount: differences.length, + uid: fullFieldContext['uid'] || restCompareObj['uid'], }; if (!changes.modified[currentPath]) changes.modified[currentPath] = modifiedField; } } export { + branchCompareSDK, + deepDiff, fetchBranchesDiff, - parseSummary, - printSummary, + filterBranchDiffDataByModule, parseCompactText, - printCompactTextView, + parseSummary, parseVerbose, - printVerboseTextView, - filterBranchDiffDataByModule, - branchCompareSDK, prepareBranchVerboseRes, - deepDiff, prepareModifiedDiff, + printCompactTextView, + printSummary, + printVerboseTextView, }; diff --git a/packages/contentstack-branches/src/utils/create-branch.ts b/packages/contentstack-branches/src/utils/create-branch.ts index 2eb6f5d92..fd0691a93 100644 --- a/packages/contentstack-branches/src/utils/create-branch.ts +++ b/packages/contentstack-branches/src/utils/create-branch.ts @@ -1,6 +1,6 @@ import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; -export async function createBranch(host: string, apiKey: string, branch: { uid: string; source: string }) { +export async function createBranch(host: string, apiKey: string, branch: { source: string; uid: string }) { const managementAPIClient = await managementSDKClient({ host }); managementAPIClient .stack({ api_key: apiKey }) @@ -11,7 +11,7 @@ export async function createBranch(host: string, apiKey: string, branch: { uid: 'Branch creation in progress. Once ready, it will show in the results of the branch list command `csdx cm:branches`', ), ) - .catch((err: { errorCode: number; errorMessage: string, errors:any }) => { + .catch((err: { errorCode: number; errorMessage: string, errors: any }) => { if (err.errorCode === 910) cliux.error(`error : Branch with UID '${branch.uid}' already exists, please enter a unique branch UID`); else if (err.errorCode === 903){ diff --git a/packages/contentstack-branches/src/utils/create-merge-scripts.ts b/packages/contentstack-branches/src/utils/create-merge-scripts.ts index bc1c7c3b3..0d2e08abe 100644 --- a/packages/contentstack-branches/src/utils/create-merge-scripts.ts +++ b/packages/contentstack-branches/src/utils/create-merge-scripts.ts @@ -1,15 +1,16 @@ +import { cliux, formatDate, formatTime } from '@contentstack/cli-utilities'; import fs from 'fs'; -import { cliux, formatTime, formatDate } from '@contentstack/cli-utilities'; + +import { assetFolderCreateScript } from './asset-folder-create-script'; import { entryCreateScript } from './entry-create-script'; -import { entryUpdateScript } from './entry-update-script'; import { entryCreateUpdateScript } from './entry-create-update-script'; -import { assetFolderCreateScript } from './asset-folder-create-script'; +import { entryUpdateScript } from './entry-update-script'; type CreateMergeScriptsProps = { - uid: string; entry_merge_strategy?: string; - type?: string; status?: string; + type?: string; + uid: string; }; export function generateMergeScripts(mergeSummary, mergeJobUID) { @@ -28,8 +29,8 @@ export function generateMergeScripts(mergeSummary, mergeJobUID) { const mergeStrategies = { asset_create_folder: assetFolderCreateScript, - merge_existing_new: entryCreateUpdateScript, merge_existing: entryUpdateScript, + merge_existing_new: entryCreateUpdateScript, merge_new: entryCreateScript, }; @@ -45,7 +46,7 @@ export function generateMergeScripts(mergeSummary, mergeJobUID) { } }; - processContentType({ type: 'assets', uid: '', entry_merge_strategy: '' }, mergeStrategies['asset_create_folder']); + processContentType({ entry_merge_strategy: '', type: 'assets', uid: '' }, mergeStrategies['asset_create_folder']); processContentTypes(mergeSummary.modified, 'Modified'); processContentTypes(mergeSummary.added, 'New'); @@ -90,7 +91,7 @@ export function createMergeScripts(contentType: CreateMergeScriptsProps, mergeJo fs.mkdirSync(fullPath); } let filePath: string; - let milliSeconds = date.getMilliseconds().toString().padStart(3, '0'); + const milliSeconds = date.getMilliseconds().toString().padStart(3, '0'); if (contentType.type === 'assets') { filePath = `${fullPath}/${fileCreatedAt}${milliSeconds}_create_assets_folder.js`; } else { diff --git a/packages/contentstack-branches/src/utils/csv-utility.ts b/packages/contentstack-branches/src/utils/csv-utility.ts index f1328099d..0d5d7e513 100644 --- a/packages/contentstack-branches/src/utils/csv-utility.ts +++ b/packages/contentstack-branches/src/utils/csv-utility.ts @@ -1,7 +1,8 @@ -import { writeFileSync, existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; import { cliux, log, sanitizePath } from '@contentstack/cli-utilities'; -import { BranchDiffVerboseRes, CSVRow, ModifiedFieldsInput, ContentTypeItem, AddCSVRowParams, FIELD_TYPES, CSV_HEADER } from '../interfaces'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +import { AddCSVRowParams, BranchDiffVerboseRes, CSV_HEADER, CSVRow, ContentTypeItem, FIELD_TYPES, ModifiedFieldsInput } from '../interfaces'; /** * Get display name for a field with special handling for system fields @@ -95,7 +96,7 @@ export function formatValue(value: any): string { * @param path - Array of path segments * @returns The value at the path, or undefined if not found */ -export function extractValueFromPath(obj: any, path: (string | number)[]): any { +export function extractValueFromPath(obj: any, path: (number | string)[]): any { if (!obj || !path || path.length === 0) return undefined; try { @@ -179,11 +180,11 @@ function addContentTypeRows( const contentTypeName = item?.title || item?.uid || 'Unknown'; addCSVRow(csvRows, { - srNo: getSrNo(), contentTypeName, fieldName: 'Content Type', fieldType: operation, sourceValue: 'N/A', + srNo: getSrNo(), targetValue: 'N/A' }); } @@ -227,12 +228,12 @@ export function generateCSVDataFromVerbose(verboseRes: BranchDiffVerboseRes): CS */ function addCSVRow(csvRows: CSVRow[], params: AddCSVRowParams): void { csvRows.push({ - srNo: params.srNo, contentTypeName: params.contentTypeName, fieldName: params.fieldName, fieldPath: 'N/A', operation: params.fieldType, sourceBranchValue: params.sourceValue, + srNo: params.srNo, targetBranchValue: params.targetValue, }); } @@ -261,21 +262,21 @@ function addFieldChangesToCSV(csvRows: CSVRow[], params: { if (field.propertyChanges?.length > 0) { field.propertyChanges.forEach(propertyChange => { addCSVRow(csvRows, { - srNo: srNo++, contentTypeName: params.contentTypeName, fieldName, fieldType, sourceValue: formatValue(propertyChange.newValue), + srNo: srNo++, targetValue: formatValue(propertyChange.oldValue) }); }); } else { addCSVRow(csvRows, { - srNo: srNo++, contentTypeName: params.contentTypeName, fieldName, fieldType, sourceValue: fieldType === 'added' ? 'N/A' : formatValue(field), + srNo: srNo++, targetValue: fieldType === 'deleted' ? 'N/A' : formatValue(field) }); } diff --git a/packages/contentstack-branches/src/utils/delete-branch.ts b/packages/contentstack-branches/src/utils/delete-branch.ts index 9c691cbd9..bdb3940bb 100644 --- a/packages/contentstack-branches/src/utils/delete-branch.ts +++ b/packages/contentstack-branches/src/utils/delete-branch.ts @@ -1,4 +1,5 @@ import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; + import { refreshbranchConfig } from '.'; export async function deleteBranch(host: string, apiKey: string, uid: string) { diff --git a/packages/contentstack-branches/src/utils/index.ts b/packages/contentstack-branches/src/utils/index.ts index 64645a298..6220fabc8 100644 --- a/packages/contentstack-branches/src/utils/index.ts +++ b/packages/contentstack-branches/src/utils/index.ts @@ -1,10 +1,11 @@ /** * Command specific utilities can be written here */ +import { cliux, configHandler, messageHandler, sanitizePath } from '@contentstack/cli-utilities'; import fs from 'fs'; -import path from 'path'; import forEach from 'lodash/forEach'; -import { configHandler, cliux, messageHandler, sanitizePath } from '@contentstack/cli-utilities'; +import path from 'path'; + import { MergeParams } from '../interfaces'; export const getbranchesList = (branchResult, baseBranch: string) => { @@ -12,10 +13,10 @@ export const getbranchesList = (branchResult, baseBranch: string) => { branchResult.map((item) => { branches.push({ - Branch: item.uid, - Source: item.source, Aliases: item.alias, + Branch: item.uid, Created: new Date(item.created_at).toLocaleDateString(), + Source: item.source, Updated: new Date(item.updated_at).toLocaleDateString(), }); }); @@ -23,7 +24,7 @@ export const getbranchesList = (branchResult, baseBranch: string) => { const currentBranch = branches.filter((branch) => branch.Branch === baseBranch); const otherBranches = branches.filter((branch) => branch.Branch !== baseBranch); - return { currentBranch, otherBranches, branches }; + return { branches, currentBranch, otherBranches }; }; export const getbranchConfig = (stackApiKey: string) => { @@ -78,8 +79,8 @@ export async function getMergeQueueStatus(stackAPIClient, payload): Promise export async function executeMergeRequest(stackAPIClient, payload): Promise { const { - host, apiKey, + host, params: { base_branch, compare_branch, default_merge_strategy, item_merge_strategies, merge_comment, no_revert }, } = payload; const queryParams: MergeParams = { @@ -139,11 +140,12 @@ export function validateCompareData(branchCompareData) { return validCompareData; } -export * from './interactive'; -export * from './merge-helper'; +export * as branchDiffUtility from './branch-diff-utility'; export * from './create-merge-scripts'; -export * from './entry-update-script'; +export * as deleteBranchUtility from './delete-branch'; export * from './entry-create-script'; +export * from './entry-update-script'; +export * from './interactive'; export * as interactive from './interactive'; -export * as branchDiffUtility from './branch-diff-utility'; -export * as deleteBranchUtility from './delete-branch'; +export * from './merge-helper'; +export * from './merge-status-helper'; diff --git a/packages/contentstack-branches/src/utils/interactive.ts b/packages/contentstack-branches/src/utils/interactive.ts index dc8730fd6..73356b848 100644 --- a/packages/contentstack-branches/src/utils/interactive.ts +++ b/packages/contentstack-branches/src/utils/interactive.ts @@ -1,81 +1,81 @@ -import isEmpty from 'lodash/isEmpty'; -import startCase from 'lodash/startCase'; +import { cliux, messageHandler, validatePath } from '@contentstack/cli-utilities'; import camelCase from 'lodash/camelCase'; import forEach from 'lodash/forEach'; +import isEmpty from 'lodash/isEmpty'; +import startCase from 'lodash/startCase'; import path from 'path'; -import { cliux, messageHandler, validatePath } from '@contentstack/cli-utilities'; import { BranchDiffRes } from '../interfaces'; export async function selectModule(): Promise { return await cliux.inquire({ - type: 'list', - name: 'module', - message: 'CLI_BRANCH_MODULE', choices: [ { name: 'Content Types', value: 'content-types' }, { name: 'Global Fields', value: 'global-fields' }, { name: 'All', value: 'all' }, ], + message: 'CLI_BRANCH_MODULE', + name: 'module', + type: 'list', validate: inquireRequireFieldValidation, }); } export async function askCompareBranch(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_COMPARE_BRANCH', name: 'compare_branch', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askStackAPIKey(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_STACK_API_KEY', name: 'api_key', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askBaseBranch(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_BASE_BRANCH', name: 'branch_branch', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askSourceBranch(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_SOURCE_BRANCH', name: 'source_branch', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askBranchUid(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_BRANCH_UID', name: 'branch_uid', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askConfirmation(): Promise { const resp = await cliux.inquire({ - type: 'confirm', message: 'Are you sure you want to delete this branch?', name: 'confirm', + type: 'confirm', }); return resp; } -export function inquireRequireFieldValidation(input: any): string | boolean { +export function inquireRequireFieldValidation(input: any): boolean | string { if (isEmpty(input)) { return messageHandler.parse('CLI_BRANCH_REQUIRED_FIELD'); } @@ -85,8 +85,6 @@ export function inquireRequireFieldValidation(input: any): string | boolean { export async function selectMergeStrategy(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'Merge, Prefer Base', value: 'merge_prefer_base' }, { name: 'Merge, Prefer Compare', value: 'merge_prefer_compare' }, @@ -94,6 +92,8 @@ export async function selectMergeStrategy(): Promise { { name: 'Overwrite with Compare', value: 'overwrite_with_compare' }, ], message: 'What merge strategy would you like to choose? ', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -107,8 +107,6 @@ export async function selectMergeStrategy(): Promise { export async function selectMergeStrategySubOptions(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'New in Compare Only', value: 'new' }, { name: 'Modified Only', value: 'modified' }, @@ -117,6 +115,8 @@ export async function selectMergeStrategySubOptions(): Promise { { name: 'Start Over', value: 'restart' }, ], message: 'What do you want to merge?', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -130,8 +130,6 @@ export async function selectMergeStrategySubOptions(): Promise { export async function selectMergeExecution(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'Execute Merge', value: 'both' }, { name: 'Export Merge Summary', value: 'export' }, @@ -141,6 +139,8 @@ export async function selectMergeExecution(): Promise { { name: 'Start Over', value: 'restart' }, ], message: 'What would you like to do?', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -154,8 +154,6 @@ export async function selectMergeExecution(): Promise { export async function selectContentMergePreference(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'Both existing and new', value: 'existing_new' }, { name: 'New only', value: 'new' }, @@ -163,6 +161,8 @@ export async function selectContentMergePreference(): Promise { { name: 'Ask for preference', value: 'ask_preference' }, ], message: 'What content entries do you want to migrate?', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -175,9 +175,9 @@ export async function selectContentMergePreference(): Promise { export async function askExportMergeSummaryPath(): Promise { return await cliux.inquire({ - type: 'input', message: 'Enter the file path to export the summary', name: 'filePath', + type: 'input', validate: inquireRequireFieldValidation, }); } @@ -185,9 +185,9 @@ export async function askExportMergeSummaryPath(): Promise { export async function askMergeComment(): Promise { return await cliux.inquire({ - type: 'input', message: 'Enter a comment for merge', name: 'comment', + type: 'input', validate: inquireRequireFieldValidation, }); } @@ -226,11 +226,6 @@ export async function selectCustomPreferences(module, payload) { } const selectedStrategies = await cliux.inquire({ - type: 'table', - message: `Select the ${startCase(camelCase(module))} changes for merge`, - name: 'mergeContentTypePreferences', - selectAll: true, - pageSize: 10, columns: [ { name: 'Merge Prefer Base', @@ -249,10 +244,15 @@ export async function selectCustomPreferences(module, payload) { value: 'ignore', }, ], + message: `Select the ${startCase(camelCase(module))} changes for merge`, + name: 'mergeContentTypePreferences', + pageSize: 10, rows: tableRows, + selectAll: true, + type: 'table', }); - let updatedArray = []; + const updatedArray = []; forEach(selectedStrategies, (strategy: string, index: number) => { const selectedItem = tableRows[index]; if (strategy && selectedItem) { @@ -267,9 +267,9 @@ export async function selectCustomPreferences(module, payload) { export async function askBranchNameConfirmation(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_NAME_CONFIRMATION', name: 'branch_name', + type: 'input', validate: inquireRequireFieldValidation, }); } @@ -298,11 +298,6 @@ export async function selectContentMergeCustomPreferences(payload) { } const selectedStrategies = await cliux.inquire({ - type: 'table', - message: `Select the Content Entry changes for merge`, - name: 'mergeContentEntriesPreferences', - selectAll: true, - pageSize: 10, columns: [ { name: 'Merge New Only', @@ -321,10 +316,15 @@ export async function selectContentMergeCustomPreferences(payload) { value: 'ignore', }, ], + message: `Select the Content Entry changes for merge`, + name: 'mergeContentEntriesPreferences', + pageSize: 10, rows: tableRows, + selectAll: true, + type: 'table', }); - let updatedArray = []; + const updatedArray = []; forEach(selectedStrategies, (strategy: string, index: number) => { const selectedItem = tableRows[index]; diff --git a/packages/contentstack-branches/src/utils/merge-helper.ts b/packages/contentstack-branches/src/utils/merge-helper.ts index 2fbcef286..0d558451e 100644 --- a/packages/contentstack-branches/src/utils/merge-helper.ts +++ b/packages/contentstack-branches/src/utils/merge-helper.ts @@ -1,18 +1,19 @@ -import startCase from 'lodash/startCase'; +import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; import camelCase from 'lodash/camelCase'; +import startCase from 'lodash/startCase'; import path from 'path'; -import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; + import { BranchDiffPayload, MergeSummary } from '../interfaces'; import { + askBaseBranch, askCompareBranch, askStackAPIKey, - askBaseBranch, - getbranchConfig, branchDiffUtility as branchDiff, - writeFile, executeMergeRequest, getMergeQueueStatus, + getbranchConfig, readFile, + writeFile, } from './'; export const prepareMergeRequestPayload = (options) => { @@ -50,12 +51,12 @@ function validateMergeSummary(mergeSummary: MergeSummary) { export const setupMergeInputs = async (mergeFlags) => { if (mergeFlags['use-merge-summary']) { - let mergeSummary: MergeSummary = (await readFile(mergeFlags['use-merge-summary'])) as MergeSummary; + const mergeSummary: MergeSummary = (await readFile(mergeFlags['use-merge-summary'])) as MergeSummary; validateMergeSummary(mergeSummary); mergeFlags.mergeSummary = mergeSummary; } - let { requestPayload: { base_branch = null, compare_branch = null } = {} } = mergeFlags.mergeSummary || {}; + const { requestPayload: { base_branch = null, compare_branch = null } = {} } = mergeFlags.mergeSummary || {}; if (!mergeFlags['stack-api-key']) { mergeFlags['stack-api-key'] = await askStackAPIKey(); @@ -85,12 +86,12 @@ export const setupMergeInputs = async (mergeFlags) => { export const displayBranchStatus = async (options) => { const spinner = cliux.loaderV2('Loading branch differences...'); - let payload: BranchDiffPayload = { - module: '', + const payload: BranchDiffPayload = { apiKey: options.stackAPIKey, baseBranch: options.baseBranch, compareBranch: options.compareBranch, host: options.host, + module: '', }; payload.spinner = spinner; @@ -98,8 +99,8 @@ export const displayBranchStatus = async (options) => { const diffData = branchDiff.filterBranchDiffDataByModule(branchDiffData); cliux.loaderV2('', spinner); - let parsedResponse = {}; - for (let module in diffData) { + const parsedResponse = {}; + for (const module in diffData) { const branchModuleData = diffData[module]; payload.module = module; cliux.print(' '); @@ -125,7 +126,7 @@ export const displayBranchStatus = async (options) => { export const displayMergeSummary = (options) => { cliux.print(' '); cliux.print(`Merge Summary:`, { color: 'yellow' }); - for (let module in options.compareData) { + for (const module in options.compareData) { if (options.format === 'compact-text') { branchDiff.printCompactTextView(options.compareData[module]); } else if (options.format === 'detailed-text') { @@ -135,6 +136,16 @@ export const displayMergeSummary = (options) => { cliux.print(' '); }; +/** + * Executes a merge request and waits for completion with limited polling. + * If the merge is in_progress, polls for status with max 10 retries and exponential backoff. + * Returns immediately if merge is complete, throws error if failed. + * + * @param apiKey - Stack API key + * @param mergePayload - Merge request payload + * @param host - API host + * @returns Promise - Merge response with status and details + */ export const executeMerge = async (apiKey, mergePayload, host): Promise => { const stackAPIClient = await (await managementSDKClient({ host })).stack({ api_key: apiKey }); const mergeResponse = await executeMergeRequest(stackAPIClient, { params: mergePayload }); @@ -147,31 +158,61 @@ export const executeMerge = async (apiKey, mergePayload, host): Promise => } }; -export const fetchMergeStatus = async (stackAPIClient, mergePayload, delay = 5000): Promise => { - return new Promise(async (resolve, reject) => { +/** + * Fetches merge status with retry-limited polling (max 10 attempts) and exponential backoff. + * Returns a structured response on polling timeout instead of throwing an error. + * + * @param stackAPIClient - The stack API client for making requests + * @param mergePayload - The merge payload containing the UID + * @param initialDelay - Initial delay between retries in milliseconds (default: 5000ms) + * @param maxRetries - Maximum number of retry attempts (default: 10) + * @returns Promise - Merge response object with optional pollingTimeout flag + */ +export const fetchMergeStatus = async ( + stackAPIClient, + mergePayload, + initialDelay = 5000, + maxRetries = 10000, // Temporary making infinite polling to unblock the users +): Promise => { + let delayMs = initialDelay; + const maxDelayMs = 60000; // Cap delay at 60 seconds + + for (let attempt = 1; attempt <= maxRetries; attempt++) { const mergeStatusResponse = await getMergeQueueStatus(stackAPIClient, { uid: mergePayload.uid }); if (mergeStatusResponse?.queue?.length >= 1) { const mergeRequestStatusResponse = mergeStatusResponse.queue[0]; const mergeStatus = mergeRequestStatusResponse.merge_details?.status; + if (mergeStatus === 'complete') { - resolve(mergeRequestStatusResponse); + return mergeRequestStatusResponse; } else if (mergeStatus === 'in-progress' || mergeStatus === 'in_progress') { - setTimeout(async () => { - await fetchMergeStatus(stackAPIClient, mergePayload, delay).then(resolve).catch(reject); - }, delay); + if (attempt < maxRetries) { + cliux.print(`Merge in progress... (Attempt ${attempt}/${maxRetries})`, { color: 'grey' }); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + delayMs = Math.min(delayMs + 1000, maxDelayMs); + } else { + // Polling timeout: return structured response instead of throwing + cliux.print(`Merge in progress... (Attempt ${attempt}/${maxRetries})`, { color: 'grey' }); + return { + merge_details: mergeRequestStatusResponse.merge_details, + pollingTimeout: true, + status: 'in_progress', + uid: mergePayload.uid, + }; + } } else if (mergeStatus === 'failed') { if (mergeRequestStatusResponse?.errors?.length > 0) { const errorPath = path.join(process.cwd(), 'merge-error.log'); await writeFile(errorPath, mergeRequestStatusResponse.errors); cliux.print(`\nComplete error log can be found in ${path.resolve(errorPath)}`, { color: 'grey' }); } - return reject(`merge uid: ${mergePayload.uid}`); + throw new Error(`merge uid: ${mergePayload.uid}`); } else { - return reject(`Invalid merge status found with merge ID ${mergePayload.uid}`); + throw new Error(`Invalid merge status found with merge ID ${mergePayload.uid}`); } } else { - return reject(`No queue found with merge ID ${mergePayload.uid}`); + throw new Error(`No queue found with merge ID ${mergePayload.uid}`); } - }); + } }; diff --git a/packages/contentstack-branches/src/utils/merge-status-helper.ts b/packages/contentstack-branches/src/utils/merge-status-helper.ts new file mode 100644 index 000000000..4f4d4f83c --- /dev/null +++ b/packages/contentstack-branches/src/utils/merge-status-helper.ts @@ -0,0 +1,165 @@ +import { cliux } from '@contentstack/cli-utilities'; + +import { getMergeQueueStatus } from './'; + +/** + * Maps merge status to a user-friendly message with visual indicator. + * @param status - The merge status (complete, in_progress, failed, or unknown) + * @returns User-friendly status message + */ +export const getMergeStatusMessage = (status: string): string => { + switch (status) { + case 'complete': + return '✅ Merge completed successfully'; + case 'in_progress': + case 'in-progress': + return '⏳ Merge is still processing'; + case 'failed': + return '❌ Merge failed'; + default: + return '⚠️ Unknown status'; + } +}; + +/** + * Formats and displays merge status details in a user-friendly format. + * Shows merge metadata, summary statistics, and errors if present. + * @param mergeResponse - The merge response object containing status details + */ +export const displayMergeStatusDetails = (mergeResponse: any): void => { + if (!mergeResponse) { + cliux.print('No merge information available', { color: 'yellow' }); + return; + } + + const { errors = [], merge_details = {}, merge_summary = {}, uid } = mergeResponse; + const status = merge_details.status || 'unknown'; + const statusMessage = getMergeStatusMessage(status); + + const statusColor = getStatusColor(status); + + cliux.print(' '); + cliux.print(`${statusMessage}`, { color: statusColor }); + + cliux.print(' '); + cliux.print('Merge Details:', { color: 'cyan' }); + cliux.print(` ├─ Merge UID: ${uid}`, { color: 'grey' }); + + if (merge_details.created_at) { + cliux.print(` ├─ Created: ${merge_details.created_at}`, { color: 'grey' }); + } + + if (merge_details.updated_at) { + cliux.print(` ├─ Updated: ${merge_details.updated_at}`, { color: 'grey' }); + } + + if (merge_details.completed_at && status === 'complete') { + cliux.print(` ├─ Completed: ${merge_details.completed_at}`, { color: 'grey' }); + } + + if (merge_details.completion_percentage !== undefined && status === 'in_progress') { + cliux.print(` ├─ Progress: ${merge_details.completion_percentage}%`, { color: 'grey' }); + } + + const statusIndicator = status === 'complete' ? ' ✓' : ''; + cliux.print(` └─ Status: ${status}${statusIndicator}`, { color: 'grey' }); + + displayMergeSummary(merge_summary); + displayMergeErrors(errors); + + cliux.print(' '); +}; + +/** + * Gets the appropriate color for the status message + * @param status - The merge status + * @returns The color name (green, red, or yellow) + */ +const getStatusColor = (status: string): 'green' | 'red' | 'yellow' => { + if (status === 'complete') return 'green'; + if (status === 'failed') return 'red'; + return 'yellow'; +}; + +/** + * Displays the merge summary statistics + * @param merge_summary - The merge summary object containing content types and global fields stats + */ +const displayMergeSummary = (merge_summary: any): void => { + if (!merge_summary || (!merge_summary.content_types && !merge_summary.global_fields)) { + return; + } + + cliux.print(' '); + cliux.print('Summary:', { color: 'cyan' }); + + if (merge_summary.content_types) { + const ct = merge_summary.content_types; + const added = ct.added || 0; + const modified = ct.modified || 0; + const deleted = ct.deleted || 0; + cliux.print(` ├─ Content Types: +${added}, ~${modified}, -${deleted}`, { color: 'grey' }); + } + + if (merge_summary.global_fields) { + const gf = merge_summary.global_fields; + const added = gf.added || 0; + const modified = gf.modified || 0; + const deleted = gf.deleted || 0; + cliux.print(` └─ Global Fields: +${added}, ~${modified}, -${deleted}`, { color: 'grey' }); + } +}; + +/** + * Displays merge errors if any exist + * @param errors - Array of error objects to display + */ +const displayMergeErrors = (errors: any[]): void => { + if (!errors || errors.length === 0) { + return; + } + + cliux.print(' '); + cliux.print('Errors:', { color: 'red' }); + errors.forEach((error, index) => { + const isLast = index === errors.length - 1; + const prefix = isLast ? '└─' : '├─'; + cliux.print(` ${prefix} ${error.message || error}`, { color: 'grey' }); + }); +}; + +/** + * Fetches merge status and extracts content type data for script generation. + * Validates that the merge status is 'complete' before returning content type data. + * @param stackAPIClient - The stack API client for making requests + * @param mergeUID - The merge job UID + * @returns Promise - Merge status response with content type data or error + */ +export const getMergeStatusWithContentTypes = async ( + stackAPIClient, + mergeUID: string +): Promise => { + try { + const mergeStatusResponse = await getMergeQueueStatus(stackAPIClient, { uid: mergeUID }); + + if (!mergeStatusResponse?.queue?.length) { + throw new Error(`No merge job found with UID: ${mergeUID}`); + } + + const mergeRequestStatusResponse = mergeStatusResponse.queue[0]; + const mergeStatus = mergeRequestStatusResponse.merge_details?.status; + + if (mergeStatus !== 'complete') { + return { + error: `Merge job is not complete. Current status: ${mergeStatus}`, + merge_details: mergeRequestStatusResponse.merge_details, + status: mergeStatus, + uid: mergeUID, + }; + } + + return mergeRequestStatusResponse; + } catch (error) { + throw new Error(`Failed to fetch merge status: ${error.message || error}`); + } +}; diff --git a/packages/contentstack-branches/test/unit/commands/cm/branches/merge-status.test.ts b/packages/contentstack-branches/test/unit/commands/cm/branches/merge-status.test.ts new file mode 100644 index 000000000..a43a6d66f --- /dev/null +++ b/packages/contentstack-branches/test/unit/commands/cm/branches/merge-status.test.ts @@ -0,0 +1,49 @@ +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import { stub } from 'sinon'; +import { cliux } from '@contentstack/cli-utilities'; +import BranchMergeStatusCommand from '../../../../../src/commands/cm/branches/merge-status'; +import * as utils from '../../../../../src/utils'; + +describe('Merge Status Command', () => { + let printStub; + let loaderStub; + let isAuthenticatedStub; + let managementSDKClientStub; + let displayMergeStatusDetailsStub; + + beforeEach(() => { + printStub = stub(cliux, 'print'); + loaderStub = stub(cliux, 'loaderV2').returns('spinner'); + isAuthenticatedStub = stub().returns(true); + managementSDKClientStub = stub(); + displayMergeStatusDetailsStub = stub(utils, 'displayMergeStatusDetails'); + }); + + afterEach(() => { + printStub.restore(); + loaderStub.restore(); + isAuthenticatedStub.restore(); + managementSDKClientStub.restore(); + displayMergeStatusDetailsStub.restore(); + }); + + it('should have correct description', () => { + expect(BranchMergeStatusCommand.description).to.equal('Check the status of a branch merge job'); + }); + + it('should have correct usage', () => { + expect(BranchMergeStatusCommand.usage).to.equal('cm:branches:merge-status -k --merge-uid '); + }); + + it('should have example command', () => { + expect(BranchMergeStatusCommand.examples.length).to.be.greaterThan(0); + expect(BranchMergeStatusCommand.examples[0]).to.include('merge-status'); + expect(BranchMergeStatusCommand.examples[0]).to.include('merge_abc123'); + }); + + it('should have required flags', () => { + expect(BranchMergeStatusCommand.flags['stack-api-key'].required).to.be.true; + expect(BranchMergeStatusCommand.flags['merge-uid'].required).to.be.true; + }); +}); diff --git a/packages/contentstack-branches/test/unit/utils/merge-status-helper.test.ts b/packages/contentstack-branches/test/unit/utils/merge-status-helper.test.ts new file mode 100644 index 000000000..61bc2e047 --- /dev/null +++ b/packages/contentstack-branches/test/unit/utils/merge-status-helper.test.ts @@ -0,0 +1,229 @@ +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import { stub } from 'sinon'; +import { cliux } from '@contentstack/cli-utilities'; +import { displayMergeStatusDetails, getMergeStatusMessage, getMergeStatusWithContentTypes } from '../../../../../src/utils/merge-status-helper'; +import * as utils from '../../../../../src/utils'; + +describe('Merge Status Helper', () => { + let printStub; + + beforeEach(() => { + printStub = stub(cliux, 'print'); + }); + + afterEach(() => { + printStub.restore(); + }); + + describe('getMergeStatusMessage', () => { + it('should return complete status message for complete status', () => { + const message = getMergeStatusMessage('complete'); + expect(message).to.equal('✅ Merge completed successfully'); + }); + + it('should return in_progress status message for in_progress status', () => { + const message = getMergeStatusMessage('in_progress'); + expect(message).to.equal('⏳ Merge is still processing'); + }); + + it('should return in_progress status message for in-progress status', () => { + const message = getMergeStatusMessage('in-progress'); + expect(message).to.equal('⏳ Merge is still processing'); + }); + + it('should return failed status message for failed status', () => { + const message = getMergeStatusMessage('failed'); + expect(message).to.equal('❌ Merge failed'); + }); + + it('should return unknown status message for unknown status', () => { + const message = getMergeStatusMessage('unknown'); + expect(message).to.equal('⚠️ Unknown status'); + }); + }); + + describe('displayMergeStatusDetails', () => { + it('should display merge status details for completed merge', () => { + const mergeResponse = { + uid: 'merge_123', + merge_details: { + status: 'complete', + created_at: '2024-01-01T10:00:00Z', + updated_at: '2024-01-01T10:30:00Z', + completed_at: '2024-01-01T10:30:00Z', + }, + merge_summary: { + content_types: { added: 2, modified: 3, deleted: 1 }, + global_fields: { added: 0, modified: 1, deleted: 0 }, + }, + errors: [], + }; + + displayMergeStatusDetails(mergeResponse); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + const printed = calls.map((c) => c.args[0]).join(' '); + expect(printed).to.include('merge_123'); + expect(printed).to.include('complete'); + }); + + it('should display merge status details for in-progress merge', () => { + const mergeResponse = { + uid: 'merge_456', + merge_details: { + status: 'in_progress', + created_at: '2024-01-01T10:00:00Z', + updated_at: '2024-01-01T10:15:00Z', + completion_percentage: 60, + }, + merge_summary: { + content_types: { added: 1, modified: 2, deleted: 0 }, + global_fields: { added: 0, modified: 0, deleted: 0 }, + }, + errors: [], + }; + + displayMergeStatusDetails(mergeResponse); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + const printed = calls.map((c) => c.args[0]).join(' '); + expect(printed).to.include('merge_456'); + expect(printed).to.include('in_progress'); + expect(printed).to.include('60'); + }); + + it('should display merge status details with errors', () => { + const mergeResponse = { + uid: 'merge_789', + merge_details: { + status: 'failed', + created_at: '2024-01-01T10:00:00Z', + updated_at: '2024-01-01T10:20:00Z', + }, + merge_summary: { + content_types: { added: 0, modified: 0, deleted: 0 }, + global_fields: { added: 0, modified: 0, deleted: 0 }, + }, + errors: [{ message: 'Content type conflict' }, { message: 'Field mismatch' }], + }; + + displayMergeStatusDetails(mergeResponse); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + const printed = calls.map((c) => c.args[0]).join(' '); + expect(printed).to.include('merge_789'); + expect(printed).to.include('failed'); + expect(printed).to.include('conflict'); + }); + + it('should handle null merge response gracefully', () => { + displayMergeStatusDetails(null); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + expect(calls[0].args[0]).to.equal('No merge information available'); + }); + + it('should handle undefined merge response gracefully', () => { + displayMergeStatusDetails(undefined); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + expect(calls[0].args[0]).to.equal('No merge information available'); + }); + }); + + describe('getMergeStatusWithContentTypes', () => { + let getMergeQueueStatusStub; + + beforeEach(() => { + getMergeQueueStatusStub = stub(utils, 'getMergeQueueStatus'); + }); + + afterEach(() => { + getMergeQueueStatusStub.restore(); + }); + + it('should return merge response when merge is complete', async () => { + const mockMergeResponse = { + queue: [ + { + uid: 'merge_complete', + merge_details: { status: 'complete' }, + content_types: { added: [], modified: [], deleted: [] }, + }, + ], + }; + + getMergeQueueStatusStub.resolves(mockMergeResponse); + + const result = await getMergeStatusWithContentTypes({}, 'merge_complete'); + + expect(result.uid).to.equal('merge_complete'); + expect(result.merge_details.status).to.equal('complete'); + }); + + it('should return error when merge is in_progress', async () => { + const mockMergeResponse = { + queue: [ + { + uid: 'merge_inprogress', + merge_details: { status: 'in_progress' }, + }, + ], + }; + + getMergeQueueStatusStub.resolves(mockMergeResponse); + + const result = await getMergeStatusWithContentTypes({}, 'merge_inprogress'); + + expect(result.error).to.exist; + expect(result.error).to.include('not complete'); + expect(result.status).to.equal('in_progress'); + }); + + it('should return error when merge is failed', async () => { + const mockMergeResponse = { + queue: [ + { + uid: 'merge_failed', + merge_details: { status: 'failed' }, + }, + ], + }; + + getMergeQueueStatusStub.resolves(mockMergeResponse); + + const result = await getMergeStatusWithContentTypes({}, 'merge_failed'); + + expect(result.error).to.exist; + expect(result.error).to.include('not complete'); + }); + + it('should throw error when no queue found', async () => { + getMergeQueueStatusStub.resolves({ queue: [] }); + + try { + await getMergeStatusWithContentTypes({}, 'merge_notfound'); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error.message).to.include('No merge job found'); + } + }); + + it('should throw error when response is invalid', async () => { + getMergeQueueStatusStub.resolves(null); + + try { + await getMergeStatusWithContentTypes({}, 'merge_invalid'); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error.message).to.include('No merge job found'); + } + }); + }); +}); diff --git a/packages/contentstack-bulk-publish/package.json b/packages/contentstack-bulk-publish/package.json index 7d098a884..af6cb7212 100644 --- a/packages/contentstack-bulk-publish/package.json +++ b/packages/contentstack-bulk-publish/package.json @@ -13,7 +13,7 @@ "chalk": "^4.1.2", "dotenv": "^16.5.0", "inquirer": "8.2.7", - "lodash": "4.18.1", + "lodash": "^4.18.1", "winston": "^3.17.0" }, "devDependencies": { diff --git a/packages/contentstack-clone/package.json b/packages/contentstack-clone/package.json index 74a5c76cc..c46eec96a 100644 --- a/packages/contentstack-clone/package.json +++ b/packages/contentstack-clone/package.json @@ -14,7 +14,7 @@ "@oclif/plugin-help": "^6.2.28", "chalk": "^4.1.2", "inquirer": "8.2.7", - "lodash": "4.18.1", + "lodash": "^4.18.1", "merge": "^2.1.1", "ora": "^5.4.1", "prompt": "^1.3.0", diff --git a/packages/contentstack-export/package.json b/packages/contentstack-export/package.json index d4ab0955f..02bf71d10 100644 --- a/packages/contentstack-export/package.json +++ b/packages/contentstack-export/package.json @@ -13,7 +13,7 @@ "big-json": "^3.2.0", "bluebird": "^3.7.2", "chalk": "^4.1.2", - "lodash": "4.18.1", + "lodash": "^4.18.1", "merge": "^2.1.1", "mkdirp": "^1.0.4", "progress-stream": "^2.0.0", diff --git a/packages/contentstack-import-setup/package.json b/packages/contentstack-import-setup/package.json index 4ae3435db..63fb39a8f 100644 --- a/packages/contentstack-import-setup/package.json +++ b/packages/contentstack-import-setup/package.json @@ -11,7 +11,7 @@ "big-json": "^3.2.0", "chalk": "^4.1.2", "fs-extra": "^11.3.0", - "lodash": "4.18.1", + "lodash": "^4.18.1", "merge": "^2.1.1", "mkdirp": "^1.0.4", "winston": "^3.17.0" diff --git a/packages/contentstack-import/package.json b/packages/contentstack-import/package.json index 27906e1ae..8b2c295cf 100644 --- a/packages/contentstack-import/package.json +++ b/packages/contentstack-import/package.json @@ -15,7 +15,7 @@ "chalk": "^4.1.2", "debug": "^4.4.3", "fs-extra": "^11.3.3", - "lodash": "4.18.1", + "lodash": "^4.18.1", "marked": "^4.3.0", "merge": "^2.1.1", "mkdirp": "^1.0.4", diff --git a/packages/contentstack-variants/package.json b/packages/contentstack-variants/package.json index d0eba99fb..a630918fa 100644 --- a/packages/contentstack-variants/package.json +++ b/packages/contentstack-variants/package.json @@ -31,7 +31,7 @@ "@contentstack/cli-utilities": "~1.18.0", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", - "lodash": "4.18.1", + "lodash": "^4.18.1", "mkdirp": "^1.0.4", "winston": "^3.17.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 592bdbb0e..1deefd74c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,8 @@ settings: overrides: picomatch: 4.0.4 brace-expansion: 5.0.5 + lodash: 4.18.1 + tmp: 0.2.4 importers: @@ -33,9 +35,6 @@ importers: '@oclif/plugin-help': specifier: ^6.2.28 version: 6.2.37 - '@oclif/plugin-plugins': - specifier: ^5.4.54 - version: 5.4.56 chalk: specifier: ^4.1.2 version: 4.1.2 @@ -174,8 +173,8 @@ importers: specifier: ^4.17.46 version: 4.22.81(@types/node@14.18.63) tmp: - specifier: ^0.2.3 - version: 0.2.5 + specifier: 0.2.4 + version: 0.2.4 ts-node: specifier: ^8.10.2 version: 8.10.2(typescript@4.9.5) @@ -899,8 +898,8 @@ importers: specifier: ^7.5.11 version: 7.5.11 tmp: - specifier: ^0.2.5 - version: 0.2.5 + specifier: 0.2.4 + version: 0.2.4 devDependencies: '@types/inquirer': specifier: ^9.0.9 @@ -1945,10 +1944,6 @@ packages: resolution: {integrity: sha512-6RD/EuIUGxAYR45nMQg+nw+PqwCXUxkR6Eyn+1fvbVjtb9d+60OPwB77LCRUI4zKNI+n0LOFaMniEdSpb+A7kQ==} engines: {node: '>=18.0.0'} - '@oclif/plugin-plugins@5.4.56': - resolution: {integrity: sha512-mZjRudlmVSr6Stz0CVFuaIZOjwZ5DqjWepQCR/yK9nbs8YunGautpuxBx/CcqaEH29xiQfsuNOIUWa1w/+3VSA==} - engines: {node: '>=18.0.0'} - '@oclif/plugin-warn-if-update-available@3.1.55': resolution: {integrity: sha512-VIEBoaoMOCjl3y+w/kdfZMODi0mVMnDuM0vkBf3nqeidhRXVXq87hBqYDdRwN1XoD+eDfE8tBbOP7qtSOONztQ==} engines: {node: '>=18.0.0'} @@ -4573,10 +4568,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.5: - resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} - engines: {node: '>=18'} - isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} @@ -5227,10 +5218,6 @@ packages: resolution: {integrity: sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==} engines: {node: '>=14.16'} - npm-package-arg@11.0.3: - resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} - engines: {node: ^16.14.0 || >=18.0.0} - npm-run-path@2.0.2: resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} engines: {node: '>=4'} @@ -5239,84 +5226,6 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - npm@10.9.4: - resolution: {integrity: sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - bundledDependencies: - - '@isaacs/string-locale-compare' - - '@npmcli/arborist' - - '@npmcli/config' - - '@npmcli/fs' - - '@npmcli/map-workspaces' - - '@npmcli/package-json' - - '@npmcli/promise-spawn' - - '@npmcli/redact' - - '@npmcli/run-script' - - '@sigstore/tuf' - - abbrev - - archy - - cacache - - chalk - - ci-info - - cli-columns - - fastest-levenshtein - - fs-minipass - - glob - - graceful-fs - - hosted-git-info - - ini - - init-package-json - - is-cidr - - json-parse-even-better-errors - - libnpmaccess - - libnpmdiff - - libnpmexec - - libnpmfund - - libnpmhook - - libnpmorg - - libnpmpack - - libnpmpublish - - libnpmsearch - - libnpmteam - - libnpmversion - - make-fetch-happen - - minimatch - - minipass - - minipass-pipeline - - ms - - node-gyp - - nopt - - normalize-package-data - - npm-audit-report - - npm-install-checks - - npm-package-arg - - npm-pick-manifest - - npm-profile - - npm-registry-fetch - - npm-user-validate - - p-map - - pacote - - parse-conflict-json - - proc-log - - qrcode-terminal - - read - - semver - - spdx-expression-parse - - ssri - - supports-color - - tar - - text-table - - tiny-relative-date - - treeverse - - validate-npm-package-name - - which - - write-file-atomic - number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} engines: {node: '>=0.10.0'} @@ -5349,10 +5258,6 @@ packages: resolution: {integrity: sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==} engines: {node: '>=8.0.0'} - object-treeify@4.0.1: - resolution: {integrity: sha512-Y6tg5rHfsefSkfKujv2SwHulInROy/rCL5F4w0QOWxut8AnxYxf0YmNhTh95Zfyxpsudo66uqkux0ACFnyMSgQ==} - engines: {node: '>= 16'} - object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -5400,10 +5305,6 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - otplib@12.0.1: resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} @@ -5512,10 +5413,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -5580,10 +5477,6 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - proc-log@4.2.0: - resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -6223,12 +6116,8 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + tmp@0.2.4: + resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==} engines: {node: '>=14.14'} tmpl@1.0.5: @@ -6543,11 +6432,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} @@ -6653,11 +6537,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yarn@1.22.22: - resolution: {integrity: sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==} - engines: {node: '>=4.0.0'} - hasBin: true - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -8573,22 +8452,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@oclif/plugin-plugins@5.4.56': - dependencies: - '@oclif/core': 4.8.1 - ansis: 3.17.0 - debug: 4.4.3(supports-color@8.1.1) - npm: 10.9.4 - npm-package-arg: 11.0.3 - npm-run-path: 5.3.0 - object-treeify: 4.0.1 - semver: 7.7.4 - validate-npm-package-name: 5.0.1 - which: 4.0.0 - yarn: 1.22.22 - transitivePeerDependencies: - - supports-color - '@oclif/plugin-warn-if-update-available@3.1.55': dependencies: '@oclif/core': 4.8.1 @@ -11435,7 +11298,7 @@ snapshots: dependencies: chardet: 0.4.2 iconv-lite: 0.4.24 - tmp: 0.0.33 + tmp: 0.2.4 eyes@0.1.8: {} @@ -12186,8 +12049,6 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.5: {} - isstream@0.1.2: {} istanbul-lib-coverage@3.2.2: {} @@ -13044,13 +12905,6 @@ snapshots: normalize-url@8.1.1: {} - npm-package-arg@11.0.3: - dependencies: - hosted-git-info: 7.0.2 - proc-log: 4.2.0 - semver: 7.7.4 - validate-npm-package-name: 5.0.1 - npm-run-path@2.0.2: dependencies: path-key: 2.0.1 @@ -13059,12 +12913,6 @@ snapshots: dependencies: path-key: 3.1.1 - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - npm@10.9.4: {} - number-is-nan@1.0.1: {} nyc@15.1.0: @@ -13114,8 +12962,6 @@ snapshots: object-to-spawn-args@2.0.1: {} - object-treeify@4.0.1: {} - object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -13281,8 +13127,6 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - os-tmpdir@1.0.2: {} - otplib@12.0.1: dependencies: '@otplib/core': 12.0.1 @@ -13387,8 +13231,6 @@ snapshots: path-key@3.1.1: {} - path-key@4.0.0: {} - path-parse@1.0.7: {} path-scurry@2.0.2: @@ -13439,8 +13281,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - proc-log@4.2.0: {} - process-nextick-args@2.0.1: {} process-on-spawn@1.1.0: @@ -14127,11 +13967,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - - tmp@0.2.5: {} + tmp@0.2.4: {} tmpl@1.0.5: {} @@ -14551,10 +14387,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@4.0.0: - dependencies: - isexe: 3.1.5 - widest-line@3.1.0: dependencies: string-width: 4.2.3 @@ -14694,8 +14526,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yarn@1.22.22: {} - yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 721daf770..f8b70b785 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,5 @@ packages: overrides: picomatch: 4.0.4 brace-expansion: 5.0.5 + lodash: 4.18.1 + tmp: 0.2.4